blob: f6cf80df26e41763edb96d2b33811645192fc548 [file] [log] [blame]
/*
* 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 com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
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.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.resource.ClassPathResource;
import org.apache.guacamole.resource.Resource;
/**
* A Guacamole extension, which may provide custom authentication, static
* files, theming/branding, etc.
*/
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 extension .jar file.
*/
private final File file;
/**
* 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;
/**
* Map of all JavaScript resources defined within the extension, where each
* key is the path to that resource within the extension.
*/
private final Map<String, Resource> javaScriptResources;
/**
* Map of all CSS resources defined within the extension, where each key is
* the path to that resource within the extension.
*/
private final Map<String, Resource> cssResources;
/**
* Map of all HTML patch resources defined within the extension, where each
* key is the path to that resource within the extension.
*/
private final Map<String, Resource> htmlResources;
/**
* Map of all translation resources defined within the extension, where
* each key is the path to that resource within the extension.
*/
private final Map<String, Resource> translationResources;
/**
* Map of all resources defined within the extension which are not already
* associated as JavaScript, CSS, or translation resources, where each key
* is the path to that resource within the extension.
*/
private final Map<String, Resource> staticResources;
/**
* The collection of all AuthenticationProvider classes defined within the
* 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.
*/
private final Resource smallIcon;
/**
* The resource foe the large favicon for the extension. If provided, this
* will replace the default Guacamole icon.
*/
private final Resource largeIcon;
/**
* Returns a new map of all resources corresponding to the collection of
* paths provided. Each resource will be associated with the given
* mimetype, and stored in the map using its path as the key.
*
* @param mimetype
* The mimetype to associate with each resource.
*
* @param paths
* The paths corresponding to the resources desired.
*
* @return
* A new, unmodifiable map of resources corresponding to the
* collection of paths provided, where the key of each entry in the
* map is the path for the resource stored in that entry.
*/
private Map<String, Resource> getClassPathResources(String mimetype, Collection<String> paths) {
// If no paths are provided, just return an empty map
if (paths == null)
return Collections.<String, Resource>emptyMap();
// Add classpath resource for each path provided
Map<String, Resource> resources = new LinkedHashMap<>(paths.size());
for (String path : paths)
resources.put(path, new ClassPathResource(classLoader, mimetype, path));
// Callers should not rely on modifying the result
return Collections.unmodifiableMap(resources);
}
/**
* Returns a new map of all resources corresponding to the map of resource
* paths provided. Each resource will be associated with the mimetype
* stored in the given map using its path as the key.
*
* @param resourceTypes
* A map of all paths to their corresponding mimetypes.
*
* @return
* A new, unmodifiable map of resources corresponding to the
* collection of paths provided, where the key of each entry in the
* map is the path for the resource stored in that entry.
*/
private Map<String, Resource> getClassPathResources(Map<String, String> resourceTypes) {
// If no paths are provided, just return an empty map
if (resourceTypes == null)
return Collections.<String, Resource>emptyMap();
// Add classpath resource for each path/mimetype pair provided
Map<String, Resource> resources = new LinkedHashMap<>(resourceTypes.size());
for (Map.Entry<String, String> resource : resourceTypes.entrySet()) {
// Get path and mimetype from entry
String path = resource.getKey();
String mimetype = resource.getValue();
// Store as path/resource pair
resources.put(path, new ClassPathResource(classLoader, mimetype, path));
}
// Callers should not rely on modifying the result
return Collections.unmodifiableMap(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);
}
catch (LinkageError e) {
throw new GuacamoleException("Authentication provider class cannot be loaded (wrong version of API?).", 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);
}
/**
* 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.
*
* @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.
*
* @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
* 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,
final List<File> temporaryFiles) throws GuacamoleException {
// Associate extension abstraction with original file
this.file = file;
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);
if (manifest == null)
throw new GuacamoleServerException("Contents of " + MANIFEST_NAME + " must be a valid JSON object.");
}
// Always close zip file, if possible
finally {
extension.close();
}
// Create isolated classloader for this extension
classLoader = ExtensionClassLoader.getInstance(file, temporaryFiles, parent);
}
// 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());
htmlResources = getClassPathResources("text/html", manifest.getHTMLPaths());
translationResources = getClassPathResources("application/json", manifest.getTranslationPaths());
staticResources = getClassPathResources(manifest.getResourceTypes());
// 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());
else
smallIcon = null;
// Get large icon resource if provided
if (manifest.getLargeIcon() != null)
largeIcon = new ClassPathResource(classLoader, "image/png", manifest.getLargeIcon());
else
largeIcon = null;
}
/**
* Returns the .jar file containing this Guacamole extension.
*
* @return
* The extension .jar file.
*/
public File getFile() {
return file;
}
/**
* 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 a map of all declared JavaScript resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. JavaScript resources are declared
* within the extension manifest.
*
* @return
* All declared JavaScript resources associated with this extension.
*/
public Map<String, Resource> getJavaScriptResources() {
return javaScriptResources;
}
/**
* Returns a map of all declared CSS resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. CSS resources are declared within
* the extension manifest.
*
* @return
* All declared CSS resources associated with this extension.
*/
public Map<String, Resource> getCSSResources() {
return cssResources;
}
/**
* Returns a map of all declared HTML patch resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. HTML patch resources are declared
* within the extension manifest.
*
* @return
* All declared HTML patch resources associated with this extension.
*/
public Map<String, Resource> getHTMLResources() {
return htmlResources;
}
/**
* Returns a map of all declared translation resources associated with this
* extension, where the key of each entry in the map is the path to that
* resource within the extension .jar. Translation resources are declared
* within the extension manifest.
*
* @return
* All declared translation resources associated with this extension.
*/
public Map<String, Resource> getTranslationResources() {
return translationResources;
}
/**
* Returns a map of all declared resources associated with this extension,
* where these resources are not already associated as JavaScript, CSS, or
* translation resources. The key of each entry in the map is the path to
* that resource within the extension .jar. Static resources are declared
* within the extension manifest.
*
* @return
* All declared static resources associated with this extension.
*/
public Map<String, Resource> getStaticResources() {
return staticResources;
}
/**
* 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;
}
/**
* 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.
*
* @return
* The resource for the small favicon.
*/
public Resource getSmallIcon() {
return smallIcon;
}
/**
* Returns the resource for the large favicon for the extension. If
* provided, this will replace the default Guacamole icon.
*
* @return
* The resource for the large favicon.
*/
public Resource getLargeIcon() {
return largeIcon;
}
}