/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2012 ZAP development team
*
* Licensed 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.zaproxy.zap.control;
import java.awt.EventQueue;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.core.scanner.AbstractPlugin;
import org.parosproxy.paros.extension.Extension;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.control.AddOn.AddOnRunRequirements;
import org.zaproxy.zap.control.AddOn.ExtensionRunRequirements;
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;
/**
* This class is heavily based on the original Paros class org.parosproxy.paros.common.DynamicLoader
* However its been restructured and enhanced to support multiple directories or versioned ZAP addons.
* The constructor takes an array of directories.
* All of the generic jars in the directories are loaded.
* Only the latest ZAP addons are loaded, so if the following addons are found:
* zap-ext-test-alpha-1.zap
* zap-ext-test-beta-2.zap
* zap-ext-test-alpha-3.zap
* then only the latest one (zap-ext-test-alpha-3.zap) will be loaded - this is entirely based on the
* version number.
* The status (alpha/beta/release) is for informational purposes only.
*/
public class AddOnLoader extends URLClassLoader {
public static final String ADDONS_BLOCK_LIST = "addons.block";
private static final String ADDONS_RUNNABLE_BASE_KEY = "runnableAddOns";
private static final String ADDONS_RUNNABLE_KEY = ADDONS_RUNNABLE_BASE_KEY + ".addon";
private static final String ADDON_RUNNABLE_ID_KEY = "id";
private static final String ADDON_RUNNABLE_VERSION_KEY = "version";
private static final String ADDON_RUNNABLE_ALL_EXTENSIONS_KEY = "extensions.extension";
/**
* A "null" object, for use when no callback is given during the uninstallation process.
*/
private static final AddOnUninstallationProgressCallback NULL_CALLBACK = new NullUninstallationProgressCallBack();
private static final Logger logger = Logger.getLogger(AddOnLoader.class);
private AddOnCollection aoc = null;
private List<File> jars = new ArrayList<>();
/**
* Addons can be included in the ZAP release, in which case the user might not have permissions to delete the files.
* To support the removal of such addons we just maintain a 'block list' in the configs which is a comma separated
* list of the addon ids that the user has uninstalled
*/
private List<String> blockList = new ArrayList<>();
/**
* The runnable add-ons and its extensions.
* <p>
* The key is the add-on itself and the value its runnable extensions.
*/
private Map<AddOn, List<String>> runnableAddOns;
/**
* The list of add-ons' IDs that have running issues (either the add-on itself or one of its extensions) since last run
* because of changes in the dependencies.
*/
private List<String> idsAddOnsWithRunningIssuesSinceLastRun;
/*
* Using sub-classloaders means we can unload and reload addons
*/
private Map<String, AddOnClassLoader> addOnLoaders = new HashMap<>();
public AddOnLoader(File[] dirs) {
super(new URL[0], AddOnLoader.class.getClassLoader());
this.loadBlockList();
this.aoc = new AddOnCollection(dirs);
loadAllAddOns();
if (dirs != null) {
for (File dir : dirs) {
try {
this.addDirectory(dir);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
for (File jar : jars) {
try {
this.addURL(jar.toURI().toURL());
} catch (MalformedURLException e) {
logger.error(e.getMessage(), e);
}
}
// Install any files that are not already present
for (Entry<String, AddOnClassLoader> entry : addOnLoaders.entrySet()) {
AddOnInstaller.installMissingAddOnFiles(entry.getValue(), getAddOnCollection().getAddOn(entry.getKey()));
}
}
/**
* Returns a list with the IDs of add-ons that have running issues since last run, either Java version was changed, or
* add-on dependencies are no longer met for the add-on or one of its extensions.
*
* @return a list with the add-ons that are not longer runnable
* @since 2.4.0
*/
public List<String> getIdsAddOnsWithRunningIssuesSinceLastRun() {
return Collections.unmodifiableList(idsAddOnsWithRunningIssuesSinceLastRun);
}
private void loadAllAddOns() {
runnableAddOns = new HashMap<>();
idsAddOnsWithRunningIssuesSinceLastRun = new ArrayList<>();
Map<AddOn, List<String>> oldRunnableAddOns = loadAddOnsRunState(aoc);
List<AddOn> runAddons = new ArrayList<>();
for (Iterator<AddOn> iterator = aoc.getAddOns().iterator(); iterator.hasNext();) {
AddOn addOn = iterator.next();
if (canLoadAddOn(addOn)) {
AddOnRunRequirements reqs = calculateRunRequirements(addOn, aoc.getAddOns());
if (reqs.isRunnable()) {
List<String> runnableExtensions;
if (addOn.hasExtensionsWithDeps()) {
runnableExtensions = getRunnableExtensionsWithDeps(reqs);
List<String> oldRunnableExtensions = oldRunnableAddOns.get(addOn);
if (oldRunnableExtensions != null && !oldRunnableExtensions.isEmpty()) {
oldRunnableExtensions.removeAll(runnableExtensions);
if (!oldRunnableExtensions.isEmpty()) {
idsAddOnsWithRunningIssuesSinceLastRun.add(addOn.getId());
}
}
} else {
runnableExtensions = Collections.emptyList();
}
runnableAddOns.put(addOn, runnableExtensions);
runAddons.add(addOn);
} else if (oldRunnableAddOns.get(addOn) != null) {
idsAddOnsWithRunningIssuesSinceLastRun.add(addOn.getId());
}
} else {
iterator.remove();
}
}
saveAddOnsRunState(runnableAddOns);
for (AddOn addOn : runAddons) {
addOn.setInstallationStatus(AddOn.InstallationStatus.INSTALLED);
createAndAddAddOnClassLoader(addOn);
}
}
private static List<String> getRunnableExtensionsWithDeps(AddOnRunRequirements runRequirements) {
List<String> runnableExtensions = new ArrayList<>();
for (ExtensionRunRequirements extReqs : runRequirements.getExtensionRequirements()) {
if (extReqs.isRunnable()) {
runnableExtensions.add(extReqs.getClassname());
}
}
return runnableExtensions;
}
private boolean canLoadAddOn(AddOn ao) {
if (blockList.contains(ao.getId())) {
if (logger.isDebugEnabled()) {
logger.debug("Can't load add-on " + ao.getName()
+ " it's on the block list (add-on uninstalled but the file couldn't be removed).");
}
return false;
}
if (!ao.canLoadInCurrentVersion()) {
if (logger.isDebugEnabled()) {
logger.debug("Can't load add-on " + ao.getName() + " because of ZAP version constraints; Not before="
+ ao.getNotBeforeVersion() + " Not from=" + ao.getNotFromVersion() + " Current Version="
+ Constant.PROGRAM_VERSION);
}
return false;
}
return true;
}
private static AddOnRunRequirements calculateRunRequirements(AddOn ao, Collection<AddOn> availableAddOns) {
AddOnRunRequirements reqs = ao.calculateRunRequirements(availableAddOns);
if (!reqs.isRunnable()) {
if (logger.isDebugEnabled()) {
logger.debug("Can't run add-on " + ao.getName() + " because of missing requirements: "
+ AddOnRunIssuesUtils.getRunningIssues(reqs));
}
}
return reqs;
}
private AddOnClassLoader createAndAddAddOnClassLoader(AddOn ao) {
try {
AddOnClassLoader addOnClassLoader = addOnLoaders.get(ao.getId());
if (addOnClassLoader != null) {
return addOnClassLoader;
}
List<String> idsAddOnDependencies = ao.getIdsAddOnDependencies();
if (idsAddOnDependencies.isEmpty()) {
addOnClassLoader = new AddOnClassLoader(ao.getFile().toURI().toURL(), this, ao.getAddOnClassnames());
this.addOnLoaders.put(ao.getId(), addOnClassLoader);
return addOnClassLoader;
}
List<AddOnClassLoader> dependencies = new ArrayList<>(idsAddOnDependencies.size());
for (String addOnId : idsAddOnDependencies) {
addOnClassLoader = addOnLoaders.get(addOnId);
if (addOnClassLoader == null) {
addOnClassLoader = createAndAddAddOnClassLoader(aoc.getAddOn(addOnId));
}
dependencies.add(addOnClassLoader);
}
addOnClassLoader = new AddOnClassLoader(ao.getFile().toURI().toURL(), this, dependencies, ao.getAddOnClassnames());
this.addOnLoaders.put(ao.getId(), addOnClassLoader);
return addOnClassLoader;
} catch (MalformedURLException e) {
logger.error(e.getMessage(), e);
throw new RuntimeException("Failed to convert URL for AddOnClassLoader " + ao.getFile().toURI(), e);
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
return loadClass(name, false);
} catch (ClassNotFoundException e) {
// Continue for now
}
for (AddOnClassLoader loader : addOnLoaders.values()) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException e) {
// Continue for now
}
}
throw new ClassNotFoundException(name);
}
@Override
public URL getResource(String name) {
URL url = super.getResource(name);
if (url != null) {
return url;
}
for (AddOnClassLoader loader : addOnLoaders.values()) {
url = loader.findResourceInAddOn(name);
if (url != null) {
return url;
}
}
return url;
}
public AddOnCollection getAddOnCollection() {
return this.aoc;
}
private void addDirectory (File dir) {
if (dir == null) {
logger.error("Null directory supplied");
return;
}
if (! dir.exists()) {
logger.error("No such directory: " + dir.getAbsolutePath());
return;
}
if (! dir.isDirectory()) {
logger.error("Not a directory: " + dir.getAbsolutePath());
return;
}
// Load the jar files
File[] listJars = dir.listFiles(new JarFilenameFilter());
if (listJars != null) {
for (File jar : listJars) {
this.jars.add(jar);
}
}
}
public synchronized void addAddon(AddOn ao) {
if (! ao.canLoadInCurrentVersion()) {
throw new IllegalArgumentException("Cant load add-on " + ao.getName() +
" Not before=" + ao.getNotBeforeVersion() + " Not from=" + ao.getNotFromVersion() +
" Version=" + Constant.PROGRAM_VERSION);
}
if (!this.aoc.addAddOn(ao)) {
return;
}
addAddOnImpl(ao);
}
private void addAddOnImpl(AddOn ao) {
if (AddOn.InstallationStatus.INSTALLED == ao.getInstallationStatus()) {
return;
}
if (this.blockList.contains(ao.getId())) {
// Explicitly being added back, so remove from the block list
this.blockList.remove(ao.getId());
this.saveBlockList();
}
if (!isDynamicallyInstallable(ao)) {
return;
}
AddOnRunRequirements reqs = calculateRunRequirements(ao, aoc.getInstalledAddOns());
if (!reqs.isRunnable()) {
ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED);
return;
}
AddOnInstaller.install(createAndAddAddOnClassLoader(ao), ao);
ao.setInstallationStatus(AddOn.InstallationStatus.INSTALLED);
Control.getSingleton().getExtensionLoader().addOnInstalled(ao);
if (runnableAddOns.get(ao) == null) {
runnableAddOns.put(ao, getRunnableExtensionsWithDeps(reqs));
saveAddOnsRunState(runnableAddOns);
}
checkAndLoadDependentExtensions();
checkAndInstallAddOnsNotInstalled();
if (View.isInitialised()) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
View.getSingleton().refreshTabViewMenus();
}
});
}
}
/**
* Checks and installs all the add-ons whose installation status is {@code NOT_INSTALLED} that have (now) all required
* dependencies fulfilled.
* <p>
* Should be called after an installation of an add-on.
*
* @see #addAddOnImpl(AddOn)
* @see AddOn.InstallationStatus#NOT_INSTALLED
* @since 2.4.0
*/
private void checkAndInstallAddOnsNotInstalled() {
List<AddOn> runnableAddOns = new ArrayList<>();
for (AddOn addOn : aoc.getAddOns()) {
if (AddOn.InstallationStatus.NOT_INSTALLED == addOn.getInstallationStatus() && addOnLoaders.get(addOn.getId()) == null) {
AddOnRunRequirements reqs = addOn.calculateRunRequirements(aoc.getInstalledAddOns());
if (reqs.isRunnable()) {
runnableAddOns.add(addOn);
}
}
}
for (AddOn addOn : runnableAddOns) {
addAddOnImpl(addOn);
}
}
/**
* Checks and loads all the extensions that have (now) all required dependencies fulfilled.
* <p>
* Should be called after an installation of an add-on.
*
* @see #addAddOnImpl(AddOn)
* @since 2.4.0
*/
private void checkAndLoadDependentExtensions() {
boolean changed = false;
for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) {
AddOn runningAddOn = aoc.getAddOn(entry.getKey());
for (String extClassName : runningAddOn.getExtensionsWithDeps()) {
if (!runningAddOn.isExtensionLoaded(extClassName)) {
AddOn.AddOnRunRequirements reqs = runningAddOn.calculateExtensionRunRequirements(
extClassName,
aoc.getInstalledAddOns());
ExtensionRunRequirements extReqs = reqs.getExtensionRequirements().get(0);
if (extReqs.isRunnable()) {
List<AddOnClassLoader> dependencies = new ArrayList<>(extReqs.getDependencies().size());
for (AddOn addOnDep : extReqs.getDependencies()) {
dependencies.add(addOnLoaders.get(addOnDep.getId()));
}
AddOnClassLoader extAddOnClassLoader = new AddOnClassLoader(
entry.getValue(),
dependencies,
runningAddOn.getExtensionAddOnClassnames(extClassName));
Extension ext = loadAddOnExtension(runningAddOn, extReqs.getClassname(), extAddOnClassLoader);
AddOnInstaller.installAddOnExtension(runningAddOn, ext);
runnableAddOns.get(runningAddOn).add(extReqs.getClassname());
changed = true;
}
}
}
}
if (changed) {
saveAddOnsRunState(runnableAddOns);
}
}
/**
* Tells whether or not the given {@code addOn} is dynamically installable.
* <p>
* It checks if the given {@code addOn} is dynamically installable by calling the method {@code AddOn#hasZapAddOnEntry()}.
*
* @param addOn the add-on that will be checked
* @return {@code true} if the given add-on is dynamically installable, {@code false} otherwise.
* @see AddOn#hasZapAddOnEntry()
* @since 2.3.0
*/
private static boolean isDynamicallyInstallable(AddOn addOn) {
return addOn.hasZapAddOnEntry();
}
public synchronized boolean removeAddOn(AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback progressCallback) {
AddOnUninstallationProgressCallback callback = (progressCallback == null) ? NULL_CALLBACK : progressCallback;
callback.uninstallingAddOn(ao, upgrading);
boolean removed = removeAddOnImpl(ao, upgrading, callback);
callback.addOnUninstalled(removed);
return removed;
}
private boolean removeAddOnImpl(AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback callback) {
if (!isDynamicallyInstallable(ao)) {
return false;
}
if (AddOn.InstallationStatus.SOFT_UNINSTALLATION_FAILED == ao.getInstallationStatus()) {
if (runnableAddOns.remove(ao) != null) {
saveAddOnsRunState(runnableAddOns);
}
AddOnInstaller.uninstallAddOnFiles(ao, NULL_CALLBACK);
removeAddOnClassLoader(ao);
deleteAddOnFile(ao, upgrading);
ao.setInstallationStatus(AddOn.InstallationStatus.UNINSTALLATION_FAILED);
Control.getSingleton().getExtensionLoader().addOnUninstalled(ao, false);
return false;
}
if (!this.aoc.includesAddOn(ao.getId())) {
logger.warn("Trying to uninstall an add-on that is not installed: " + ao.getId());
return false;
}
if (AddOn.InstallationStatus.NOT_INSTALLED == ao.getInstallationStatus()) {
if (runnableAddOns.remove(ao) != null) {
saveAddOnsRunState(runnableAddOns);
}
deleteAddOnFile(ao, upgrading);
return this.aoc.removeAddOn(ao);
}
unloadDependentExtensions(ao);
softUninstallDependentAddOns(ao);
boolean uninstalledWithoutErrors = AddOnInstaller.uninstall(ao, callback);
if (uninstalledWithoutErrors && ! this.aoc.removeAddOn(ao)) {
uninstalledWithoutErrors = false;
}
if (uninstalledWithoutErrors) {
removeAddOnClassLoader(ao);
}
deleteAddOnFile(ao, upgrading);
if (runnableAddOns.remove(ao) != null) {
saveAddOnsRunState(runnableAddOns);
}
ao.setInstallationStatus(uninstalledWithoutErrors
? AddOn.InstallationStatus.AVAILABLE
: AddOn.InstallationStatus.UNINSTALLATION_FAILED);
Control.getSingleton().getExtensionLoader().addOnUninstalled(ao, uninstalledWithoutErrors);
return uninstalledWithoutErrors;
}
private void deleteAddOnFile(AddOn addOn, boolean upgrading) {
if (addOn.getFile() != null && addOn.getFile().exists()) {
if (!addOn.getFile().delete() && !upgrading) {
logger.debug("Cant delete " + addOn.getFile().getAbsolutePath());
this.blockList.add(addOn.getId());
this.saveBlockList();
}
}
}
private void removeAddOnClassLoader(AddOn addOn) {
if (this.addOnLoaders.containsKey(addOn.getId())) {
try (AddOnClassLoader addOnClassLoader = this.addOnLoaders.remove(addOn.getId())) {
if (!addOn.getIdsAddOnDependencies().isEmpty()) {
addOnClassLoader.clearDependencies();
}
ResourceBundle.clearCache(addOnClassLoader);
} catch (Exception e) {
logger.error("Failure while closing class loader of " + addOn.getId() + " add-on:", e);
}
}
}
private void unloadDependentExtensions(AddOn ao) {
boolean changed = true;
for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) {
AddOn runningAddOn = aoc.getAddOn(entry.getKey());
for (Extension ext : runningAddOn.getLoadedExtensionsWithDeps()) {
if (runningAddOn.dependsOn(ext, ao)) {
String classname = ext.getClass().getCanonicalName();
AddOnInstaller.uninstallAddOnExtension(runningAddOn, ext, NULL_CALLBACK);
try (AddOnClassLoader extensionClassLoader = (AddOnClassLoader) ext.getClass().getClassLoader()) {
ext = null;
entry.getValue().removeChildClassLoader(extensionClassLoader);
extensionClassLoader.clearDependencies();
ResourceBundle.clearCache(extensionClassLoader);
} catch (Exception e) {
logger.error("Failure while closing class loader of extension '" + classname + "':", e);
}
runnableAddOns.get(runningAddOn).remove(classname);
changed = true;
}
}
}
if (changed) {
saveAddOnsRunState(runnableAddOns);
}
}
private void softUninstallDependentAddOns(AddOn ao) {
for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) {
AddOn runningAddOn = aoc.getAddOn(entry.getKey());
if (runningAddOn.dependsOn(ao)) {
softUninstallDependentAddOns(runningAddOn);
softUninstall(runningAddOn);
}
}
}
private void softUninstall(AddOn addOn) {
if (AddOn.InstallationStatus.INSTALLED != addOn.getInstallationStatus()) {
return;
}
AddOn.InstallationStatus status;
if (isDynamicallyInstallable(addOn) && AddOnInstaller.softUninstall(addOn, NULL_CALLBACK)) {
removeAddOnClassLoader(addOn);
status = AddOn.InstallationStatus.NOT_INSTALLED;
} else {
status = AddOn.InstallationStatus.SOFT_UNINSTALLATION_FAILED;
}
addOn.setInstallationStatus(status);
Control.getSingleton()
.getExtensionLoader()
.addOnSoftUninstalled(addOn, status == AddOn.InstallationStatus.NOT_INSTALLED);
}
private void loadBlockList() {
blockList = loadList(ADDONS_BLOCK_LIST);
}
private void saveBlockList() {
saveList(ADDONS_BLOCK_LIST, this.blockList);
}
private <T> List<ClassNameWrapper> getClassNames (String packageName, Class<T> classType) {
List<ClassNameWrapper> listClassName = new ArrayList<>();
listClassName.addAll(this.getLocalClassNames(packageName));
for (String addOnId : this.addOnLoaders.keySet()) {
listClassName.addAll(this.getJarClassNames(aoc.getAddOn(addOnId), packageName));
}
for (File jar : jars) {
listClassName.addAll(this.getJarClassNames(this.getClass().getClassLoader(), jar, packageName));
}
return listClassName;
}
/**
* Returns all the {@code Extension}s of all the installed add-ons.
* <p>
* The discovery of {@code Extension}s is done by resorting to the {@link AddOn#MANIFEST_FILE_NAME manifest file} bundled in
* the add-ons.
* <p>
* Extensions with unfulfilled dependencies are not be returned.
*
* @return a list containing all {@code Extension}s of all installed add-ons
* @since 2.4.0
* @see Extension
* @see #getExtensions(AddOn)
*/
public List<Extension> getExtensions () {
List<Extension> list = new ArrayList<Extension>();
for (AddOn addOn : getAddOnCollection().getAddOns()) {
list.addAll(getExtensions(addOn));
}
return list;
}
/**
* Returns all {@code Extension}s of the given {@code addOn}.
* <p>
* The discovery of {@code Extension}s is done by resorting to {@link AddOn#MANIFEST_FILE_NAME manifest file} bundled in the
* add-on.
* <p>
* Extensions with unfulfilled dependencies are not be returned.
* <p>
* <strong>Note:</strong> If the add-on is not installed the method returns an empty list.
*
* @param addOn the add-on whose extensions will be returned
* @return a list containing the {@code Extension}s of the given {@code addOn}
* @since 2.4.0
* @see Extension
* @see #getExtensions()
*/
public List<Extension> getExtensions(AddOn addOn) {
AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
if (addOnClassLoader == null) {
return Collections.emptyList();
}
List<Extension> extensions = new ArrayList<>();
extensions.addAll(loadAddOnExtensions(addOn, addOn.getExtensions(), addOnClassLoader));
if (addOn.hasExtensionsWithDeps()) {
AddOn.AddOnRunRequirements reqs = addOn.calculateRunRequirements(aoc.getInstalledAddOns());
for (ExtensionRunRequirements extReqs : reqs.getExtensionRequirements()) {
if (extReqs.isRunnable()) {
List<AddOnClassLoader> dependencies = new ArrayList<>(extReqs.getDependencies().size());
for (AddOn addOnDep : extReqs.getDependencies()) {
dependencies.add(addOnLoaders.get(addOnDep.getId()));
}
AddOnClassLoader extAddOnClassLoader = new AddOnClassLoader(
addOnClassLoader,
dependencies,
addOn.getExtensionAddOnClassnames(extReqs.getClassname()));
Extension ext = loadAddOnExtension(addOn, extReqs.getClassname(), extAddOnClassLoader);
if (ext != null) {
extensions.add(ext);
}
} else if (logger.isDebugEnabled()) {
logger.debug("Can't run extension '" + extReqs.getClassname() + "' of add-on '" + addOn.getName()
+ "' because of missing requirements: " + AddOnRunIssuesUtils.getRunningIssues(extReqs));
}
}
}
return extensions;
}
private List<Extension> loadAddOnExtensions(AddOn addOn, List<String> extensions, AddOnClassLoader addOnClassLoader) {
if (extensions == null || extensions.isEmpty()) {
return Collections.emptyList();
}
List<Extension> list = new ArrayList<>(extensions.size());
for (String extName : extensions) {
Extension ext = loadAddOnExtension(addOn, extName, addOnClassLoader);
if (ext != null) {
list.add(ext);
}
}
return list;
}
private static Extension loadAddOnExtension(AddOn addOn, String classname, AddOnClassLoader addOnClassLoader) {
Extension extension = AddOnLoaderUtils
.loadAndInstantiateClass(addOnClassLoader, classname, Extension.class, "extension");
if (extension != null) {
addOn.addLoadedExtension(extension);
}
return extension;
}
/**
* Gets the active scan rules of all the loaded add-ons.
* <p>
* The discovery of active scan rules is done by resorting to {@link AddOn#MANIFEST_FILE_NAME manifest file} bundled in the
* add-ons.
*
* @return an unmodifiable {@code List} with all the active scan rules, never {@code null}
* @since 2.4.0
* @see AbstractPlugin
*/
public List<AbstractPlugin> getActiveScanRules () {
ArrayList<AbstractPlugin> list = new ArrayList<>();
for (AddOn addOn : getAddOnCollection().getAddOns()) {
AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
if (addOnClassLoader != null) {
list.addAll(AddOnLoaderUtils.getActiveScanRules(addOn, addOnClassLoader));
}
}
list.trimToSize();
return Collections.unmodifiableList(list);
}
/**
* Gets the passive scan rules of all the loaded add-ons.
* <p>
* The discovery of passive scan rules is done by resorting to {@link AddOn#MANIFEST_FILE_NAME manifest file} bundled in the
* add-ons.
*
* @return an unmodifiable {@code List} with all the passive scan rules, never {@code null}
* @since 2.4.0
* @see PluginPassiveScanner
*/
public List<PluginPassiveScanner> getPassiveScanRules() {
ArrayList<PluginPassiveScanner> list = new ArrayList<>();
for (AddOn addOn : getAddOnCollection().getAddOns()) {
AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
if (addOnClassLoader != null) {
list.addAll(AddOnLoaderUtils.getPassiveScanRules(addOn, addOnClassLoader));
}
}
list.trimToSize();
return Collections.unmodifiableList(list);
}
public <T> List<T> getImplementors (String packageName, Class<T> classType) {
return this.getImplementors(null, packageName, classType);
}
public <T> List<T> getImplementors (AddOn ao, String packageName, Class<T> classType) {
Class<?> cls = null;
List<T> listClass = new ArrayList<>();
List<ClassNameWrapper> classNames;
if (ao != null) {
classNames = this.getJarClassNames(ao, packageName);
} else {
classNames = this.getClassNames(packageName, classType);
}
for (ClassNameWrapper classWrapper : classNames) {
try {
cls = classWrapper.getCl().loadClass(classWrapper.getClassName());
// abstract class or interface cannot be constructed.
if (Modifier.isAbstract(cls.getModifiers()) || Modifier.isInterface(cls.getModifiers())) {
continue;
}
if (classType.isAssignableFrom(cls)) {
@SuppressWarnings("unchecked")
Constructor<T> c = (Constructor<T>) cls.getConstructor();
listClass.add(c.newInstance());
}
} catch (Throwable e) {
// Often not an error
logger.debug(e.getMessage(), e);
}
}
return listClass;
}
/**
* Check local jar (zap.jar) or related package if any target file is found.
*
* @param packageName the package name that the class must belong too
* @return a {@code List} with all the classes belonging to the given package
*/
private List<ClassNameWrapper> getLocalClassNames (String packageName) {
if (packageName == null || packageName.equals("")) {
return Collections.emptyList();
}
String folder = packageName.replace('.', '/');
URL local = AddOnLoader.class.getClassLoader().getResource(folder);
if (local == null) {
return Collections.emptyList();
}
String jarFile = null;
if (local.getProtocol().equals("jar")) {
jarFile = local.toString().substring("jar:".length());
int pos = jarFile.indexOf("!");
jarFile = jarFile.substring(0, pos);
try {
// ZAP: Changed to take into account the package name
return getJarClassNames(this.getClass().getClassLoader(), new File(new URI(jarFile)), packageName);
} catch (URISyntaxException e) {
logger.error(e.getMessage(), e);
}
} else {
try {
// ZAP: Changed to pass a FileFilter (ClassRecurseDirFileFilter)
// and to pass the "packageName" with the dots already replaced.
return parseClassDir(this.getClass().getClassLoader(), new File(new URI(local.toString())),
packageName.replace('.', File.separatorChar),
new ClassRecurseDirFileFilter(true));
} catch (URISyntaxException e) {
logger.error(e.getMessage(), e);
}
}
return Collections.emptyList();
}
// ZAP: Changed to use only one FileFilter and the packageName is already
// passed with the dots replaced.
private List<ClassNameWrapper> parseClassDir(ClassLoader cl, File file, String packageName, FileFilter fileFilter) {
List<ClassNameWrapper> classNames = new ArrayList<> ();
File[] listFile = file.listFiles(fileFilter);
for (int i=0; i<listFile.length; i++) {
File entry = listFile[i];
if (entry.isDirectory()) {
classNames.addAll(parseClassDir (cl, entry, packageName, fileFilter));
continue;
}
String fileName = entry.toString();
int pos = fileName.indexOf(packageName);
if (pos > 0) {
String className = fileName.substring(pos).replaceAll("\\.class$","").replace(File.separatorChar, '.');
classNames.add(new ClassNameWrapper(cl, className));
}
}
return classNames;
}
// ZAP: Added to take into account the package name
private List<ClassNameWrapper> getJarClassNames(ClassLoader cl, File file, String packageName) {
List<ClassNameWrapper> classNames = new ArrayList<> ();
ZipEntry entry = null;
String className = "";
try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
entry = entries.nextElement();
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
continue;
}
className = entry.toString().replaceAll("\\.class$","").replaceAll("/",".");
if (className.indexOf(packageName) >= 0) {
classNames.add(new ClassNameWrapper(cl, className));
}
}
} catch (Exception e) {
logger.error("Failed to open file: " + file.getAbsolutePath(), e);
}
return classNames;
}
private List<ClassNameWrapper> getJarClassNames(AddOn ao, String packageName) {
List<ClassNameWrapper> classNames = new ArrayList<> ();
ZipEntry entry = null;
String className = "";
try (JarFile jarFile = new JarFile(ao.getFile())) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
entry = entries.nextElement();
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
continue;
}
className = entry.toString().replaceAll("\\.class$","").replaceAll("/",".");
if (className.indexOf(packageName) >= 0) {
classNames.add(new ClassNameWrapper(this.addOnLoaders.get(ao.getId()), className));
}
}
} catch (Exception e) {
logger.error("Failed to open file: " + ao.getFile().getAbsolutePath(), e);
}
return classNames;
}
private static final class JarFilenameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String fileName) {
if (fileName.endsWith(".jar")) {
return true;
}
return false;
}
}
// ZAP: Added
private static final class ClassRecurseDirFileFilter implements FileFilter {
private boolean recurse;
public ClassRecurseDirFileFilter(boolean recurse) {
this.recurse = recurse;
}
@Override
public boolean accept(File file) {
if (recurse && file.isDirectory() && ! file.getName().startsWith(".")) {
return true;
} else if (file.isFile() && file.getName().endsWith(".class")) {
return true;
}
return false;
}
}
private class ClassNameWrapper {
private ClassLoader cl;
private String className;
public ClassNameWrapper(ClassLoader cl, String className) {
super();
this.cl = cl;
this.className = className;
}
public ClassLoader getCl() {
return cl;
}
public String getClassName() {
return className;
}
}
private static List<String> loadList(String key) {
List<String> data = new ArrayList<>();
String blockStr = Model.getSingleton().getOptionsParam().getConfig().getString(key, null);
if (blockStr != null && blockStr.length() > 0) {
for (String str : blockStr.split(",")) {
data.add(str);
}
}
return data;
}
private static void saveList(String key, List<String> list) {
StringBuilder sb = new StringBuilder();
for (String id: list) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append(id);
}
Model.getSingleton().getOptionsParam().getConfig().setProperty(key, sb.toString());
try {
Model.getSingleton().getOptionsParam().getConfig().save();
} catch (ConfigurationException e) {
logger.error("Failed to save list [" + key + "]: " + sb.toString(), e);
}
}
private static Map<AddOn, List<String>> loadAddOnsRunState(AddOnCollection addOnCollection) {
List<HierarchicalConfiguration> savedAddOns = ((HierarchicalConfiguration) Model.getSingleton()
.getOptionsParam()
.getConfig()).configurationsAt(ADDONS_RUNNABLE_KEY);
Map<AddOn, List<String>> runnableAddOns = new HashMap<>();
for (HierarchicalConfiguration savedAddOn : savedAddOns) {
AddOn addOn = addOnCollection.getAddOn(savedAddOn.getString(ADDON_RUNNABLE_ID_KEY, ""));
if (addOn == null) {
// No longer exists, skip it.
continue;
}
int version = savedAddOn.getInt(ADDON_RUNNABLE_VERSION_KEY, -1);
if (version == -1 || addOn.getFileVersion() != version) {
// No version or not the same version, skip it.
continue;
}
List<String> runnableExtensions = new ArrayList<>();
List<String> currentExtensions = addOn.getExtensionsWithDeps();
for (String savedExtension : savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) {
if (currentExtensions.contains(savedExtension)) {
runnableExtensions.add(savedExtension);
}
}
runnableAddOns.put(addOn, runnableExtensions);
}
return runnableAddOns;
}
private static void saveAddOnsRunState(Map<AddOn, List<String>> runnableAddOns) {
HierarchicalConfiguration config = (HierarchicalConfiguration) Model.getSingleton().getOptionsParam().getConfig();
config.clearTree(ADDONS_RUNNABLE_BASE_KEY);
int i = 0;
for (Map.Entry<AddOn, List<String>> runnableAddOnEntry : runnableAddOns.entrySet()) {
String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ").";
AddOn addOn = runnableAddOnEntry.getKey();
config.setProperty(elementBaseKey + ADDON_RUNNABLE_ID_KEY, addOn.getId());
config.setProperty(elementBaseKey + ADDON_RUNNABLE_VERSION_KEY, Integer.valueOf(addOn.getFileVersion()));
String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY;
for (String extension : runnableAddOnEntry.getValue()) {
config.addProperty(extensionBaseKey, extension);
}
i++;
}
try {
Model.getSingleton().getOptionsParam().getConfig().save();
} catch (ConfigurationException e) {
logger.error("Failed to save state of runnable add-ons:", e);
}
}
/**
* An {@code UninstallationProgressCallback} that does nothing. A "{@code null}" object, for use when no callback is given
* during the uninstallation process.
*/
private static class NullUninstallationProgressCallBack implements AddOnUninstallationProgressCallback {
@Override
public void uninstallingAddOn(AddOn addOn, boolean updating) {
}
@Override
public void activeScanRulesWillBeRemoved(int numberOfRules) {
}
@Override
public void activeScanRuleRemoved(String name) {
}
@Override
public void passiveScanRulesWillBeRemoved(int numberOfRules) {
}
@Override
public void passiveScanRuleRemoved(String name) {
}
@Override
public void filesWillBeRemoved(int numberOfFiles) {
}
@Override
public void fileRemoved() {
}
@Override
public void extensionsWillBeRemoved(int numberOfExtensions) {
}
@Override
public void extensionRemoved(String name) {
}
@Override
public void addOnUninstalled(boolean uninstalled) {
}
}
}