org.zaproxy.zap.control.AddOnLoader Maven / Gradle / Ivy
Show all versions of zap Show documentation
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2012 The 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.io.IOException;
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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.apache.commons.collections.iterators.EnumerationIterator;
import org.apache.commons.collections.iterators.IteratorChain;
import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.FileConfiguration;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.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.Version;
import org.zaproxy.zap.control.AddOn.AddOnRunRequirements;
import org.zaproxy.zap.control.AddOn.ExtensionRunRequirements;
import org.zaproxy.zap.extension.AddOnInstallationStatusListener;
import org.zaproxy.zap.extension.AddOnInstallationStatusListener.StatusUpdate.Status;
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;
import org.zaproxy.zap.utils.ZapXmlConfiguration;
/**
* 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_FULL_VERSION_KEY = "fullversion";
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. */
static final AddOnUninstallationProgressCallback NULL_CALLBACK =
NullUninstallationProgressCallBack.getSingleton();
private static final Logger LOGGER = LogManager.getLogger(AddOnLoader.class);
static {
ClassLoader.registerAsParallelCapable();
}
private Lock installationLock = new ReentrantLock();
private AddOnCollection aoc = null;
private List 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 blockList = new ArrayList<>();
/**
* The runnable add-ons and its extensions.
*
* The key is the add-on itself and the value its runnable extensions.
*/
private Map> 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 idsAddOnsWithRunningIssuesSinceLastRun;
/*
* Using sub-classloaders means we can unload and reload addons
*/
private Map addOnLoaders = new HashMap<>();
/** File where the data of runnable state and blocked add-ons is saved. */
private ZapXmlConfiguration addOnsStateConfig;
private PostponedTasksRunner postponedTasks;
public AddOnLoader(File[] dirs) {
super(new URL[0], AddOnLoader.class.getClassLoader());
addOnsStateConfig = new ZapXmlConfiguration();
addOnsStateConfig.setRootElementName("addonsstate");
File configFile = new File(Constant.getZapHome(), "add-ons-state.xml");
addOnsStateConfig.setFile(configFile);
if (!migrateOldAddOnsState(addOnsStateConfig) && configFile.exists()) {
try {
addOnsStateConfig.load();
} catch (ConfigurationException e) {
LOGGER.warn("Failed to read add-ons' state file:", e);
}
}
this.loadBlockList();
this.aoc = new AddOnCollection(dirs);
postponedTasks = new PostponedTasksRunner(addOnsStateConfig, aoc);
postponedTasks.run();
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 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 getIdsAddOnsWithRunningIssuesSinceLastRun() {
return Collections.unmodifiableList(idsAddOnsWithRunningIssuesSinceLastRun);
}
private void loadAllAddOns() {
AddOnInstaller.deleteLegacyAddOnLibsDir(aoc.getAddOns());
for (Iterator iterator = aoc.getAddOns().iterator(); iterator.hasNext(); ) {
AddOn addOn = iterator.next();
if (canLoadAddOn(addOn)) {
AddOnInstaller.installMissingAddOnLibs(addOn);
} else {
iterator.remove();
}
}
runnableAddOns = new HashMap<>();
idsAddOnsWithRunningIssuesSinceLastRun = new ArrayList<>();
Map oldRunnableAddOns = loadAddOnsRunState(addOnsStateConfig, aoc);
List runAddons = new ArrayList<>();
Set updatedAddOns = new HashSet<>();
Set nonRunnableAddOns = new HashSet<>();
for (Iterator iterator = aoc.getAddOns().iterator(); iterator.hasNext(); ) {
AddOn addOn = iterator.next();
AddOnRunRequirements reqs = calculateRunRequirements(addOn, aoc.getAddOns());
if (reqs.isRunnable()) {
AddOnRunState runState = oldRunnableAddOns.get(addOn);
List runnableExtensions;
if (addOn.hasExtensionsWithDeps()) {
runnableExtensions = getRunnableExtensionsWithDeps(reqs);
List oldRunnableExtensions =
runState != null ? runState.getExtensions() : Collections.emptyList();
if (!oldRunnableExtensions.isEmpty()) {
oldRunnableExtensions.removeAll(runnableExtensions);
if (!oldRunnableExtensions.isEmpty()) {
idsAddOnsWithRunningIssuesSinceLastRun.add(addOn.getId());
}
}
} else {
runnableExtensions = Collections.emptyList();
}
runnableAddOns.put(addOn, runnableExtensions);
runAddons.add(addOn);
if (runState != null && runState.hasNewerVersion()) {
updatedAddOns.add(addOn);
}
} else {
nonRunnableAddOns.add(addOn);
}
}
nonRunnableAddOns.stream()
.filter(oldRunnableAddOns::containsKey)
.map(AddOn::getId)
.forEach(idsAddOnsWithRunningIssuesSinceLastRun::add);
saveAddOnsRunState(runnableAddOns);
for (AddOn addOn : runAddons) {
addOn.setInstallationStatus(AddOn.InstallationStatus.INSTALLED);
AddOnClassLoader addOnClassLoader = createAndAddAddOnClassLoader(addOn);
if (updatedAddOns.contains(addOn)) {
AddOnInstaller.updateAddOnFiles(addOnClassLoader, addOn);
}
AddOnInstaller.installResourceBundle(addOnClassLoader, addOn);
}
}
private static List getRunnableExtensionsWithDeps(
AddOnRunRequirements runRequirements) {
List 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())) {
LOGGER.debug(
"Can't load add-on {} it is on the block list (add-on uninstalled but the file couldn't be removed).",
ao.getName());
return false;
}
if (!ao.canLoadInCurrentVersion()) {
LOGGER.debug(
"Can't load add-on {} because of ZAP version constraints; Not before={} Not from={} Current Version={}",
ao.getName(),
ao.getNotBeforeVersion(),
ao.getNotFromVersion(),
Constant.PROGRAM_VERSION);
return false;
}
return true;
}
private static AddOnRunRequirements calculateRunRequirements(
AddOn ao, Collection availableAddOns) {
AddOnRunRequirements reqs = ao.calculateRunRequirements(availableAddOns);
if (!reqs.isRunnable()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"Can't run add-on {} because of missing requirements: {}",
ao.getId(),
AddOnRunIssuesUtils.getRunningIssues(reqs));
}
}
return reqs;
}
private AddOnClassLoader createAndAddAddOnClassLoader(AddOn ao) {
try {
AddOnClassLoader addOnClassLoader = addOnLoaders.get(ao.getId());
if (addOnClassLoader != null) {
return addOnClassLoader;
}
List idsAddOnDependencies = ao.getIdsAddOnDependencies();
if (idsAddOnDependencies.isEmpty()) {
addOnClassLoader =
new AddOnClassLoader(
ao.getFile().toURI().toURL(), this, ao.getAddOnClassnames());
putAddOnClassLoader(ao, addOnClassLoader);
return addOnClassLoader;
}
List 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());
putAddOnClassLoader(ao, addOnClassLoader);
return addOnClassLoader;
} catch (MalformedURLException e) {
LOGGER.error(e.getMessage(), e);
throw new RuntimeException(
"Failed to convert URL for AddOnClassLoader " + ao.getFile().toURI(), e);
}
}
/**
* Puts the given add-on class loader into the {@link #addOnLoaders} map and {@link
* AddOn#setClassLoader(ClassLoader) sets it into the add-on}.
*
* The add-on libraries are added to the add-on class loader before that.
*
* @param ao the add-on to put in the map.
* @param addOnClassLoader the class loader of the add-on.
*/
private void putAddOnClassLoader(AddOn ao, AddOnClassLoader addOnClassLoader) {
if (!ao.getLibs().isEmpty()) {
addOnClassLoader.addUrls(
ao.getLibs().stream()
.map(AddOn.Lib::getFileSystemUrl)
.collect(Collectors.toList()));
}
ao.setClassLoader(addOnClassLoader);
addOnLoaders.put(ao.getId(), addOnClassLoader);
}
Class> loadClassNoAddOns(String name, boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
@Override
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
try {
return super.loadClass(name, resolve);
} catch (ClassNotFoundException e) {
// Continue for now
}
for (AddOnClassLoader loader : addOnLoaders.values()) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException e) {
// Continue for now
}
for (AddOnClassLoader childLoader : loader.getChildClassLoaders()) {
try {
return childLoader.loadClass(name);
} catch (ClassNotFoundException e) {
// Continue for now
}
}
}
throw new ClassNotFoundException(name);
}
}
@Override
protected Object getClassLoadingLock(String className) {
// Allow AddOnClassLoader to use the same locks.
return super.getClassLoadingLock(className);
}
@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;
}
@Override
public Enumeration getResources(String name) throws IOException {
IteratorChain urls = new IteratorChain();
urls.addIterator(new EnumerationIterator(super.getResources(name)));
for (AddOnClassLoader loader : addOnLoaders.values()) {
urls.addIterator(new EnumerationIterator(loader.getResources(name)));
}
@SuppressWarnings("unchecked")
Enumeration result = new IteratorEnumeration(urls);
return result;
}
public AddOnCollection getAddOnCollection() {
return this.aoc;
}
private void addDirectory(File dir) {
if (dir == null) {
LOGGER.error("Null directory supplied");
return;
}
if (!dir.exists()) {
LOGGER.debug("No such directory: {}", dir.getAbsolutePath());
return;
}
if (!dir.isDirectory()) {
LOGGER.warn("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 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);
}
installationLock.lock();
try {
if (!this.aoc.addAddOn(ao)) {
return;
}
addAddOnImpl(ao);
} finally {
installationLock.unlock();
}
}
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;
}
if (!AddOnInstaller.installAddOnLibs(ao)) {
ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED);
return;
}
AddOnRunRequirements reqs = calculateRunRequirements(ao, aoc.getInstalledAddOns());
if (!reqs.isRunnable()) {
ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED);
return;
}
Control.getSingleton()
.getExtensionLoader()
.addOnStatusUpdate(new AddOnStatusUpdate(ao, Status.INSTALL, true));
AddOnInstaller.install(createAndAddAddOnClassLoader(ao), ao);
ao.setInstallationStatus(AddOn.InstallationStatus.INSTALLED);
Control.getSingleton()
.getExtensionLoader()
.addOnStatusUpdate(new AddOnStatusUpdate(ao, Status.INSTALLED, true));
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.
*
* 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 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.
*
* 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 entry : new HashMap<>(addOnLoaders).entrySet()) {
AddOn runningAddOn = aoc.getAddOn(entry.getKey());
if (runningAddOn.getInstallationStatus()
== AddOn.InstallationStatus.UNINSTALLATION_FAILED) {
continue;
}
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 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);
if (ext != null) {
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.
*
* 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 boolean removeAddOn(
AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback progressCallback) {
installationLock.lock();
try {
AddOnUninstallationProgressCallback callback =
(progressCallback == null) ? NULL_CALLBACK : progressCallback;
callback.uninstallingAddOn(ao, upgrading);
boolean removed = removeAddOnImpl(ao, upgrading, callback);
callback.addOnUninstalled(removed);
return removed;
} finally {
installationLock.unlock();
}
}
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, runnableAddOns.keySet(), postponedTasks);
removeAddOnClassLoader(ao);
deleteAddOn(ao, upgrading);
ao.setInstallationStatus(AddOn.InstallationStatus.UNINSTALLATION_FAILED);
Control.getSingleton()
.getExtensionLoader()
.addOnStatusUpdate(new AddOnStatusUpdate(ao, Status.UNINSTALLED, 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);
}
deleteAddOn(ao, upgrading);
return this.aoc.removeAddOn(ao);
}
if (!canUnloadAllExtensions(ao)) {
LOGGER.debug("Can't dynamically unload all the extensions of: {}", ao);
ao.setInstallationStatus(AddOn.InstallationStatus.UNINSTALLATION_FAILED);
postponedTasks.addUninstallAddOnTask(ao);
return false;
}
Control.getSingleton()
.getExtensionLoader()
.addOnStatusUpdate(new AddOnStatusUpdate(ao, Status.UNINSTALL, true));
unloadDependentExtensions(ao);
softUninstallDependentAddOns(ao);
boolean uninstalledWithoutErrors =
AddOnInstaller.uninstall(ao, callback, runnableAddOns.keySet(), postponedTasks);
if (uninstalledWithoutErrors && !this.aoc.removeAddOn(ao)) {
uninstalledWithoutErrors = false;
}
if (uninstalledWithoutErrors) {
removeAddOnClassLoader(ao);
}
deleteAddOn(ao, upgrading);
if (runnableAddOns.remove(ao) != null) {
saveAddOnsRunState(runnableAddOns);
}
ao.setInstallationStatus(
uninstalledWithoutErrors
? AddOn.InstallationStatus.AVAILABLE
: AddOn.InstallationStatus.UNINSTALLATION_FAILED);
Control.getSingleton()
.getExtensionLoader()
.addOnStatusUpdate(
new AddOnStatusUpdate(ao, Status.UNINSTALLED, uninstalledWithoutErrors));
return uninstalledWithoutErrors;
}
private static boolean canUnloadAllExtensions(AddOn ao) {
for (Extension e : ao.getLoadedExtensions()) {
if (e.isEnabled() && !e.canUnload()) {
return false;
}
}
return true;
}
/**
* Deletes the file and libraries of the given add-on.
*
*
The add-on is added to the {@link #blockList block list} when not able to delete it and if
* not updating it.
*
* @param addOn the add-on to be deleted.
* @param upgrading {@code true} if the add-on is being updated, {@code false} otherwise.
* @see AddOnInstaller#uninstallAddOnLibs(AddOn)
*/
private void deleteAddOn(AddOn addOn, boolean upgrading) {
AddOnInstaller.uninstallAddOnLibs(addOn);
File addOnFile = addOn.getFile();
if (addOnFile == null || !addOnFile.exists()) {
return;
}
if (isInstallationAddOn(addOnFile.toPath())) {
LOGGER.debug(
"Not removing add-on in the installation directory: {}",
addOnFile.getAbsolutePath());
if (!upgrading) {
saveBlockedAddOn(addOn);
}
return;
}
if (addOn.getFile() != null && addOn.getFile().exists()) {
if (!addOn.getFile().delete() && !upgrading) {
LOGGER.debug("Can't delete {}", addOn.getFile().getAbsolutePath());
saveBlockedAddOn(addOn);
}
}
}
private void saveBlockedAddOn(AddOn addOn) {
if (addOn.isMandatory()) {
return;
}
blockList.add(addOn.getId());
saveBlockList();
}
List getBlockList() {
return blockList;
}
private static boolean isInstallationAddOn(Path file) {
Path installDir = Paths.get(Constant.getZapInstall()).resolve(Constant.FOLDER_PLUGIN);
if (Files.notExists(installDir)) {
return false;
}
try {
return Files.isSameFile(installDir, file.getParent());
} catch (IOException e) {
LOGGER.warn("An error occurred while checking the add-on's dir:", e);
return false;
}
}
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 {} add-on:", addOn.getId(), e);
}
addOn.setClassLoader(null);
}
}
private void unloadDependentExtensions(AddOn ao) {
boolean changed = false;
for (Entry 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 entry : new HashMap<>(addOnLoaders).entrySet()) {
AddOn runningAddOn = aoc.getAddOn(entry.getKey());
if (runningAddOn.dependsOn(ao)) {
softUninstallDependentAddOns(runningAddOn);
unloadDependentExtensions(runningAddOn);
softUninstall(runningAddOn);
}
}
}
private void softUninstall(AddOn addOn) {
if (AddOn.InstallationStatus.INSTALLED != addOn.getInstallationStatus()) {
return;
}
Control.getSingleton()
.getExtensionLoader()
.addOnStatusUpdate(new AddOnStatusUpdate(addOn, Status.SOFT_UNINSTALL, true));
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()
.addOnStatusUpdate(
new AddOnStatusUpdate(
addOn,
Status.SOFT_UNINSTALLED,
status == AddOn.InstallationStatus.NOT_INSTALLED));
}
private void loadBlockList() {
blockList = loadList(addOnsStateConfig, ADDONS_BLOCK_LIST);
}
private void saveBlockList() {
saveList(addOnsStateConfig, ADDONS_BLOCK_LIST, this.blockList);
}
private List getClassNames(String packageName, Class classType) {
List 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.
*
* The discovery of {@code Extension}s is done by resorting to the {@link
* AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-ons.
*
*
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 getExtensions() {
List list = new ArrayList<>();
for (AddOn addOn : getAddOnCollection().getAddOns()) {
list.addAll(getExtensions(addOn));
}
return list;
}
/**
* Returns all {@code Extension}s of the given {@code addOn}.
*
* The discovery of {@code Extension}s is done by resorting to {@link
* AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-on.
*
*
Extensions with unfulfilled dependencies are not be returned.
*
*
Note: 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 getExtensions(AddOn addOn) {
AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
if (addOnClassLoader == null) {
return Collections.emptyList();
}
List 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 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 '{}' of add-on '{}' because of missing requirements: {}",
extReqs.getClassname(),
addOn.getId(),
AddOnRunIssuesUtils.getRunningIssues(extReqs));
}
}
}
return extensions;
}
private List loadAddOnExtensions(
AddOn addOn, List extensions, AddOnClassLoader addOnClassLoader) {
if (extensions == null || extensions.isEmpty()) {
return Collections.emptyList();
}
List 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.
*
* 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 getActiveScanRules() {
ArrayList 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();
validateNames(list);
return Collections.unmodifiableList(list);
}
private static void validateNames(List scanRules) {
scanRules.forEach(
rule -> {
if (StringUtils.isBlank(rule.getName())) {
LOGGER.log(
Constant.isDevBuild() ? Level.ERROR : Level.WARN,
"Scan rule {} does not have a name.",
rule.getClass().getCanonicalName());
}
});
}
/**
* Gets the passive scan rules of all the loaded add-ons.
*
* 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
* @deprecated (2.15.0) The scan rules are loaded by the corresponding extension.
*/
@Deprecated(since = "2.15.0", forRemoval = true)
public List getPassiveScanRules() {
return List.of();
}
/**
* Gets a list of classes that implement the given type in the given package.
*
* It searches in the dependencies, add-ons, and the ZAP JAR.
*
* @param packageName the name of the package that the classes must be in.
* @param classType the type of the classes.
* @return a list with the classes that implement the given type, never {@code null}.
* @deprecated (2.8.0) The use of this method is discouraged (specially during ZAP startup, as
* it's delayed), it's preferable to provide means to register/declare the required classes
* instead of searching "everywhere".
*/
@Deprecated
public List getImplementors(String packageName, Class classType) {
return this.getImplementors(null, packageName, classType);
}
/**
* Gets a list of classes that implement the given type in the given package.
*
* It searches in the given add-on, if not {@code null}, otherwise it searches in the
* dependencies, add-ons, and the ZAP JAR.
*
* @param ao the add-on to search in, might be {@code null}.
* @param packageName the name of the package that the classes must be in.
* @param classType the type of the classes.
* @return a list with the classes that implement the given type, never {@code null}.
* @deprecated (2.8.0) The use of this method is discouraged (specially during ZAP startup, as
* it's delayed), it's preferable to provide means to register/declare the required classes
* instead of searching "everywhere".
*/
@Deprecated
public List getImplementors(AddOn ao, String packageName, Class classType) {
Class> cls = null;
List listClass = new ArrayList<>();
List 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 c = (Constructor) 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 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 parseClassDir(
ClassLoader cl, File file, String packageName, FileFilter fileFilter) {
List classNames = new ArrayList<>();
File[] listFile = file.listFiles(fileFilter);
for (File entry : listFile) {
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 getJarClassNames(ClassLoader cl, File file, String packageName) {
List classNames = new ArrayList<>();
ZipEntry entry = null;
String className = "";
try (JarFile jarFile = new JarFile(file)) {
Enumeration 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 getJarClassNames(AddOn ao, String packageName) {
List classNames = new ArrayList<>();
ZipEntry entry = null;
String className = "";
try (JarFile jarFile = new JarFile(ao.getFile())) {
Enumeration 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 loadList(Configuration config, String key) {
List data = new ArrayList<>();
String blockStr = config.getString(key, null);
if (blockStr != null && blockStr.length() > 0) {
for (String str : blockStr.split(",")) {
data.add(str);
}
}
return data;
}
private static void saveList(FileConfiguration config, String key, List list) {
StringBuilder sb = new StringBuilder();
for (String id : list) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append(id);
}
config.setProperty(key, sb.toString());
try {
config.save();
} catch (ConfigurationException e) {
LOGGER.error("Failed to save list [{}]: {}", key, sb, e);
}
}
private static Map loadAddOnsRunState(
HierarchicalConfiguration config, AddOnCollection addOnCollection) {
List savedAddOns = config.configurationsAt(ADDONS_RUNNABLE_KEY);
Map 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;
}
String version = savedAddOn.getString(ADDON_RUNNABLE_FULL_VERSION_KEY, "");
if (version.isEmpty()) {
// Try read the old version, which was an integer.
version = savedAddOn.getString(ADDON_RUNNABLE_VERSION_KEY, "");
}
if (version.isEmpty()) {
// No version, skip it.
continue;
}
int result =
addOn.getVersion().compareTo(createLegacyVersion(version, addOn.getName()));
if (result != 0) {
if (result > 1) {
runnableAddOns.put(addOn, new AddOnRunState());
}
// Different version, nothing more to do.
continue;
}
List runnableExtensions = new ArrayList<>();
List currentExtensions = addOn.getExtensionsWithDeps();
for (String savedExtension :
savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) {
if (currentExtensions.contains(savedExtension)) {
runnableExtensions.add(savedExtension);
}
}
runnableAddOns.put(addOn, new AddOnRunState(runnableExtensions));
}
return runnableAddOns;
}
private static Version createLegacyVersion(String version, String addOnName) {
try {
return new Version(version);
} catch (IllegalArgumentException e) {
LOGGER.debug(
"Failed to create (legacy?) version with [{}] for runnable add-on [{}]",
version,
addOnName,
e);
}
try {
return new Version(version + ".0.0");
} catch (IllegalArgumentException e) {
LOGGER.debug(
"Failed to create legacy version with [{}.0.0] for runnable add-on [{}]",
version,
addOnName,
e);
}
return null;
}
private void saveAddOnsRunState(Map> runnableAddOns) {
addOnsStateConfig.clearTree(ADDONS_RUNNABLE_BASE_KEY);
int i = 0;
for (Map.Entry> runnableAddOnEntry : runnableAddOns.entrySet()) {
String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ").";
AddOn addOn = runnableAddOnEntry.getKey();
addOnsStateConfig.setProperty(elementBaseKey + ADDON_RUNNABLE_ID_KEY, addOn.getId());
addOnsStateConfig.setProperty(
elementBaseKey + ADDON_RUNNABLE_FULL_VERSION_KEY, addOn.getVersion());
// For older ZAP versions, which can't read the semantic version, just an integer.
addOnsStateConfig.setProperty(
elementBaseKey + ADDON_RUNNABLE_VERSION_KEY,
addOn.getVersion().getMajorVersion());
String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY;
for (String extension : runnableAddOnEntry.getValue()) {
addOnsStateConfig.addProperty(extensionBaseKey, extension);
}
i++;
}
try {
addOnsStateConfig.save();
} catch (ConfigurationException e) {
LOGGER.error("Failed to save state of runnable add-ons:", e);
}
}
private static boolean migrateOldAddOnsState(ZapXmlConfiguration newConfig) {
boolean dataMigrated = false;
HierarchicalConfiguration oldConfig =
(HierarchicalConfiguration) Model.getSingleton().getOptionsParam().getConfig();
if (oldConfig.containsKey(ADDONS_BLOCK_LIST)) {
List blockList = loadList(oldConfig, ADDONS_BLOCK_LIST);
oldConfig.clearProperty(ADDONS_BLOCK_LIST);
saveList(newConfig, ADDONS_BLOCK_LIST, blockList);
dataMigrated = true;
}
List oldAddOnsState =
oldConfig.configurationsAt(ADDONS_RUNNABLE_KEY);
if (!oldAddOnsState.isEmpty()) {
int i = 0;
for (HierarchicalConfiguration savedAddOn : oldAddOnsState) {
String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ").";
newConfig.setProperty(
elementBaseKey + ADDON_RUNNABLE_ID_KEY,
savedAddOn.getString(ADDON_RUNNABLE_ID_KEY, ""));
String version = savedAddOn.getString(ADDON_RUNNABLE_FULL_VERSION_KEY, "");
if (version.isEmpty()) {
newConfig.setProperty(
elementBaseKey + ADDON_RUNNABLE_VERSION_KEY,
savedAddOn.getString(ADDON_RUNNABLE_VERSION_KEY, ""));
} else {
newConfig.setProperty(
elementBaseKey + ADDON_RUNNABLE_FULL_VERSION_KEY, version);
}
String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY;
for (String extension :
savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) {
newConfig.addProperty(extensionBaseKey, extension);
}
}
oldConfig.clearTree(ADDONS_RUNNABLE_KEY);
dataMigrated = true;
}
return dataMigrated;
}
private static class AddOnRunState {
private final boolean newerVersion;
private final List extensions;
public AddOnRunState() {
this.newerVersion = true;
this.extensions = Collections.emptyList();
}
public AddOnRunState(List extensions) {
this.newerVersion = false;
this.extensions = extensions;
}
public boolean hasNewerVersion() {
return newerVersion;
}
public List getExtensions() {
return extensions;
}
}
private static class AddOnStatusUpdate implements AddOnInstallationStatusListener.StatusUpdate {
private final AddOn addOn;
private final Status status;
private final boolean successful;
private AddOnStatusUpdate(AddOn addOn, Status status, boolean successful) {
this.addOn = addOn;
this.status = status;
this.successful = successful;
}
@Override
public boolean isSuccessful() {
return successful;
}
@Override
public Status getStatus() {
return status;
}
@Override
public AddOn getAddOn() {
return addOn;
}
}
}