mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-1364: Add "extension-priority" property for overriding extension load order.
This commit is contained in:
@@ -55,6 +55,11 @@ public class 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.
|
||||
@@ -357,6 +362,9 @@ public class Extension {
|
||||
*/
|
||||
public Extension(final ClassLoader parent, final File file) throws GuacamoleException {
|
||||
|
||||
// Associate extension abstraction with original file
|
||||
this.file = file;
|
||||
|
||||
try {
|
||||
|
||||
// Open extension
|
||||
@@ -427,6 +435,16 @@ public class Extension {
|
||||
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.
|
||||
|
@@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -40,7 +41,6 @@ import org.apache.guacamole.properties.StringSetProperty;
|
||||
import org.apache.guacamole.resource.Resource;
|
||||
import org.apache.guacamole.resource.ResourceServlet;
|
||||
import org.apache.guacamole.resource.SequenceResource;
|
||||
import org.apache.guacamole.resource.WebApplicationResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -105,6 +105,22 @@ public class ExtensionModule extends ServletModule {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A comma-separated list of the namespaces of all extensions that should
|
||||
* be loaded in a specific order. The special value "*" can be used in
|
||||
* lieu of a namespace to represent all extensions that are not listed. All
|
||||
* extensions explicitly listed will be sorted in the order given, while
|
||||
* all extensions not explicitly listed will be sorted by their filenames.
|
||||
*/
|
||||
public static final ExtensionOrderProperty EXTENSION_PRIORITY = new ExtensionOrderProperty() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "extension-priority";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The Guacamole server environment.
|
||||
*/
|
||||
@@ -394,9 +410,101 @@ public class ExtensionModule extends ServletModule {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comparator that sorts extensions by their desired load order,
|
||||
* as dictated by the "extension-priority" property and their filenames.
|
||||
*
|
||||
* @return
|
||||
* A comparator that sorts extensions by their desired load order.
|
||||
*/
|
||||
private Comparator<Extension> getExtensionLoadOrder() {
|
||||
|
||||
// Parse desired sort order of extensions
|
||||
try {
|
||||
return environment.getProperty(EXTENSION_PRIORITY, ExtensionOrderProperty.DEFAULT_COMPARATOR);
|
||||
}
|
||||
|
||||
// Sort by filename if the desired order cannot be read
|
||||
catch (GuacamoleException e) {
|
||||
logger.warn("The list of extensions specified via the \"{}\" property could not be parsed: {}", EXTENSION_PRIORITY.getName(), e.getMessage());
|
||||
logger.debug("Unable to parse \"{}\" property.", EXTENSION_PRIORITY.getName(), e);
|
||||
return ExtensionOrderProperty.DEFAULT_COMPARATOR;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all installed extensions in the order they should be
|
||||
* loaded. Extension load order is dictated by the "extension-priority"
|
||||
* property and by extension filename. Each extension within
|
||||
* GUACAMOLE_HOME/extensions is read and validated, but not fully loaded.
|
||||
* It is the responsibility of the caller to continue the load process with
|
||||
* the extensions in the returned list.
|
||||
*
|
||||
* @return
|
||||
* A list of all installed extensions, ordered by load priority.
|
||||
*/
|
||||
private List<Extension> getExtensions() {
|
||||
|
||||
// Retrieve and validate extensions directory
|
||||
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
|
||||
if (!extensionsDir.isDirectory())
|
||||
return Collections.emptyList();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Verify contents are accessible
|
||||
if (extensionFiles == null) {
|
||||
logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Read (but do not fully load) each extension within the extension
|
||||
// directory
|
||||
List<Extension> extensions = new ArrayList<>(extensionFiles.length);
|
||||
for (File extensionFile : extensionFiles) {
|
||||
|
||||
logger.debug("Reading 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.");
|
||||
}
|
||||
|
||||
extensions.add(extension);
|
||||
|
||||
}
|
||||
catch (GuacamoleException e) {
|
||||
logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage());
|
||||
logger.debug("Unable to load extension.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extensions.sort(getExtensionLoadOrder());
|
||||
return extensions;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all extensions within the GUACAMOLE_HOME/extensions directory, if
|
||||
* any, adding their static resource to the given resoure collections.
|
||||
* any, adding their static resource to the given resource collections.
|
||||
*
|
||||
* @param javaScriptResources
|
||||
* A modifiable collection of static JavaScript resources which may
|
||||
@@ -420,84 +528,57 @@ public class ExtensionModule extends ServletModule {
|
||||
Collection<Resource> cssResources,
|
||||
Set<String> toleratedAuthProviders) {
|
||||
|
||||
// Retrieve and validate extensions directory
|
||||
File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
|
||||
if (!extensionsDir.isDirectory())
|
||||
return;
|
||||
// Advise of current extension load order and how the order may be
|
||||
// changed
|
||||
List<Extension> extensions = getExtensions();
|
||||
if (extensions.size() > 1) {
|
||||
logger.info("Multiple extensions are installed and will be "
|
||||
+ "loaded in order of decreasing priority:");
|
||||
|
||||
// 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);
|
||||
for (Extension extension : extensions) {
|
||||
logger.info(" - [{}] \"{}\" ({})", extension.getNamespace(),
|
||||
extension.getName(), extension.getFile());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Verify contents are accessible
|
||||
if (extensionFiles == null) {
|
||||
logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read.");
|
||||
return;
|
||||
logger.info("To change this order, set the \"{}\" property or "
|
||||
+ "rename the extension files. The default priority of "
|
||||
+ "extensions is dictated by the sort order of their "
|
||||
+ "filenames.", EXTENSION_PRIORITY.getName());
|
||||
}
|
||||
|
||||
// Sort files lexicographically
|
||||
Arrays.sort(extensionFiles);
|
||||
// Load all extensions
|
||||
for (Extension extension : extensions) {
|
||||
|
||||
// Load each extension within the extension directory
|
||||
for (File extensionFile : extensionFiles) {
|
||||
// Add any JavaScript / CSS resources
|
||||
javaScriptResources.addAll(extension.getJavaScriptResources().values());
|
||||
cssResources.addAll(extension.getCSSResources().values());
|
||||
|
||||
logger.debug("Loading extension: \"{}\"", extensionFile.getName());
|
||||
// Attempt to load all authentication providers
|
||||
bindAuthenticationProviders(extension.getAuthenticationProviderClasses(), toleratedAuthProviders);
|
||||
|
||||
try {
|
||||
// Attempt to load all listeners
|
||||
bindListeners(extension.getListenerClasses());
|
||||
|
||||
// Load extension from file
|
||||
Extension extension = new Extension(getParentClassLoader(), extensionFile);
|
||||
// Add any translation resources
|
||||
serveLanguageResources(extension.getTranslationResources());
|
||||
|
||||
// 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 all HTML patch resources
|
||||
patchResourceService.addPatchResources(extension.getHTMLResources().values());
|
||||
|
||||
// Add any JavaScript / CSS resources
|
||||
javaScriptResources.addAll(extension.getJavaScriptResources().values());
|
||||
cssResources.addAll(extension.getCSSResources().values());
|
||||
// Add all static resources under namespace-derived prefix
|
||||
String staticResourcePrefix = "/app/ext/" + extension.getNamespace() + "/";
|
||||
serveStaticResources(staticResourcePrefix, extension.getStaticResources());
|
||||
|
||||
// Attempt to load all authentication providers
|
||||
bindAuthenticationProviders(extension.getAuthenticationProviderClasses(), toleratedAuthProviders);
|
||||
// Serve up the small favicon if provided
|
||||
if(extension.getSmallIcon() != null)
|
||||
serve("/images/logo-64.png").with(new ResourceServlet(extension.getSmallIcon()));
|
||||
|
||||
// Attempt to load all listeners
|
||||
bindListeners(extension.getListenerClasses());
|
||||
// Serve up the large favicon if provided
|
||||
if(extension.getLargeIcon()!= null)
|
||||
serve("/images/logo-144.png").with(new ResourceServlet(extension.getLargeIcon()));
|
||||
|
||||
// Add any translation resources
|
||||
serveLanguageResources(extension.getTranslationResources());
|
||||
|
||||
// Add all HTML patch resources
|
||||
patchResourceService.addPatchResources(extension.getHTMLResources().values());
|
||||
|
||||
// Add all static resources under namespace-derived prefix
|
||||
String staticResourcePrefix = "/app/ext/" + extension.getNamespace() + "/";
|
||||
serveStaticResources(staticResourcePrefix, extension.getStaticResources());
|
||||
|
||||
// Serve up the small favicon if provided
|
||||
if(extension.getSmallIcon() != null)
|
||||
serve("/images/logo-64.png").with(new ResourceServlet(extension.getSmallIcon()));
|
||||
|
||||
// Serve up the large favicon if provided
|
||||
if(extension.getLargeIcon()!= null)
|
||||
serve("/images/logo-144.png").with(new ResourceServlet(extension.getLargeIcon()));
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Log successful loading of extension by name
|
||||
logger.info("Extension \"{}\" ({}) loaded.", extension.getName(), extension.getNamespace());
|
||||
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.properties.GuacamoleProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A GuacamoleProperty that defines the order of Guacamole extensions. The
|
||||
* property value is a comma-separated list of extension namespaces, with "*"
|
||||
* used to represent all extensions that aren't listed. For example, a value
|
||||
* like "saml, *, ldap" would order SAML support first and LDAP support last,
|
||||
* with all other extensions loaded between the two in filename order. For
|
||||
* values without "*", all other extensions are implicitly after all extensions
|
||||
* that are explicitly listed.
|
||||
*/
|
||||
public abstract class ExtensionOrderProperty implements GuacamoleProperty<Comparator<Extension>> {
|
||||
|
||||
/**
|
||||
* Logger for this class.
|
||||
*/
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExtensionOrderProperty.class);
|
||||
|
||||
/**
|
||||
* A pattern which matches against the delimiters between values. This is
|
||||
* currently simply a comma and any following whitespace. Parts of the
|
||||
* input string which match this pattern will not be included in the parsed
|
||||
* result.
|
||||
*/
|
||||
private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*");
|
||||
|
||||
/**
|
||||
* Static comparator instance that sorts extensions by their filenames
|
||||
* alone.
|
||||
*/
|
||||
public static final Comparator<Extension> DEFAULT_COMPARATOR = new ExtensionComparator();
|
||||
|
||||
/**
|
||||
* Comparator that sorts extensions in order of priority, as dictated by a
|
||||
* list of the extensions that should be ordered first or last. All
|
||||
* extensions not explicitly listed will instead be sorted by filename.
|
||||
*/
|
||||
private static class ExtensionComparator implements Comparator<Extension> {
|
||||
|
||||
/**
|
||||
* The string value representing the set of all extensions not
|
||||
* explicitly listed.
|
||||
*/
|
||||
private final String OTHER_EXTENSIONS = "*";
|
||||
|
||||
/**
|
||||
* The relative priorities of all extensions. Any extension not listed
|
||||
* within this map should be sorted with the priority value stored in
|
||||
* {@link #defaultPriority}.
|
||||
*/
|
||||
private final Map<String, Integer> extensionPriority;
|
||||
|
||||
/**
|
||||
* The relative priority that should be used for all extensions not
|
||||
* explicitly listed within {@link #extensionPriority}.
|
||||
*/
|
||||
private final int defaultPriority;
|
||||
|
||||
/**
|
||||
* Creates a new ExtensionComparator that sorts all extensions by their
|
||||
* filenames only.
|
||||
*/
|
||||
public ExtensionComparator() {
|
||||
defaultPriority = 0;
|
||||
extensionPriority = Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ExtensionComparator that ensures each of the given
|
||||
* extensions are sorted in the relative order listed, with any
|
||||
* extensions not explicitly listed sorted by filename.
|
||||
*
|
||||
* @param name
|
||||
* The name of the property defining the provided list of
|
||||
* extensions.
|
||||
*
|
||||
* @param extensions
|
||||
* The namespaces of the extensions in the order they should be
|
||||
* sorted, with the special value "*" functioning as a
|
||||
* placeholder for all extensions that are not explicitly listed.
|
||||
*/
|
||||
public ExtensionComparator(String name, String... extensions) {
|
||||
|
||||
extensionPriority = new HashMap<>(extensions.length);
|
||||
|
||||
for (int priority = 0; priority < extensions.length; priority++) {
|
||||
String extension = extensions[priority];
|
||||
if (extensionPriority.putIfAbsent(extension, priority) != null)
|
||||
logger.warn("The value \"{}\" was specified multiple "
|
||||
+ "times for property \"{}\". Only the first "
|
||||
+ "occurrence of this value will have any effect.",
|
||||
extension, name);
|
||||
}
|
||||
|
||||
Integer otherExtensionPriority = extensionPriority.remove(OTHER_EXTENSIONS);
|
||||
if (otherExtensionPriority != null)
|
||||
defaultPriority = otherExtensionPriority;
|
||||
else
|
||||
defaultPriority = extensions.length;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Extension extA, Extension extB) {
|
||||
|
||||
int priorityA = extensionPriority.getOrDefault(extA.getNamespace(), defaultPriority);
|
||||
int priorityB = extensionPriority.getOrDefault(extB.getNamespace(), defaultPriority);
|
||||
|
||||
// Sort by explicit priority first
|
||||
if (priorityA != priorityB)
|
||||
return priorityA - priorityB;
|
||||
|
||||
// Sort all extensions without explicit priorities by their
|
||||
// filenames (no extensions will have the same priority except
|
||||
// those that aren't explicitly listed)
|
||||
return extA.getFile().compareTo(extB.getFile());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<Extension> parseValue(String value) throws GuacamoleException {
|
||||
|
||||
// If no property provided, return null.
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
// Split string into a set of individual values
|
||||
return new ExtensionComparator(getName(), DELIMITER_PATTERN.split(value));
|
||||
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user