Merge pull request #178 from glyptodon/extension-format

GUAC-587: Adopt extension format including manifest.
This commit is contained in:
James Muehlner
2015-05-13 20:44:39 -07:00
35 changed files with 1385 additions and 426 deletions

View File

@@ -98,6 +98,18 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
@Inject
private ConnectionRecordMapper connectionRecordMapper;
/**
* The hostname to use when connecting to guacd if no hostname is provided
* within guacamole.properties.
*/
private static final String DEFAULT_GUACD_HOSTNAME = "localhost";
/**
* The port to use when connecting to guacd if no port is provided within
* guacamole.properties.
*/
private static final int DEFAULT_GUACD_PORT = 4822;
/**
* All active connections through the tunnel having a given UUID.
*/
@@ -266,17 +278,17 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
throws GuacamoleException {
// Use SSL if requested
if (environment.getProperty(Environment.GUACD_SSL, true))
return new ManagedInetGuacamoleSocket(
environment.getRequiredProperty(Environment.GUACD_HOSTNAME),
environment.getRequiredProperty(Environment.GUACD_PORT),
if (environment.getProperty(Environment.GUACD_SSL, false))
return new ManagedSSLGuacamoleSocket(
environment.getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME),
environment.getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT),
socketClosedCallback
);
// Otherwise, just use straight TCP
return new ManagedInetGuacamoleSocket(
environment.getRequiredProperty(Environment.GUACD_HOSTNAME),
environment.getRequiredProperty(Environment.GUACD_PORT),
environment.getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME),
environment.getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT),
socketClosedCallback
);

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.auth.jdbc.tunnel;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.SSLGuacamoleSocket;
/**
* Implementation of GuacamoleSocket which connects via SSL to a given hostname
* and port. If the socket is closed for any reason, a given task is run.
*
* @author Michael Jumper
*/
public class ManagedSSLGuacamoleSocket extends SSLGuacamoleSocket {
/**
* The task to run when the socket is closed.
*/
private final Runnable socketClosedTask;
/**
* Creates a new socket which connects via SSL to a given hostname and
* port. If the socket is closed for any reason, the given task is run.
*
* @param hostname
* The hostname of the Guacamole proxy server to connect to.
*
* @param port
* The port of the Guacamole proxy server to connect to.
*
* @param socketClosedTask
* The task to run when the socket is closed. This task will NOT be
* run if an exception occurs during connection, and this
* ManagedInetGuacamoleSocket instance is ultimately not created.
*
* @throws GuacamoleException
* If an error occurs while connecting to the Guacamole proxy server.
*/
public ManagedSSLGuacamoleSocket(String hostname, int port,
Runnable socketClosedTask) throws GuacamoleException {
super(hostname, port);
this.socketClosedTask = socketClosedTask;
}
@Override
public void close() throws GuacamoleException {
super.close();
socketClosedTask.run();
}
}

View File

@@ -51,6 +51,7 @@
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>

View File

@@ -0,0 +1,13 @@
{
"guacamoleVersion" : "0.9.6",
"name" : "MySQL Authentication",
"namespace" : "guac-mysql",
"authProviders" : [
"net.sourceforge.guacamole.net.auth.mysql.MySQLAuthenticationProvider"
]
}

View File

@@ -51,6 +51,7 @@
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>

View File

@@ -0,0 +1,13 @@
{
"guacamoleVersion" : "0.9.6",
"name" : "PostgreSQL Authentication",
"namespace" : "guac-postgresql",
"authProviders" : [
"org.glyptodon.guacamole.auth.postgresql.PostgreSQLAuthenticationProvider"
]
}

View File

@@ -32,6 +32,26 @@
</configuration>
</plugin>
<!-- Copy dependencies prior to packaging -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Assembly plugin - for easy distribution -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
@@ -64,6 +84,7 @@
<groupId>org.glyptodon.guacamole</groupId>
<artifactId>guacamole-common</artifactId>
<version>0.9.6</version>
<scope>provided</scope>
</dependency>
<!-- Guacamole Extension API -->
@@ -71,6 +92,7 @@
<groupId>org.glyptodon.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>0.9.6</version>
<scope>provided</scope>
</dependency>
<!-- JLDAP -->

View File

@@ -11,43 +11,29 @@
<format>tar.gz</format>
</formats>
<!-- Include docs and schema -->
<!-- Include docs, schema, and extension .jar -->
<fileSets>
<!-- Include docs -->
<fileSet>
<directory>doc</directory>
</fileSet>
<!-- Include docs -->
<fileSet>
<directory>doc</directory>
</fileSet>
<!-- Include schema -->
<fileSet>
<outputDirectory>schema</outputDirectory>
<directory>schema</directory>
</fileSet>
<!-- Include schema -->
<fileSet>
<outputDirectory>schema</outputDirectory>
<directory>schema</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
<!-- Include self and all dependencies except guacamole-common
and guacamole-ext -->
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<scope>runtime</scope>
<unpack>false</unpack>
<useProjectArtifact>true</useProjectArtifact>
<useTransitiveFiltering>true</useTransitiveFiltering>
<excludes>
<!-- Do not include guacamole-common -->
<exclude>org.glyptodon.guacamole:guacamole-common</exclude>
<!-- Do not include guacamole-ext -->
<exclude>org.glyptodon.guacamole:guacamole-ext</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,12 @@
{
"guacamoleVersion" : "0.9.6",
"name" : "LDAP Authentication",
"namespace" : "guac-ldap",
"authProviders" : [
"net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider"
]
}

View File

@@ -32,6 +32,26 @@
</configuration>
</plugin>
<!-- Copy dependencies prior to packaging -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Assembly plugin - for easy distribution -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
@@ -64,6 +84,7 @@
<groupId>org.glyptodon.guacamole</groupId>
<artifactId>guacamole-common</artifactId>
<version>0.9.6</version>
<scope>provided</scope>
</dependency>
<!-- Guacamole Extension API -->
@@ -71,6 +92,7 @@
<groupId>org.glyptodon.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>0.9.6</version>
<scope>provided</scope>
</dependency>
</dependencies>

View File

@@ -11,7 +11,7 @@
<format>tar.gz</format>
</formats>
<!-- Include docs and schema -->
<!-- Include docs and extension .jar -->
<fileSets>
<!-- Include docs -->
@@ -19,29 +19,15 @@
<directory>doc</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
<!-- Include self and all dependencies except guacamole-common
and guacamole-ext -->
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<scope>runtime</scope>
<unpack>false</unpack>
<useProjectArtifact>true</useProjectArtifact>
<useTransitiveFiltering>true</useTransitiveFiltering>
<excludes>
<!-- Do not include guacamole-common -->
<exclude>org.glyptodon.guacamole:guacamole-common</exclude>
<!-- Do not include guacamole-ext -->
<exclude>org.glyptodon.guacamole:guacamole-ext</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -54,7 +54,6 @@ import org.xml.sax.helpers.XMLReaderFactory;
*
* Example `guacamole.properties`:
*
* auth-provider: net.sourceforge.guacamole.net.auth.noauth.NoAuthenticationProvider
* noauth-config: /etc/guacamole/noauth-config.xml
*
*
@@ -93,7 +92,7 @@ public class NoAuthenticationProvider extends SimpleAuthenticationProvider {
private final Environment environment;
/**
* The filename of the XML file to read the user mapping from.
* The XML file to read the configuration from.
*/
public static final FileGuacamoleProperty NOAUTH_CONFIG = new FileGuacamoleProperty() {
@@ -104,6 +103,12 @@ public class NoAuthenticationProvider extends SimpleAuthenticationProvider {
};
/**
* The default filename to use for the configuration, if not defined within
* guacamole.properties.
*/
public static final String DEFAULT_NOAUTH_CONFIG = "noauth-config.xml";
/**
* Creates a new NoAuthenticationProvider that does not perform any
* authentication at all. All attempts to access the Guacamole system are
@@ -125,7 +130,14 @@ public class NoAuthenticationProvider extends SimpleAuthenticationProvider {
* property.
*/
private File getConfigurationFile() throws GuacamoleException {
return environment.getRequiredProperty(NOAUTH_CONFIG);
// Get config file, defaulting to GUACAMOLE_HOME/noauth-config.xml
File configFile = environment.getProperty(NOAUTH_CONFIG);
if (configFile == null)
configFile = new File(environment.getGuacamoleHome(), DEFAULT_NOAUTH_CONFIG);
return configFile;
}
public synchronized void init() throws GuacamoleException {

View File

@@ -0,0 +1,12 @@
{
"guacamoleVersion" : "0.9.6",
"name" : "Disabled Authentication",
"namespace" : "guac-noauth",
"authProviders" : [
"net.sourceforge.guacamole.net.auth.noauth.NoAuthenticationProvider"
]
}

View File

@@ -95,28 +95,28 @@ public class LocalEnvironment implements Environment {
properties = new Properties();
try {
InputStream stream;
InputStream stream = null;
// If not a directory, load from classpath
if (!guacHome.isDirectory()) {
// Read from classpath
if (!guacHome.isDirectory())
stream = LocalEnvironment.class.getResourceAsStream("/guacamole.properties");
if (stream == null)
throw new GuacamoleServerException(
"guacamole.properties not loaded from " + guacHome
+ " (not a directory), and guacamole.properties could"
+ " not be found as a resource in the classpath.");
}
// Otherwise, try to load from file
else
stream = new FileInputStream(new File(guacHome, "guacamole.properties"));
else {
File propertiesFile = new File(guacHome, "guacamole.properties");
if (propertiesFile.exists())
stream = new FileInputStream(propertiesFile);
}
// Load properties, always close stream
try { properties.load(stream); }
finally { stream.close(); }
// Load properties from stream, if any, always closing stream when done
if (stream != null) {
try { properties.load(stream); }
finally { stream.close(); }
}
// Notify if we're proceeding without guacamole.properties
else
logger.info("No guacamole.properties file found within GUACAMOLE_HOME or the classpath. Using defaults.");
}
catch (IOException e) {

View File

@@ -45,6 +45,18 @@ import org.glyptodon.guacamole.protocol.GuacamoleConfiguration;
*/
public class SimpleConnection extends AbstractConnection {
/**
* The hostname to use when connecting to guacd if no hostname is provided
* within guacamole.properties.
*/
private static final String DEFAULT_GUACD_HOSTNAME = "localhost";
/**
* The port to use when connecting to guacd if no port is provided within
* guacamole.properties.
*/
private static final int DEFAULT_GUACD_PORT = 4822;
/**
* Backing configuration, containing all sensitive information.
*/
@@ -92,8 +104,8 @@ public class SimpleConnection extends AbstractConnection {
Environment env = new LocalEnvironment();
// Get guacd connection parameters
String hostname = env.getProperty(Environment.GUACD_HOSTNAME);
int port = env.getProperty(Environment.GUACD_PORT);
String hostname = env.getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME);
int port = env.getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT);
GuacamoleSocket socket;

View File

@@ -76,7 +76,7 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
private final Environment environment;
/**
* The filename of the XML file to read the user user_mapping from.
* The XML file to read the user mapping from.
*/
public static final FileGuacamoleProperty BASIC_USER_MAPPING = new FileGuacamoleProperty() {
@@ -85,6 +85,12 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
};
/**
* The default filename to use for the user mapping, if not defined within
* guacamole.properties.
*/
public static final String DEFAULT_USER_MAPPING = "user-mapping.xml";
/**
* Creates a new BasicFileAuthenticationProvider that authenticates users
* against simple, monolithic XML file.
@@ -110,9 +116,10 @@ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvide
*/
private UserMapping getUserMapping() throws GuacamoleException {
// Get user user_mapping file
File user_mapping_file =
environment.getRequiredProperty(BASIC_USER_MAPPING);
// Get user mapping file, defaulting to GUACAMOLE_HOME/user-mapping.xml
File user_mapping_file = environment.getProperty(BASIC_USER_MAPPING);
if (user_mapping_file == null)
user_mapping_file = new File(environment.getGuacamoleHome(), DEFAULT_USER_MAPPING);
// If user_mapping not yet read, or user_mapping has been modified, reread
if (user_mapping == null ||

View File

@@ -30,6 +30,7 @@ import javax.servlet.ServletContextEvent;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.environment.LocalEnvironment;
import org.glyptodon.guacamole.net.basic.extension.ExtensionModule;
import org.glyptodon.guacamole.net.basic.log.LogModule;
import org.glyptodon.guacamole.net.basic.rest.RESTAuthModule;
import org.glyptodon.guacamole.net.basic.rest.RESTServletModule;
@@ -83,7 +84,8 @@ public class BasicServletContextListener extends GuiceServletContextListener {
return Guice.createInjector(Stage.PRODUCTION,
new EnvironmentModule(environment),
new LogModule(environment),
new RESTAuthModule(environment, sessionMap),
new ExtensionModule(environment),
new RESTAuthModule(sessionMap),
new RESTServletModule(),
new TunnelModule()
);

View File

@@ -39,10 +39,13 @@ import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
/**
* A ClassLoader implementation which finds classes within a configurable
* directory. This directory is set within guacamole.properties.
* directory. This directory is set within guacamole.properties. This class
* is deprecated in favor of DirectoryClassLoader, which is automatically
* configured based on the presence/absence of GUACAMOLE_HOME/lib.
*
* @author Michael Jumper
*/
@Deprecated
public class GuacamoleClassLoader extends ClassLoader {
/**

View File

@@ -60,11 +60,6 @@ public class GuacamoleSession {
*/
private UserContext userContext;
/**
* Collection of all event listeners configured in guacamole.properties.
*/
private final Collection<Object> listeners = new ArrayList<Object>();
/**
* The current clipboard state.
*/
@@ -98,52 +93,9 @@ public class GuacamoleSession {
*/
public GuacamoleSession(Environment environment, Credentials credentials,
UserContext userContext) throws GuacamoleException {
this.lastAccessedTime = System.currentTimeMillis();
this.credentials = credentials;
this.userContext = userContext;
// Load listeners from guacamole.properties
try {
// Get all listener classes from properties
Collection<Class> listenerClasses =
environment.getProperty(BasicGuacamoleProperties.EVENT_LISTENERS);
// Add an instance of each class to the list
if (listenerClasses != null) {
for (Class<?> listenerClass : listenerClasses) {
// Instantiate listener
Object listener = listenerClass.getConstructor().newInstance();
// Add listener to collection of listeners
listeners.add(listener);
}
}
}
catch (InstantiationException e) {
throw new GuacamoleException("Listener class is abstract.", e);
}
catch (IllegalAccessException e) {
throw new GuacamoleException("No access to listener constructor.", e);
}
catch (IllegalArgumentException e) {
// This should not happen, given there ARE no arguments
throw new GuacamoleException("Illegal arguments to listener constructor.", e);
}
catch (InvocationTargetException e) {
throw new GuacamoleException("Error while instantiating listener.", e);
}
catch (NoSuchMethodException e) {
throw new GuacamoleException("Listener has no default constructor.", e);
}
catch (SecurityException e) {
throw new GuacamoleException("Security restrictions prevent instantiation of listener.", e);
}
}
/**
@@ -198,19 +150,6 @@ public class GuacamoleSession {
return clipboardState;
}
/**
* Returns a collection which iterates over instances of all listeners
* defined in guacamole.properties. For each listener defined in
* guacamole.properties, a new instance is created and stored in this
* collection.
*
* @return A collection which iterates over instances of all listeners
* defined in guacamole.properties.
*/
public Collection<Object> getListeners() {
return Collections.unmodifiableCollection(listeners);
}
/**
* Returns whether this session has any associated active tunnels.
*

View File

@@ -24,7 +24,6 @@ package org.glyptodon.guacamole.net.basic;
import com.google.inject.servlet.ServletModule;
import java.lang.reflect.InvocationTargetException;
import org.glyptodon.guacamole.GuacamoleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,7 +54,7 @@ public class TunnelModule extends ServletModule {
try {
// Attempt to find WebSocket module
Class<?> module = (Class<?>) GuacamoleClassLoader.getInstance().findClass(classname);
Class<?> module = Class.forName(classname);
// Create loader
TunnelLoader loader = (TunnelLoader) module.getConstructor().newInstance();
@@ -85,12 +84,6 @@ public class TunnelModule extends ServletModule {
logger.debug("Error instantiating WebSocket module.", e);
}
// Log all GuacamoleExceptions
catch (GuacamoleException e) {
logger.error("Unable to load/detect WebSocket support: {}", e.getMessage());
logger.debug("Error loading/detecting WebSocket support.", e);
}
// Load attempt failed
return false;

View File

@@ -38,10 +38,6 @@ import org.glyptodon.guacamole.net.auth.ConnectionGroup;
import org.glyptodon.guacamole.net.auth.Directory;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.glyptodon.guacamole.net.event.TunnelCloseEvent;
import org.glyptodon.guacamole.net.event.TunnelConnectEvent;
import org.glyptodon.guacamole.net.event.listener.TunnelCloseListener;
import org.glyptodon.guacamole.net.event.listener.TunnelConnectListener;
import org.glyptodon.guacamole.protocol.GuacamoleClientInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -76,84 +72,6 @@ public class TunnelRequestService {
@Inject
private AuthenticationService authenticationService;
/**
* Notifies all listeners in the given session that a tunnel has been
* connected.
*
* @param session The session associated with the listeners to be notified.
* @param tunnel The tunnel being connected.
* @return true if all listeners are allowing the tunnel to connect,
* or if there are no listeners, and false if any listener is
* canceling the connection. Note that once one listener cancels,
* no other listeners will run.
* @throws GuacamoleException If any listener throws an error while being
* notified. Note that if any listener throws an
* error, the connect is canceled, and no other
* listeners will run.
*/
private boolean notifyConnect(GuacamoleSession session, GuacamoleTunnel tunnel)
throws GuacamoleException {
// Build event for auth success
TunnelConnectEvent event = new TunnelConnectEvent(
session.getUserContext(),
session.getCredentials(),
tunnel);
// Notify all listeners
for (Object listener : session.getListeners()) {
if (listener instanceof TunnelConnectListener) {
// Cancel immediately if hook returns false
if (!((TunnelConnectListener) listener).tunnelConnected(event))
return false;
}
}
return true;
}
/**
* Notifies all listeners in the given session that a tunnel has been
* closed.
*
* @param session The session associated with the listeners to be notified.
* @param tunnel The tunnel being closed.
* @return true if all listeners are allowing the tunnel to close,
* or if there are no listeners, and false if any listener is
* canceling the close. Note that once one listener cancels,
* no other listeners will run.
* @throws GuacamoleException If any listener throws an error while being
* notified. Note that if any listener throws an
* error, the close is canceled, and no other
* listeners will run.
*/
private boolean notifyClose(GuacamoleSession session, GuacamoleTunnel tunnel)
throws GuacamoleException {
// Build event for auth success
TunnelCloseEvent event = new TunnelCloseEvent(
session.getUserContext(),
session.getCredentials(),
tunnel);
// Notify all listeners
for (Object listener : session.getListeners()) {
if (listener instanceof TunnelCloseListener) {
// Cancel immediately if hook returns false
if (!((TunnelCloseListener) listener).tunnelClosed(event))
return false;
}
}
return true;
}
/**
* Reads and returns the client information provided within the given
* request.
@@ -335,10 +253,6 @@ public class TunnelRequestService {
@Override
public void close() throws GuacamoleException {
// Signal listeners
if (!notifyClose(session, this))
throw new GuacamoleException("Tunnel close canceled by listener.");
session.removeTunnel(getUUID().toString());
// Close if no exception due to listener
@@ -348,12 +262,6 @@ public class TunnelRequestService {
};
// Notify listeners about connection
if (!notifyConnect(session, monitoredTunnel)) {
logger.info("Successful connection canceled by hook.");
return null;
}
// Associate tunnel with session
session.addTunnel(monitoredTunnel);
return monitoredTunnel;

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.extension;
import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import org.glyptodon.guacamole.GuacamoleException;
/**
* A ClassLoader implementation which finds classes within .jar files within a
* given directory.
*
* @author Michael Jumper
*/
public class DirectoryClassLoader extends URLClassLoader {
/**
* Returns an instance of DirectoryClassLoader configured to load .jar
* files from the given directory. Calling this function multiple times
* will not affect previously-returned instances of DirectoryClassLoader.
*
* @param dir
* The directory from which .jar files should be read.
*
* @return
* A DirectoryClassLoader instance which loads classes from the .jar
* files in the given directory.
*
* @throws GuacamoleException
* If the given file is not a directory, or the contents of the given
* directory cannot be read.
*/
public static DirectoryClassLoader getInstance(final File dir)
throws GuacamoleException {
try {
// Attempt to create singleton classloader which loads classes from
// all .jar's in the lib directory defined in guacamole.properties
return AccessController.doPrivileged(new PrivilegedExceptionAction<DirectoryClassLoader>() {
@Override
public DirectoryClassLoader run() throws GuacamoleException {
return new DirectoryClassLoader(dir);
}
});
}
catch (PrivilegedActionException e) {
throw (GuacamoleException) e.getException();
}
}
/**
* Returns all .jar files within the given directory as an array of URLs.
*
* @param dir
* The directory to retrieve all .jar files from.
*
* @return
* An array of the URLs of all .jar files within the given directory.
*
* @throws GuacamoleException
* If the given file is not a directory, or the contents of the given
* directory cannot be read.
*/
private static URL[] getJarURLs(File dir) throws GuacamoleException {
// Validate directory is indeed a directory
if (!dir.isDirectory())
throw new GuacamoleException(dir + " is not a directory.");
// Get list of URLs for all .jar's in the lib directory
Collection<URL> jarURLs = new ArrayList<URL>();
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
// If it ends with .jar, accept the file
return name.endsWith(".jar");
}
});
// Verify directory was successfully read
if (files == null)
throw new GuacamoleException("Unable to read contents of directory " + dir);
// Add the URL for each .jar to the jar URL list
for (File file : files) {
try {
jarURLs.add(file.toURI().toURL());
}
catch (MalformedURLException e) {
throw new GuacamoleException(e);
}
}
// Set delegate classloader to new URLClassLoader which loads from the .jars found above.
URL[] urls = new URL[jarURLs.size()];
return jarURLs.toArray(urls);
}
/**
* Creates a new DirectoryClassLoader configured to load .jar files from
* the given directory.
*
* @param dir
* The directory from which .jar files should be read.
*
* @throws GuacamoleException
* If the given file is not a directory, or the contents of the given
* directory cannot be read.
*/
private DirectoryClassLoader(File dir) throws GuacamoleException {
super(getJarURLs(dir), DirectoryClassLoader.class.getClassLoader());
}
}

View File

@@ -0,0 +1,361 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.extension;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.ObjectMapper;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.basic.resource.ClassPathResource;
import org.glyptodon.guacamole.net.basic.resource.Resource;
/**
* A Guacamole extension, which may provide custom authentication, static
* files, theming/branding, etc.
*
* @author Michael Jumper
*/
public class Extension {
/**
* The Jackson parser for parsing the language JSON files.
*/
private static final ObjectMapper mapper = new ObjectMapper();
/**
* The name of the manifest file that describes the contents of a
* Guacamole extension.
*/
private static final String MANIFEST_NAME = "guac-manifest.json";
/**
* The parsed manifest file of this extension, describing the location of
* resources within the extension.
*/
private final ExtensionManifest manifest;
/**
* The classloader to use when reading resources from this extension,
* including classes and static files.
*/
private final ClassLoader classLoader;
/**
* The collection of all JavaScript resources defined within the extension.
*/
private final Collection<Resource> javaScriptResources;
/**
* The collection of all CSS resources defined within the extension.
*/
private final Collection<Resource> cssResources;
/**
* The collection of all AuthenticationProvider classes defined within the
* extension.
*/
private final Collection<Class<AuthenticationProvider>> authenticationProviderClasses;
/**
* Returns a new collection of resources corresponding to the collection of
* paths provided. Each resource will be associated with the given
* mimetype.
*
* @param mimetype
* The mimetype to associate with each resource.
*
* @param paths
* The paths corresponding to the resources desired.
*
* @return
* A new, unmodifiable collection of resources corresponding to the
* collection of paths provided.
*/
private Collection<Resource> getClassPathResources(String mimetype, Collection<String> paths) {
// If no paths are provided, just return an empty list
if (paths == null)
return Collections.<Resource>emptyList();
// Add classpath resource for each path provided
Collection<Resource> resources = new ArrayList<Resource>(paths.size());
for (String path : paths)
resources.add(new ClassPathResource(classLoader, mimetype, path));
// Callers should not rely on modifying the result
return Collections.unmodifiableCollection(resources);
}
/**
* Retrieve the AuthenticationProvider subclass having the given name. If
* the class having the given name does not exist or isn't actually a
* subclass of AuthenticationProvider, an exception will be thrown.
*
* @param name
* The name of the AuthenticationProvider class to retrieve.
*
* @return
* The subclass of AuthenticationProvider having the given name.
*
* @throws GuacamoleException
* If no such class exists, or if the class with the given name is not
* a subclass of AuthenticationProvider.
*/
@SuppressWarnings("unchecked") // We check this ourselves with isAssignableFrom()
private Class<AuthenticationProvider> getAuthenticationProviderClass(String name)
throws GuacamoleException {
try {
// Get authentication provider class
Class<?> authenticationProviderClass = classLoader.loadClass(name);
// Verify the located class is actually a subclass of AuthenticationProvider
if (!AuthenticationProvider.class.isAssignableFrom(authenticationProviderClass))
throw new GuacamoleServerException("Authentication providers MUST extend the AuthenticationProvider class.");
// Return located class
return (Class<AuthenticationProvider>) authenticationProviderClass;
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Authentication provider class not found.", e);
}
}
/**
* Returns a new collection of all AuthenticationProvider subclasses having
* the given names. If any class does not exist or isn't actually a
* subclass of AuthenticationProvider, an exception will be thrown, and
* no further AuthenticationProvider 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<AuthenticationProvider>> getAuthenticationProviderClasses(Collection<String> names)
throws GuacamoleException {
// If no classnames are provided, just return an empty list
if (names == null)
return Collections.<Class<AuthenticationProvider>>emptyList();
// Define all auth provider classes
Collection<Class<AuthenticationProvider>> classes = new ArrayList<Class<AuthenticationProvider>>(names.size());
for (String name : names)
classes.add(getAuthenticationProviderClass(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.
*
* @param parent
* The classloader to use as the parent for the isolated classloader of
* this extension.
*
* @param file
* The file to load as an extension.
*
* @throws GuacamoleException
* If the provided file is not a .jar file, does not contain the
* guac-manifest.json, or if guac-manifest.json is invalid and cannot
* be parsed.
*/
public Extension(final ClassLoader parent, final File file) throws GuacamoleException {
try {
// Open extension
ZipFile extension = new ZipFile(file);
try {
// Retrieve extension manifest
ZipEntry manifestEntry = extension.getEntry(MANIFEST_NAME);
if (manifestEntry == null)
throw new GuacamoleServerException("Extension " + file.getName() + " is missing " + MANIFEST_NAME);
// Parse manifest
manifest = mapper.readValue(extension.getInputStream(manifestEntry), ExtensionManifest.class);
}
// Always close zip file, if possible
finally {
extension.close();
}
try {
// Create isolated classloader for this extension
classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction<ClassLoader>() {
@Override
public ClassLoader run() throws GuacamoleException {
try {
// Classloader must contain only the extension itself
return new URLClassLoader(new URL[]{file.toURI().toURL()}, parent);
}
catch (MalformedURLException e) {
throw new GuacamoleException(e);
}
}
});
}
// Rethrow any GuacamoleException
catch (PrivilegedActionException e) {
throw (GuacamoleException) e.getException();
}
}
// Abort load if not a valid zip file
catch (ZipException e) {
throw new GuacamoleServerException("Extension is not a valid zip file: " + file.getName(), e);
}
// Abort if manifest cannot be parsed (invalid JSON)
catch (JsonParseException e) {
throw new GuacamoleServerException(MANIFEST_NAME + " is not valid JSON: " + file.getName(), e);
}
// Abort if zip file cannot be read at all due to I/O errors
catch (IOException e) {
throw new GuacamoleServerException("Unable to read extension: " + file.getName(), e);
}
// Define static resources
cssResources = getClassPathResources("text/css", manifest.getCSSPaths());
javaScriptResources = getClassPathResources("text/javascript", manifest.getJavaScriptPaths());
// Define authentication providers
authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders());
}
/**
* Returns the version of the Guacamole web application for which this
* extension was built.
*
* @return
* The version of the Guacamole web application for which this
* extension was built.
*/
public String getGuacamoleVersion() {
return manifest.getGuacamoleVersion();
}
/**
* Returns the name of this extension, as declared in the extension's
* manifest.
*
* @return
* The name of this extension.
*/
public String getName() {
return manifest.getName();
}
/**
* Returns the namespace of this extension, as declared in the extension's
* manifest.
*
* @return
* The namespace of this extension.
*/
public String getNamespace() {
return manifest.getNamespace();
}
/**
* Returns all declared JavaScript resources associated with this
* extension. JavaScript resources are declared within the extension
* manifest.
*
* @return
* All declared JavaScript resources associated with this extension.
*/
public Collection<Resource> getJavaScriptResources() {
return javaScriptResources;
}
/**
* Returns all declared CSS resources associated with this extension. CSS
* resources are declared within the extension manifest.
*
* @return
* All declared CSS resources associated with this extension.
*/
public Collection<Resource> getCSSResources() {
return cssResources;
}
/**
* Returns all declared authentication providers classes associated with
* this extension. Authentication providers are declared within the
* extension manifest.
*
* @return
* All declared authentication provider classes with this extension.
*/
public Collection<Class<AuthenticationProvider>> getAuthenticationProviderClasses() {
return authenticationProviderClasses;
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.extension;
import java.util.Collection;
import org.codehaus.jackson.annotate.JsonProperty;
/**
* Java representation of the JSON manifest contained within every Guacamole
* extension, identifying an extension and describing its contents.
*
* @author Michael Jumper
*/
public class ExtensionManifest {
/**
* The version of Guacamole for which this extension was built.
* Compatibility rules built into the web application will guard against
* incompatible extensions being loaded.
*/
private String guacamoleVersion;
/**
* The name of the extension associated with this manifest. The extension
* name is human-readable, and used for display purposes only.
*/
private String name;
/**
* The namespace of the extension associated with this manifest. The
* extension namespace is required for internal use, and is used wherever
* extension-specific files or resources need to be isolated from those of
* other extensions.
*/
private String namespace;
/**
* The paths of all JavaScript resources within the .jar of the extension
* associated with this manifest.
*/
private Collection<String> javaScriptPaths;
/**
* The paths of all CSS resources within the .jar of the extension
* associated with this manifest.
*/
private Collection<String> cssPaths;
/**
* The names of all authentication provider classes within this extension,
* if any.
*/
private Collection<String> authProviders;
/**
* Returns the version of the Guacamole web application for which the
* extension was built, such as "0.9.6".
*
* @return
* The version of the Guacamole web application for which the extension
* was built.
*/
public String getGuacamoleVersion() {
return guacamoleVersion;
}
/**
* Sets the version of the Guacamole web application for which the
* extension was built, such as "0.9.6".
*
* @param guacamoleVersion
* The version of the Guacamole web application for which the extension
* was built.
*/
public void setGuacamoleVersion(String guacamoleVersion) {
this.guacamoleVersion = guacamoleVersion;
}
/**
* Returns the name of the extension associated with this manifest. The
* name is human-readable, for display purposes only, and is defined within
* the manifest by the "name" property.
*
* @return
* The name of the extension associated with this manifest.
*/
public String getName() {
return name;
}
/**
* Sets the name of the extension associated with this manifest. The name
* is human-readable, for display purposes only, and is defined within the
* manifest by the "name" property.
*
* @param name
* The name of the extension associated with this manifest.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the namespace of the extension associated with this manifest.
* The namespace is required for internal use, and is used wherever
* extension-specific files or resources need to be isolated from those of
* other extensions. It is defined within the manifest by the "namespace"
* property.
*
* @return
* The namespace of the extension associated with this manifest.
*/
public String getNamespace() {
return namespace;
}
/**
* Sets the namespace of the extension associated with this manifest. The
* namespace is required for internal use, and is used wherever extension-
* specific files or resources need to be isolated from those of other
* extensions. It is defined within the manifest by the "namespace"
* property.
*
* @param namespace
* The namespace of the extension associated with this manifest.
*/
public void setNamespace(String namespace) {
this.namespace = namespace;
}
/**
* Returns the paths to all JavaScript resources within the extension.
* These paths are defined within the manifest by the "js" property as an
* array of strings, where each string is a path relative to the root of
* the extension .jar.
*
* @return
* A collection of paths to all JavaScript resources within the
* extension.
*/
@JsonProperty("js")
public Collection<String> getJavaScriptPaths() {
return javaScriptPaths;
}
/**
* Sets the paths to all JavaScript resources within the extension. These
* paths are defined within the manifest by the "js" property as an array
* of strings, where each string is a path relative to the root of the
* extension .jar.
*
* @param javaScriptPaths
* A collection of paths to all JavaScript resources within the
* extension.
*/
@JsonProperty("js")
public void setJavaScriptPaths(Collection<String> javaScriptPaths) {
this.javaScriptPaths = javaScriptPaths;
}
/**
* Returns the paths to all CSS resources within the extension. These paths
* are defined within the manifest by the "js" property as an array of
* strings, where each string is a path relative to the root of the
* extension .jar.
*
* @return
* A collection of paths to all CSS resources within the extension.
*/
@JsonProperty("css")
public Collection<String> getCSSPaths() {
return cssPaths;
}
/**
* Sets the paths to all CSS resources within the extension. These paths
* are defined within the manifest by the "js" property as an array of
* strings, where each string is a path relative to the root of the
* extension .jar.
*
* @param cssPaths
* A collection of paths to all CSS resources within the extension.
*/
@JsonProperty("css")
public void setCSSPaths(Collection<String> cssPaths) {
this.cssPaths = cssPaths;
}
/**
* Returns the classnames of all authentication provider classes within the
* extension. These classnames are defined within the manifest by the
* "authProviders" property as an array of strings, where each string is an
* authentication provider classname.
*
* @return
* A collection of classnames of all authentication providers within
* the extension.
*/
public Collection<String> getAuthProviders() {
return authProviders;
}
/**
* Sets the classnames of all authentication provider classes within the
* extension. These classnames are defined within the manifest by the
* "authProviders" property as an array of strings, where each string is an
* authentication provider classname.
*
* @param authProviders
* A collection of classnames of all authentication providers within
* the extension.
*/
public void setAuthProviders(Collection<String> authProviders) {
this.authProviders = authProviders;
}
}

View File

@@ -0,0 +1,300 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.extension;
import com.google.inject.Singleton;
import com.google.inject.servlet.ServletModule;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import net.sourceforge.guacamole.net.basic.BasicFileAuthenticationProvider;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.glyptodon.guacamole.net.basic.resource.Resource;
import org.glyptodon.guacamole.net.basic.resource.ResourceServlet;
import org.glyptodon.guacamole.net.basic.resource.SequenceResource;
import org.glyptodon.guacamole.net.basic.resource.WebApplicationResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Guice Module which loads all extensions within the
* GUACAMOLE_HOME/extensions directory, if any.
*
* @author Michael Jumper
*/
public class ExtensionModule extends ServletModule {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ExtensionModule.class);
/**
* The version strings of all Guacamole versions whose extensions are
* compatible with this release.
*/
private static final List<String> ALLOWED_GUACAMOLE_VERSIONS =
Collections.unmodifiableList(Arrays.asList(
"0.9.6"
));
/**
* The name of the directory within GUACAMOLE_HOME containing any .jars
* which should be included in the classpath of all extensions.
*/
private static final String LIB_DIRECTORY = "lib";
/**
* The name of the directory within GUACAMOLE_HOME containing all
* extensions.
*/
private static final String EXTENSIONS_DIRECTORY = "extensions";
/**
* The string that the filenames of all extensions must end with to be
* recognized as extensions.
*/
private static final String EXTENSION_SUFFIX = ".jar";
/**
* The Guacamole server environment.
*/
private final Environment environment;
/**
* The currently-bound authentication provider, if any. At the moment, we
* only support one authentication provider loaded at any one time.
*/
private Class<? extends AuthenticationProvider> boundAuthenticationProvider = null;
/**
* Returns the classloader that should be used as the parent classloader
* for all extensions. If the GUACAMOLE_HOME/lib directory exists, this
* will be a classloader that loads classes from within the .jar files in
* that directory. Lacking the GUACAMOLE_HOME/lib directory, this will
* simply be the classloader associated with the ExtensionModule class.
*
* @return
* The classloader that should be used as the parent classloader for
* all extensions.
*
* @throws GuacamoleException
* If an error occurs while retrieving the classloader.
*/
private ClassLoader getParentClassLoader() throws GuacamoleException {
// Retrieve lib directory
File libDir = new File(environment.getGuacamoleHome(), LIB_DIRECTORY);
// If lib directory does not exist, use default class loader
if (!libDir.isDirectory())
return ExtensionModule.class.getClassLoader();
// Return classloader which loads classes from all .jars within the lib directory
return DirectoryClassLoader.getInstance(libDir);
}
/**
* Creates a module which loads all extensions within the
* GUACAMOLE_HOME/extensions directory.
*
* @param environment
* The environment to use when configuring authentication.
*/
public ExtensionModule(Environment environment) {
this.environment = environment;
}
/**
* Reads the value of the now-deprecated "auth-provider" property from
* guacamole.properties, returning the corresponding AuthenticationProvider
* class. If no authentication provider could be read, or the property is
* not present, null is returned.
*
* As this property is deprecated, this function will also log warning
* messages if the property is actually specified.
*
* @return
* The value of the deprecated "auth-provider" property, or null if the
* property is not present.
*/
@SuppressWarnings("deprecation") // We must continue to use this property until it is truly no longer supported
private Class<AuthenticationProvider> getAuthProviderProperty() {
// Get and bind auth provider instance, if defined via property
try {
// Use "auth-provider" property if present, but warn about deprecation
Class<AuthenticationProvider> authenticationProvider = environment.getProperty(BasicGuacamoleProperties.AUTH_PROVIDER);
if (authenticationProvider != null)
logger.warn("The \"auth-provider\" and \"lib-directory\" properties are now deprecated. Please use the \"extensions\" and \"lib\" directories within GUACAMOLE_HOME instead.");
return authenticationProvider;
}
catch (GuacamoleException e) {
logger.warn("Value of deprecated \"auth-provider\" property within guacamole.properties is not valid: {}", e.getMessage());
logger.debug("Error reading authentication provider from guacamole.properties.", e);
}
return null;
}
/**
* Binds the given AuthenticationProvider class such that any service
* requiring access to the AuthenticationProvider can obtain it via
* injection.
*
* @param authenticationProvider
* The AuthenticationProvider class to bind.
*/
private void bindAuthenticationProvider(Class<? extends AuthenticationProvider> authenticationProvider) {
// Choose auth provider for binding if not already chosen
if (boundAuthenticationProvider == null)
boundAuthenticationProvider = authenticationProvider;
// If an auth provider is already chosen, skip and warn
else {
logger.debug("Ignoring AuthenticationProvider \"{}\".", authenticationProvider);
logger.warn("Only one authentication extension may be used at a time. Please "
+ "make sure that only one authentication extension is present "
+ "within the GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " "
+ "directory, and that you are not also specifying the deprecated "
+ "\"auth-provider\" property within guacamole.properties.");
return;
}
// Bind authentication provider
logger.debug("Binding AuthenticationProvider \"{}\".", authenticationProvider);
bind(AuthenticationProvider.class).to(authenticationProvider).in(Singleton.class);
}
/**
* Returns whether the given version of Guacamole is compatible with this
* version of Guacamole as far as extensions are concerned.
*
* @param guacamoleVersion
* The version of Guacamole the extension was built for.
*
* @return
* true if the given version of Guacamole is compatible with this
* version of Guacamole, false otherwise.
*/
private boolean isCompatible(String guacamoleVersion) {
return ALLOWED_GUACAMOLE_VERSIONS.contains(guacamoleVersion);
}
@Override
protected void configureServlets() {
// Load authentication provider from guacamole.properties for sake of backwards compatibility
Class<AuthenticationProvider> authProviderProperty = getAuthProviderProperty();
if (authProviderProperty != null)
bindAuthenticationProvider(authProviderProperty);
// Retrieve and validate extensions directory
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
if (!extensionsDir.isDirectory())
return;
// Retrieve list of all extension files within extensions directory
File[] extensionFiles = extensionsDir.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(EXTENSION_SUFFIX);
}
});
// Init JavaScript resources with base guacamole.min.js
Collection<Resource> javaScriptResources = new ArrayList<Resource>();
javaScriptResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.js"));
// Init CSS resources with base guacamole.min.css
Collection<Resource> cssResources = new ArrayList<Resource>();
cssResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.css"));
// Load each extension within the extension directory
for (File extensionFile : extensionFiles) {
logger.debug("Loading extension: \"{}\"", extensionFile.getName());
try {
// Load extension from file
Extension extension = new Extension(getParentClassLoader(), extensionFile);
// Validate Guacamole version of extension
if (!isCompatible(extension.getGuacamoleVersion())) {
logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.",
extension.getGuacamoleVersion(), extensionFile.getName());
throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not "
+ "compatible with this version of Guacamole.");
}
// Add any JavaScript / CSS resources
javaScriptResources.addAll(extension.getJavaScriptResources());
cssResources.addAll(extension.getCSSResources());
// Attempt to load all authentication providers
Collection<Class<AuthenticationProvider>> authenticationProviders = extension.getAuthenticationProviderClasses();
for (Class<AuthenticationProvider> authenticationProvider : authenticationProviders)
bindAuthenticationProvider(authenticationProvider);
// Log successful loading of extension by name
logger.info("Extension \"{}\" loaded.", extension.getName());
}
catch (GuacamoleException e) {
logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage());
logger.debug("Unable to load extension.", e);
}
}
// Default to basic auth if nothing else chosen/provided
if (boundAuthenticationProvider == null) {
logger.info("Using default, \"basic\", XML-driven authentication.");
bindAuthenticationProvider(BasicFileAuthenticationProvider.class);
}
// Dynamically generate app.js and app.css from extensions
serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));
serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources)));
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes which represent and facilitate the loading of extensions to the
* Guacamole web application.
*/
package org.glyptodon.guacamole.net.basic.extension;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Glyptodon LLC
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,22 +22,24 @@
package org.glyptodon.guacamole.net.basic.properties;
import java.lang.reflect.InvocationTargetException;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
import org.glyptodon.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is the name of a class to use to
* authenticate users. This class must implement AuthenticationProvider.
* authenticate users. This class must implement AuthenticationProvider. Use
* of this property type is deprecated in favor of the
* GUACAMOLE_HOME/extensions directory.
*
* @author Michael Jumper
*/
public abstract class AuthenticationProviderProperty implements GuacamoleProperty<AuthenticationProvider> {
@Deprecated
public abstract class AuthenticationProviderProperty implements GuacamoleProperty<Class<AuthenticationProvider>> {
@Override
public AuthenticationProvider parseValue(String authProviderClassName) throws GuacamoleException {
@SuppressWarnings("unchecked") // Explicitly checked within by isAssignableFrom()
public Class<AuthenticationProvider> parseValue(String authProviderClassName) throws GuacamoleException {
// If no property provided, return null.
if (authProviderClassName == null)
@@ -46,35 +48,21 @@ public abstract class AuthenticationProviderProperty implements GuacamolePropert
// Get auth provider instance
try {
Object obj = GuacamoleClassLoader.getInstance().loadClass(authProviderClassName)
.getConstructor().newInstance();
// Get authentication provider class
Class<?> authProviderClass = org.glyptodon.guacamole.net.basic.GuacamoleClassLoader.getInstance().loadClass(authProviderClassName);
if (!(obj instanceof AuthenticationProvider))
// Verify the located class is actually a subclass of AuthenticationProvider
if (!AuthenticationProvider.class.isAssignableFrom(authProviderClass))
throw new GuacamoleException("Specified authentication provider class is not a AuthenticationProvider.");
return (AuthenticationProvider) obj;
// Return located class
return (Class<AuthenticationProvider>) authProviderClass;
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Authentication provider class not found", e);
}
catch (NoSuchMethodException e) {
throw new GuacamoleException("Default constructor for authentication provider not present", e);
}
catch (SecurityException e) {
throw new GuacamoleException("Creation of authentication provider disallowed; check your security settings", e);
}
catch (InstantiationException e) {
throw new GuacamoleException("Unable to instantiate authentication provider", e);
}
catch (IllegalAccessException e) {
throw new GuacamoleException("Unable to access default constructor of authentication provider", e);
}
catch (InvocationTargetException e) {
throw new GuacamoleException("Internal error in constructor of authentication provider", e.getTargetException());
}
}
}

View File

@@ -22,7 +22,6 @@
package org.glyptodon.guacamole.net.basic.properties;
import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty;
import org.glyptodon.guacamole.properties.FileGuacamoleProperty;
import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty;
@@ -40,8 +39,10 @@ public class BasicGuacamoleProperties {
/**
* The authentication provider to user when retrieving the authorized
* configurations of a user.
* configurations of a user. This property is currently supported, but
* deprecated in favor of the GUACAMOLE_HOME/extensions directory.
*/
@Deprecated
public static final AuthenticationProviderProperty AUTH_PROVIDER = new AuthenticationProviderProperty() {
@Override
@@ -50,8 +51,11 @@ public class BasicGuacamoleProperties {
};
/**
* The directory to search for authentication provider classes.
* The directory to search for authentication provider classes. This
* property is currently supported, but deprecated in favor of the
* GUACAMOLE_HOME/lib directory.
*/
@Deprecated
public static final FileGuacamoleProperty LIB_DIRECTORY = new FileGuacamoleProperty() {
@Override
@@ -59,16 +63,6 @@ public class BasicGuacamoleProperties {
};
/**
* The comma-separated list of all classes to use as event listeners.
*/
public static final EventListenersProperty EVENT_LISTENERS = new EventListenersProperty() {
@Override
public String getName() { return "event-listeners"; }
};
/**
* The session timeout for the API, in minutes.
*/

View File

@@ -1,72 +0,0 @@
/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.properties;
import java.util.ArrayList;
import java.util.Collection;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
import org.glyptodon.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is a comma-separated list of class names,
* where each class will be used as a listener for events.
*
* @author Michael Jumper
*/
public abstract class EventListenersProperty implements GuacamoleProperty<Collection<Class>> {
@Override
public Collection<Class> parseValue(String classNameList) throws GuacamoleException {
// If no property provided, return null.
if (classNameList == null)
return null;
// Parse list
String[] classNames = classNameList.split(",[\\s]*");
// Fill list of classes
Collection<Class> listeners = new ArrayList<Class>();
try {
// Load all classes in list
for (String className : classNames) {
Class clazz = GuacamoleClassLoader.getInstance().loadClass(className);
listeners.add(clazz);
}
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Listener class not found.", e);
}
catch (SecurityException e) {
throw new GuacamoleException("Security settings prevent loading of listener class.", e);
}
return listeners;
}
}

View File

@@ -23,10 +23,6 @@
package org.glyptodon.guacamole.net.basic.rest;
import com.google.inject.AbstractModule;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.environment.Environment;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthTokenGenerator;
import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService;
import org.glyptodon.guacamole.net.basic.rest.auth.SecureRandomAuthTokenGenerator;
@@ -47,11 +43,6 @@ public class RESTAuthModule extends AbstractModule {
*/
private final Logger logger = LoggerFactory.getLogger(RESTAuthModule.class);
/**
* The Guacamole server environment.
*/
private final Environment environment;
/**
* Singleton instance of TokenSessionMap.
*/
@@ -61,16 +52,11 @@ public class RESTAuthModule extends AbstractModule {
* Creates a module which handles binding of authentication-related
* objects, including the singleton TokenSessionMap.
*
* @param environment
* The environment to use when configuring authentication.
*
* @param tokenSessionMap
* An instance of TokenSessionMap to inject as a singleton wherever
* needed.
*/
public RESTAuthModule(Environment environment,
TokenSessionMap tokenSessionMap) {
this.environment = environment;
public RESTAuthModule(TokenSessionMap tokenSessionMap) {
this.tokenSessionMap = tokenSessionMap;
}
@@ -84,17 +70,6 @@ public class RESTAuthModule extends AbstractModule {
bind(AuthenticationService.class);
bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class);
// Get and bind auth provider instance
try {
AuthenticationProvider authProvider = environment.getRequiredProperty(BasicGuacamoleProperties.AUTH_PROVIDER);
bind(AuthenticationProvider.class).toInstance(authProvider);
}
catch (GuacamoleException e) {
logger.error("Unable to read authentication provider from guacamole.properties: {}", e.getMessage());
logger.debug("Error reading authentication provider from guacamole.properties.", e);
throw new RuntimeException(e);
}
}
}

View File

@@ -28,8 +28,6 @@ import java.util.Arrays;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
import org.glyptodon.guacamole.net.basic.TunnelLoader;
import org.glyptodon.guacamole.net.basic.TunnelRequestService;
import org.slf4j.Logger;
@@ -53,7 +51,7 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
try {
// Attempt to find WebSocket servlet
GuacamoleClassLoader.getInstance().findClass("javax.websocket.Endpoint");
Class.forName("javax.websocket.Endpoint");
// Support found
return true;
@@ -65,12 +63,6 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Log all GuacamoleExceptions
catch (GuacamoleException e) {
logger.error("Unable to load/detect WebSocket support: {}", e.getMessage());
logger.debug("Error loading/detecting WebSocket support.", e);
}
// Support not found
return false;

View File

@@ -23,8 +23,6 @@
package org.glyptodon.guacamole.net.basic.websocket.jetty8;
import com.google.inject.servlet.ServletModule;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
import org.glyptodon.guacamole.net.basic.TunnelLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,7 +45,7 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
try {
// Attempt to find WebSocket servlet
GuacamoleClassLoader.getInstance().findClass("org.glyptodon.guacamole.net.basic.websocket.jetty8.BasicGuacamoleWebSocketTunnelServlet");
Class.forName("org.glyptodon.guacamole.net.basic.websocket.jetty8.BasicGuacamoleWebSocketTunnelServlet");
// Support found
return true;
@@ -59,12 +57,6 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Log all GuacamoleExceptions
catch (GuacamoleException e) {
logger.error("Unable to load/detect WebSocket support: {}", e.getMessage());
logger.debug("Error loading/detecting WebSocket support.", e);
}
// Support not found
return false;

View File

@@ -23,8 +23,6 @@
package org.glyptodon.guacamole.net.basic.websocket.jetty9;
import com.google.inject.servlet.ServletModule;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
import org.glyptodon.guacamole.net.basic.TunnelLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,7 +45,7 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
try {
// Attempt to find WebSocket servlet
GuacamoleClassLoader.getInstance().findClass("org.glyptodon.guacamole.net.basic.websocket.jetty9.BasicGuacamoleWebSocketTunnelServlet");
Class.forName("org.glyptodon.guacamole.net.basic.websocket.jetty9.BasicGuacamoleWebSocketTunnelServlet");
// Support found
return true;
@@ -59,12 +57,6 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Log all GuacamoleExceptions
catch (GuacamoleException e) {
logger.error("Unable to load/detect WebSocket support: {}", e.getMessage());
logger.debug("Error loading/detecting WebSocket support.", e);
}
// Support not found
return false;

View File

@@ -23,8 +23,6 @@
package org.glyptodon.guacamole.net.basic.websocket.tomcat;
import com.google.inject.servlet.ServletModule;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.basic.GuacamoleClassLoader;
import org.glyptodon.guacamole.net.basic.TunnelLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,7 +45,7 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
try {
// Attempt to find WebSocket servlet
GuacamoleClassLoader.getInstance().findClass("org.glyptodon.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet");
Class.forName("org.glyptodon.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet");
// Support found
return true;
@@ -59,12 +57,6 @@ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader
catch (ClassNotFoundException e) {}
catch (NoClassDefFoundError e) {}
// Log all GuacamoleExceptions
catch (GuacamoleException e) {
logger.error("Unable to load/detect WebSocket support: {}", e.getMessage());
logger.debug("Error loading/detecting WebSocket support.", e);
}
// Support not found
return false;

View File

@@ -8,7 +8,7 @@
<link rel="icon" type="image/png" href="images/guacamole-logo-64.png"/>
<link rel="icon" type="image/png" sizes="144x144" href="images/guacamole-logo-144.png"/>
<link rel="apple-touch-icon" type="image/png" href="images/guacamole-logo-144.png"/>
<link rel="stylesheet" type="text/css" href="guacamole.min.css">
<link rel="stylesheet" type="text/css" href="app.css">
<title ng-bind="page.title | translate"></title>
</head>
<!--
@@ -52,6 +52,6 @@
<!-- Login screen for logged-out users -->
<guac-login ng-show="expectedCredentials" form="expectedCredentials"></guac-login>
<script type="text/javascript" src="guacamole.min.js"></script>
<script type="text/javascript" src="app.js"></script>
</body>
</html>