GUACAMOLE-1508: Merge add support for nesting .jar files within extensions.

This commit is contained in:
Virtually Nick
2022-01-23 15:06:51 -05:00
committed by GitHub
5 changed files with 282 additions and 31 deletions

View File

@@ -64,12 +64,11 @@
<id>copy-runtime-dependencies</id> <id>copy-runtime-dependencies</id>
<phase>prepare-package</phase> <phase>prepare-package</phase>
<goals> <goals>
<goal>unpack-dependencies</goal> <goal>copy-dependencies</goal>
</goals> </goals>
<configuration> <configuration>
<includeScope>runtime</includeScope> <includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/classes</outputDirectory> <outputDirectory>${project.build.directory}/classes</outputDirectory>
<excludes>META-INF/*.SF,META-INF/*.DSA</excludes>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@@ -19,6 +19,7 @@
package org.apache.guacamole; package org.apache.guacamole;
import com.google.common.collect.Lists;
import org.apache.guacamole.tunnel.TunnelModule; import org.apache.guacamole.tunnel.TunnelModule;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
@@ -117,6 +118,14 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
@Inject @Inject
private List<AuthenticationProvider> authProviders; private List<AuthenticationProvider> authProviders;
/**
* All temporary files that should be deleted upon application shutdown, in
* reverse order of desired deletion. This will typically simply be the
* order that each file was created.
*/
@Inject
private List<File> temporaryFiles;
/** /**
* Internal reference to the Guice injector that was lazily created when * Internal reference to the Guice injector that was lazily created when
* getInjector() was first invoked. * getInjector() was first invoked.
@@ -194,20 +203,49 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener
}); });
} }
/**
* Deletes the given temporary file/directory, if possible. If the deletion
* operation fails, a warning is logged noting the failure. If the given
* file is a directory, it will only be deleted if empty.
*
* @param temp
* The temporary file to delete.
*/
private void deleteTemporaryFile(File temp) {
if (!temp.delete()) {
logger.warn("Temporary file/directory \"{}\" could not be "
+ "deleted. The file may remain until the JVM exits, or "
+ "may need to be manually deleted.", temp);
}
else
logger.debug("Deleted temporary file/directory \"{}\".", temp);
}
@Override @Override
public void contextDestroyed(ServletContextEvent servletContextEvent) { public void contextDestroyed(ServletContextEvent servletContextEvent) {
try {
// Clean up reference to Guice injector
servletContextEvent.getServletContext().removeAttribute(GUICE_INJECTOR);
// Clean up reference to Guice injector // Shutdown TokenSessionMap
servletContextEvent.getServletContext().removeAttribute(GUICE_INJECTOR); if (sessionMap != null)
sessionMap.shutdown();
// Shutdown TokenSessionMap // Unload all extensions
if (sessionMap != null) if (authProviders != null) {
sessionMap.shutdown(); for (AuthenticationProvider authProvider : authProviders)
authProvider.shutdown();
}
}
finally {
// Regardless of what may succeed/fail here, always attempt to
// clean up ALL temporary files
if (temporaryFiles != null)
Lists.reverse(temporaryFiles).stream().forEachOrdered(this::deleteTemporaryFile);
// Unload all extensions
if (authProviders != null) {
for (AuthenticationProvider authProvider : authProviders)
authProvider.shutdown();
} }
// Continue any Guice-specific cleanup // Continue any Guice-specific cleanup

View File

@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipException; import java.util.zip.ZipException;
@@ -355,12 +356,18 @@ public class Extension {
* @param file * @param file
* The file to load as an extension. * The file to load as an extension.
* *
* @param temporaryFiles
* A modifiable List that should be populated with all temporary files
* created for this extension. These files should be deleted on
* application shutdown in reverse order.
*
* @throws GuacamoleException * @throws GuacamoleException
* If the provided file is not a .jar file, does not contain the * 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 * guac-manifest.json, or if guac-manifest.json is invalid and cannot
* be parsed. * be parsed.
*/ */
public Extension(final ClassLoader parent, final File file) throws GuacamoleException { public Extension(final ClassLoader parent, final File file,
final List<File> temporaryFiles) throws GuacamoleException {
// Associate extension abstraction with original file // Associate extension abstraction with original file
this.file = file; this.file = file;
@@ -390,7 +397,7 @@ public class Extension {
} }
// Create isolated classloader for this extension // Create isolated classloader for this extension
classLoader = ExtensionClassLoader.getInstance(file, parent); classLoader = ExtensionClassLoader.getInstance(file, temporaryFiles, parent);
} }

View File

@@ -20,14 +20,27 @@
package org.apache.guacamole.extension; package org.apache.guacamole.extension;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedActionException; import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* ClassLoader implementation which prioritizes the classes defined within a * ClassLoader implementation which prioritizes the classes defined within a
@@ -38,6 +51,23 @@ import org.apache.guacamole.GuacamoleServerException;
*/ */
public class ExtensionClassLoader extends URLClassLoader { public class ExtensionClassLoader extends URLClassLoader {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ExtensionClassLoader.class);
/**
* The prefix that should be given to the temporary directory containing
* all library .jar files that were bundled with the extension.
*/
private static final String EXTENSION_TEMP_DIR_PREFIX = "guac-extension-lib-";
/**
* The prefix that should be given to any files created for temporary
* storage of a library .jar file that was bundled with the extension.
*/
private static final String EXTENSION_TEMP_LIB_PREFIX = "bundled-";
/** /**
* The ClassLoader to use if class resolution through the extension .jar * The ClassLoader to use if class resolution through the extension .jar
* fails. * fails.
@@ -54,6 +84,11 @@ public class ExtensionClassLoader extends URLClassLoader {
* @param extension * @param extension
* The extension .jar file from which classes should be loaded. * The extension .jar file from which classes should be loaded.
* *
* @param temporaryFiles
* A modifiable List that should be populated with all temporary files
* created for the given extension. These files should be deleted on
* application shutdown in reverse order.
*
* @param parent * @param parent
* The ClassLoader to use if class resolution through the extension * The ClassLoader to use if class resolution through the extension
* .jar fails. * .jar fails.
@@ -67,7 +102,8 @@ public class ExtensionClassLoader extends URLClassLoader {
* file cannot be read. * file cannot be read.
*/ */
public static ExtensionClassLoader getInstance(final File extension, public static ExtensionClassLoader getInstance(final File extension,
final ClassLoader parent) throws GuacamoleException { final List<File> temporaryFiles, final ClassLoader parent)
throws GuacamoleException {
try { try {
// Attempt to create classloader which loads classes from the given // Attempt to create classloader which loads classes from the given
@@ -76,7 +112,7 @@ public class ExtensionClassLoader extends URLClassLoader {
@Override @Override
public ExtensionClassLoader run() throws GuacamoleException { public ExtensionClassLoader run() throws GuacamoleException {
return new ExtensionClassLoader(extension, parent); return new ExtensionClassLoader(extension, temporaryFiles, parent);
} }
}); });
@@ -89,27 +125,26 @@ public class ExtensionClassLoader extends URLClassLoader {
} }
/** /**
* Returns a URL which points to the given extension .jar file. * Returns the URL that refers to the given file. If the given file refers
* to a directory, an exception is thrown.
* *
* @param extension * @param file
* The extension .jar file to generate a URL for. * The file to determine the URL of.
* *
* @return * @return
* A URL which points to the given extension .jar. * A URL that refers to the given file.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If the given file is not actually a file, or the contents of the * If the given file refers to a directory.
* file cannot be read.
*/ */
private static URL getExtensionURL(File extension) private static URL getFileURL(File file) throws GuacamoleException {
throws GuacamoleException {
// Validate extension file is indeed a file // Validate extension-related file is indeed a file
if (!extension.isFile()) if (!file.isFile())
throw new GuacamoleException(extension + " is not a file."); throw new GuacamoleServerException("\"" + file + "\" is not a file.");
try { try {
return extension.toURI().toURL(); return file.toURI().toURL();
} }
catch (MalformedURLException e) { catch (MalformedURLException e) {
throw new GuacamoleServerException(e); throw new GuacamoleServerException(e);
@@ -117,6 +152,152 @@ public class ExtensionClassLoader extends URLClassLoader {
} }
/**
* Copies all bytes of data from a file within a .jar to a destination
* file.
*
* @param jar
* The JarFile containing the file to be copied.
*
* @param source
* The JarEntry representing the file to be copied within the given
* JarFile.
*
* @param dest
* The destination file that the data should be copied to.
*
* @throws IOException
* If an error occurs reading from the source .jar or writing to the
* destination file.
*/
private static void copyEntryToFile(JarFile jar, JarEntry source, File dest)
throws IOException {
int length;
byte[] buffer = new byte[8192];
try (InputStream input = jar.getInputStream(source)) {
try (OutputStream output = new FileOutputStream(dest)) {
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
}
}
}
/**
* Returns the URLs for the .jar files relevant to the given extension .jar
* file. Unless the extension bundles additional Java libraries, only the
* URL of the extension .jar will be returned. If additional Java libraries
* are bundled within the extension, URLs for those libraries will be
* included, as well. Temporary directories and/or files will be created as
* necessary to house bundled libraries. Only .jar files located directly
* within the root of the main extension .jar are considered.
*
* @param extension
* The extension .jar file to generate URLs for.
*
* @param temporaryFiles
* A modifiable List that should be populated with all temporary files
* created for the given extension. These files should be deleted on
* application shutdown in reverse order.
*
* @return
* An array of all URLs relevant to the given extension .jar.
*
* @throws GuacamoleException
* If the given file is not actually a file, the contents of the file
* cannot be read, or any necessary temporary files/directories cannot
* be created.
*/
private static URL[] getExtensionURLs(File extension,
List<File> temporaryFiles) throws GuacamoleException {
JarFile extensionJar;
try {
extensionJar = new JarFile(extension);
}
catch (IOException e) {
throw new GuacamoleServerException("Contents of extension \""
+ extension + "\" cannot be read.", e);
}
// Include extension itself within classpath
List<URL> urls = new ArrayList<>();
urls.add(getFileURL(extension));
Path extensionTempLibDir = null;
// Iterate through all entries (files) within the extension .jar,
// adding any nested .jar files within the archive root to the
// classpath
Enumeration<JarEntry> entries = extensionJar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
// Consider only .jar files located in root of archive
if (entry.isDirectory() ||! name.endsWith(".jar") || name.indexOf('/') != -1)
continue;
// Create temporary directory for housing this extension's
// bundled .jar files, if not already created
try {
if (extensionTempLibDir == null) {
extensionTempLibDir = Files.createTempDirectory(EXTENSION_TEMP_DIR_PREFIX);
temporaryFiles.add(extensionTempLibDir.toFile());
extensionTempLibDir.toFile().deleteOnExit();
}
}
catch (IOException e) {
throw new GuacamoleServerException("Temporary directory "
+ "for libraries bundled with extension \""
+ extension + "\" could not be created.", e);
}
// Create temporary file to hold the contents of the current
// bundled .jar
File tempLibrary;
try {
tempLibrary = Files.createTempFile(extensionTempLibDir, EXTENSION_TEMP_LIB_PREFIX, ".jar").toFile();
temporaryFiles.add(tempLibrary);
tempLibrary.deleteOnExit();
}
catch (IOException e) {
throw new GuacamoleServerException("Temporary file "
+ "for library \"" + name + "\" bundled with "
+ "extension \"" + extension + "\" could not be "
+ "created.", e);
}
// Copy contents of bundled .jar to temporary file
try {
copyEntryToFile(extensionJar, entry, tempLibrary);
}
catch (IOException e) {
throw new GuacamoleServerException("Contents of library "
+ "\"" + name + "\" bundled with extension \""
+ extension + "\" could not be copied to a "
+ "temporary file.", e);
}
// Add temporary .jar file to classpath
urls.add(getFileURL(tempLibrary));
}
if (extensionTempLibDir != null)
logger.debug("Libraries bundled within extension \"{}\" have been "
+ "copied to temporary directory \"{}\".", extension, extensionTempLibDir);
return urls.toArray(new URL[0]);
}
/** /**
* Creates a new ExtensionClassLoader configured to load classes from the * Creates a new ExtensionClassLoader configured to load classes from the
* given extension .jar. If a necessary class cannot be found within the * given extension .jar. If a necessary class cannot be found within the
@@ -127,6 +308,11 @@ public class ExtensionClassLoader extends URLClassLoader {
* @param extension * @param extension
* The extension .jar file from which classes should be loaded. * The extension .jar file from which classes should be loaded.
* *
* @param temporaryFiles
* A modifiable List that should be populated with all temporary files
* created for the given extension. These files should be deleted on
* application shutdown in reverse order.
*
* @param parent * @param parent
* The ClassLoader to use if class resolution through the extension * The ClassLoader to use if class resolution through the extension
* .jar fails. * .jar fails.
@@ -135,9 +321,9 @@ public class ExtensionClassLoader extends URLClassLoader {
* If the given file is not actually a file, or the contents of the * If the given file is not actually a file, or the contents of the
* file cannot be read. * file cannot be read.
*/ */
private ExtensionClassLoader(File extension, ClassLoader parent) private ExtensionClassLoader(File extension, List<File> temporaryFiles,
throws GuacamoleException { ClassLoader parent) throws GuacamoleException {
super(new URL[]{ getExtensionURL(extension) }, null); super(getExtensionURLs(extension, temporaryFiles), null);
this.parent = parent; this.parent = parent;
} }

View File

@@ -140,6 +140,13 @@ public class ExtensionModule extends ServletModule {
private final List<Listener> boundListeners = private final List<Listener> boundListeners =
new ArrayList<Listener>(); new ArrayList<Listener>();
/**
* All temporary files that should be deleted upon application shutdown, in
* reverse order of desired deletion. This will typically simply be the
* order that each file was created.
*/
private final List<File> temporaryFiles = new ArrayList<>();
/** /**
* Service for adding and retrieving language resources. * Service for adding and retrieving language resources.
*/ */
@@ -261,6 +268,20 @@ public class ExtensionModule extends ServletModule {
return Collections.unmodifiableList(boundAuthenticationProviders); return Collections.unmodifiableList(boundAuthenticationProviders);
} }
/**
* Returns a list of all temporary files that should be deleted upon
* application shutdown, in reverse order of desired deletion. This will
* typically simply be the order that each file was created.
*
* @return
* A List of all temporary files that should be deleted upon
* application shutdown. The List is not modifiable.
*/
@Provides
public List<File> getTemporaryFiles() {
return Collections.unmodifiableList(temporaryFiles);
}
/** /**
* Binds the given provider class such that a listener is bound for each * 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 interface implemented by the provider and such that all bound
@@ -479,7 +500,7 @@ public class ExtensionModule extends ServletModule {
try { try {
// Load extension from file // Load extension from file
Extension extension = new Extension(getParentClassLoader(), extensionFile); Extension extension = new Extension(getParentClassLoader(), extensionFile, temporaryFiles);
// Validate Guacamole version of extension // Validate Guacamole version of extension
if (!isCompatible(extension.getGuacamoleVersion())) { if (!isCompatible(extension.getGuacamoleVersion())) {