/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2014 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.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang.Validate;
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.core.scanner.PluginFactory;
import org.parosproxy.paros.extension.Extension;
import org.parosproxy.paros.extension.ExtensionLoader;
import org.parosproxy.paros.model.Model;
import org.zaproxy.zap.extension.pscan.ExtensionPassiveScan;
import org.zaproxy.zap.extension.pscan.PassiveScanner;
import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;
/**
* Helper class responsible to install and uninstall add-ons and all its (dynamically installable) components
* ({@code Extension}s, {@code Plugin}s, {@code PassiveScanner}s and files).
*
* @see Extension
* @see org.parosproxy.paros.core.scanner.Plugin
* @see PassiveScanner
* @since 2.3.0
*/
public final class AddOnInstaller {
private static final Logger logger = Logger.getLogger(AddOnInstaller.class);
private AddOnInstaller() {
}
/**
* Installs all the (dynamically installable) components ({@code Extension}s, {@code Plugin}s, {@code PassiveScanner}s and
* files) of the given {@code addOn}.
* <p>
* It's also responsible to notify the installed extensions when the installation has finished by calling the method
* {@code Extension#postInstall()}.
* <p>
* The components are installed in the following order:
* <ol>
* <li>Files;</li>
* <li>Extensions;</li>
* <li>Active scanners;</li>
* <li>Passive scanners.</li>
* </ol>
* The files are installed first as they might be required by extensions and scanners.
*
* @param addOnClassLoader the class loader of the given {@code addOn}
* @param addOn the add-on that will be installed
* @see Extension
* @see PassiveScanner
* @see org.parosproxy.paros.core.scanner.Plugin
* @see Extension#postInstall()
*/
public static void install(AddOnClassLoader addOnClassLoader, AddOn addOn) {
installAddOnFiles(addOnClassLoader, addOn, true);
List<Extension> listExts = installAddOnExtensions(addOn);
installAddOnActiveScanRules(addOn, addOnClassLoader);
installAddOnPassiveScanRules(addOn, addOnClassLoader);
// postInstall actions
for (Extension ext : listExts) {
try {
ext.postInstall();
} catch (Exception e) {
logger.error("Post install method failed for add-on " + addOn.getId() + " extension " + ext.getName());
}
}
}
/**
* Uninstalls all the (dynamically installable) components ({@code Extension}s, {@code Plugin}s, {@code PassiveScanner}s and
* files) of the given {@code addOn}.
* <p>
* The components are uninstalled in the following order (inverse to installation):
* <ol>
* <li>Passive scanners;</li>
* <li>Active scanners;</li>
* <li>Extensions;</li>
* <li>Files;</li>
* </ol>
*
* @param addOn the add-on that will be uninstalled
* @param callback the callback that will be notified of the progress of the uninstallation
* @return {@code true} if the add-on was uninstalled without errors, {@code false} otherwise.
* @throws IllegalArgumentException if {@code addOn} or {@code callback} are null.
* @see #softUninstall(AddOn, AddOnUninstallationProgressCallback)
* @see Extension
* @see PassiveScanner
* @see org.parosproxy.paros.core.scanner.Plugin
*/
public static boolean uninstall(AddOn addOn, AddOnUninstallationProgressCallback callback) {
Validate.notNull(addOn, "Parameter addOn must not be null.");
validateCallbackNotNull(callback);
try {
boolean uninstalledWithoutErrors = true;
uninstalledWithoutErrors &= uninstallAddOnPassiveScanRules(addOn, callback);
uninstalledWithoutErrors &= uninstallAddOnActiveScanRules(addOn, callback);
uninstalledWithoutErrors &= uninstallAddOnExtensions(addOn, callback);
uninstalledWithoutErrors &= uninstallAddOnFiles(addOn, callback);
return uninstalledWithoutErrors;
} catch (Throwable e) {
logger.error("An error occurred while uninstalling the add-on: " + addOn.getId(), e);
return false;
}
}
/**
* Uninstalls Java classes ({@code Extension}s, {@code Plugin}s, {@code PassiveScanner}s) of the given {@code addOn}. Should
* be called when the add-on must be temporarily uninstalled for an update of a dependency.
* <p>
* The Java classes are uninstalled in the following order (inverse to installation):
* <ol>
* <li>Passive scanners;</li>
* <li>Active scanners;</li>
* <li>Extensions.</li>
* </ol>
*
* @param addOn the add-on that will be softly uninstalled
* @param callback the callback that will be notified of the progress of the uninstallation
* @return {@code true} if the add-on was uninstalled without errors, {@code false} otherwise.
* @since 2.4.0
* @see Extension
* @see PassiveScanner
* @see org.parosproxy.paros.core.scanner.Plugin
*/
public static boolean softUninstall(AddOn addOn, AddOnUninstallationProgressCallback callback) {
Validate.notNull(addOn, "Parameter addOn must not be null.");
validateCallbackNotNull(callback);
try {
boolean uninstalledWithoutErrors = true;
uninstalledWithoutErrors &= uninstallAddOnPassiveScanRules(addOn, callback);
uninstalledWithoutErrors &= uninstallAddOnActiveScanRules(addOn, callback);
uninstalledWithoutErrors &= uninstallAddOnExtensions(addOn, callback);
return uninstalledWithoutErrors;
} catch (Throwable e) {
logger.error("An error occurred while uninstalling the add-on: " + addOn.getId(), e);
return false;
}
}
private static List<Extension> installAddOnExtensions(AddOn addOn) {
ExtensionLoader extensionLoader = Control.getSingleton().getExtensionLoader();
List<Extension> listExts = ExtensionFactory.loadAddOnExtensions(extensionLoader, Model.getSingleton()
.getOptionsParam()
.getConfig(), addOn);
for (Extension ext : listExts) {
installAddOnExtensionImpl(addOn, ext, extensionLoader);
}
return listExts;
}
public static void installAddOnExtension(AddOn addOn, Extension ext) {
ExtensionLoader extensionLoader = Control.getSingleton().getExtensionLoader();
ExtensionFactory.addAddOnExtension(extensionLoader, Model.getSingleton()
.getOptionsParam()
.getConfig(), ext);
installAddOnExtensionImpl(addOn, ext, extensionLoader);
}
private static void installAddOnExtensionImpl(AddOn addOn, Extension ext, ExtensionLoader extensionLoader) {
if (ext.isEnabled()) {
logger.debug("Starting extension " + ext.getName());
try {
extensionLoader.startLifeCycle(ext);
} catch (Exception e) {
logger.error("An error occurred while installing the add-on: " + addOn.getId(), e);
}
}
}
private static boolean uninstallAddOnExtensions(AddOn addOn, AddOnUninstallationProgressCallback callback) {
boolean uninstalledWithoutErrors = true;
callback.extensionsWillBeRemoved(addOn.getLoadedExtensions().size());
List<Extension> extensions = new ArrayList<>(addOn.getLoadedExtensions());
Collections.reverse(extensions);
for (Extension ext : extensions) {
uninstalledWithoutErrors &= uninstallAddOnExtension(addOn, ext, callback);
}
return uninstalledWithoutErrors;
}
/**
* Uninstalls the given extension.
*
* @param addOn the add-on that has the extension
* @param extension the extension that should be uninstalled
* @param callback the callback that will be notified of the progress of the uninstallation
* @return {@code true} if the extension was uninstalled without errors, {@code false} otherwise.
* @since 2.4.0
* @see Extension
*/
protected static boolean uninstallAddOnExtension(
AddOn addOn,
Extension extension,
AddOnUninstallationProgressCallback callback) {
boolean uninstalledWithoutErrors = true;
if (extension.isEnabled()) {
String extUiName = extension.getUIName();
if (extension.canUnload()) {
logger.debug("Unloading ext: " + extension.getName());
try {
extension.unload();
Control.getSingleton().getExtensionLoader().removeExtension(extension, extension.getExtensionHook());
ExtensionFactory.unloadAddOnExtension(extension);
} catch (Exception e) {
logger.error("An error occurred while uninstalling the extension \"" + extension.getName()
+ "\" bundled in the add-on \"" + addOn.getId() + "\":", e);
uninstalledWithoutErrors = false;
}
} else {
logger.debug("Cant dynamically unload ext: " + extension.getName());
uninstalledWithoutErrors = false;
}
callback.extensionRemoved(extUiName);
}
addOn.removeLoadedExtension(extension);
return uninstalledWithoutErrors;
}
private static void installAddOnActiveScanRules(AddOn addOn, AddOnClassLoader addOnClassLoader) {
List<AbstractPlugin> ascanrules = AddOnLoaderUtils.getActiveScanRules(addOn, addOnClassLoader);
if (!ascanrules.isEmpty()) {
for (AbstractPlugin ascanrule : ascanrules) {
String name = ascanrule.getClass().getCanonicalName();
logger.debug("Install ascanrule: " + name);
PluginFactory.loadedPlugin(ascanrule);
if (!PluginFactory.isPluginLoaded(ascanrule)) {
logger.error("Failed to install ascanrule: " + name);
}
}
}
}
private static boolean uninstallAddOnActiveScanRules(AddOn addOn, AddOnUninstallationProgressCallback callback) {
boolean uninstalledWithoutErrors = true;
List<AbstractPlugin> loadedAscanrules = addOn.getLoadedAscanrules();
if (!loadedAscanrules.isEmpty()) {
logger.debug("Uninstall ascanrules: " + addOn.getAscanrules());
callback.activeScanRulesWillBeRemoved(loadedAscanrules.size());
for (AbstractPlugin ascanrule : loadedAscanrules) {
String name = ascanrule.getClass().getCanonicalName();
logger.debug("Uninstall ascanrule: " + name);
PluginFactory.unloadedPlugin(ascanrule);
if (PluginFactory.isPluginLoaded(ascanrule)) {
logger.error("Failed to uninstall ascanrule: " + name);
uninstalledWithoutErrors = false;
}
callback.activeScanRuleRemoved(name);
}
addOn.setLoadedAscanrules(Collections.<AbstractPlugin>emptyList());
addOn.setLoadedAscanrulesSet(false);
}
return uninstalledWithoutErrors;
}
private static void installAddOnPassiveScanRules(AddOn addOn, AddOnClassLoader addOnClassLoader) {
List<PluginPassiveScanner> pscanrules = AddOnLoaderUtils.getPassiveScanRules(addOn, addOnClassLoader);
ExtensionPassiveScan extPscan = (ExtensionPassiveScan) Control.getSingleton()
.getExtensionLoader()
.getExtension(ExtensionPassiveScan.NAME);
if (!pscanrules.isEmpty() && extPscan != null) {
for (PluginPassiveScanner pscanrule : pscanrules) {
String name = pscanrule.getClass().getCanonicalName();
logger.debug("Install pscanrule: " + name);
if (!extPscan.addPassiveScanner(pscanrule)) {
logger.error("Failed to install pscanrule: " + name);
}
}
}
}
private static boolean uninstallAddOnPassiveScanRules(AddOn addOn, AddOnUninstallationProgressCallback callback) {
boolean uninstalledWithoutErrors = true;
List<PluginPassiveScanner> loadedPscanrules = addOn.getLoadedPscanrules();
ExtensionPassiveScan extPscan = (ExtensionPassiveScan) Control.getSingleton()
.getExtensionLoader()
.getExtension(ExtensionPassiveScan.NAME);
if (!loadedPscanrules.isEmpty()) {
logger.debug("Uninstall pscanrules: " + addOn.getPscanrules());
callback.passiveScanRulesWillBeRemoved(loadedPscanrules.size());
for (PluginPassiveScanner pscanrule : loadedPscanrules) {
String name = pscanrule.getClass().getCanonicalName();
logger.debug("Uninstall pscanrule: " + name);
if (!extPscan.removePassiveScanner(pscanrule)) {
logger.error("Failed to uninstall pscanrule: " + name);
uninstalledWithoutErrors = false;
}
callback.passiveScanRuleRemoved(name);
}
addOn.setLoadedPscanrules(Collections.<PluginPassiveScanner>emptyList());
addOn.setLoadedPscanrulesSet(false);
}
return uninstalledWithoutErrors;
}
/**
* Installs all the missing files declared by the given {@code addOn}.
*
* @param addOnClassLoader the class loader of the given {@code addOn}
* @param addOn the add-on that will have the missing declared files installed
*/
public static void installMissingAddOnFiles(AddOnClassLoader addOnClassLoader, AddOn addOn) {
installAddOnFiles(addOnClassLoader, addOn, false);
}
private static void installAddOnFiles(AddOnClassLoader addOnClassLoader, AddOn addOn, boolean overwrite) {
List<String> fileNames = addOn.getFiles();
if (fileNames == null || fileNames.isEmpty()) {
return;
}
for (String name : fileNames) {
File outfile = new File(Constant.getZapHome(), name);
if (!overwrite && outfile.exists()) {
// logger.debug("Ignoring, file already exists.");
continue;
}
if (!outfile.getParentFile().exists() && !outfile.getParentFile().mkdirs()) {
logger.error("Failed to create directories for: " + outfile.getAbsolutePath());
continue;
}
logger.debug("Installing file: " + name);
URL fileURL = addOnClassLoader.findResource(name);
if (fileURL == null) {
logger.error("File not found on add-on package: " + name);
continue;
}
try (InputStream in = fileURL.openStream(); OutputStream out = new FileOutputStream(outfile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
logger.error("Failed to install file " + outfile.getAbsolutePath(), e);
}
}
Control.getSingleton().getExtensionLoader().addonFilesAdded();
}
private static void validateCallbackNotNull(AddOnUninstallationProgressCallback callback) {
Validate.notNull(callback, "Parameter callback must not be null.");
}
/**
* Uninstalls the files of the given add-on.
*
* @param addOn the add-on
* @param callback the callback for notification of progress
* @return {@code true} if not error occurred while remove the files, {@code false} otherwise.
* @throws IllegalArgumentException if {@code addOn} or {@code callback} are null.
*/
public static boolean uninstallAddOnFiles(AddOn addOn, AddOnUninstallationProgressCallback callback) {
Validate.notNull(addOn, "Parameter addOn must not be null.");
validateCallbackNotNull(callback);
List<String> fileNames = addOn.getFiles();
if (fileNames == null || fileNames.isEmpty()) {
return true;
}
callback.filesWillBeRemoved(fileNames.size());
boolean uninstalledWithoutErrors = true;
for (String name : fileNames) {
if (name == null) {
continue;
}
logger.debug("Uninstall file: " + name);
File file = new File(Constant.getZapHome(), name);
try {
File parent = file.getParentFile();
if (!file.delete()) {
logger.error("Failed to delete: " + file.getAbsolutePath());
uninstalledWithoutErrors = false;
}
callback.fileRemoved();
if (parent.isDirectory() && parent.list().length == 0) {
logger.debug("Deleting: " + parent.getAbsolutePath());
if (!parent.delete()) {
// Ignore - check for <= 2 as on *nix '.' and '..' are returned
logger.debug("Failed to delete: " + parent.getAbsolutePath());
}
}
deleteEmptyDirsCreatedForAddOnFiles(file);
} catch (Exception e) {
logger.error("Failed to uninstall file " + file.getAbsolutePath(), e);
}
}
Control.getSingleton().getExtensionLoader().addonFilesRemoved();
return uninstalledWithoutErrors;
}
private static void deleteEmptyDirsCreatedForAddOnFiles(File file) {
if (file == null) {
return;
}
File currentFile = file;
// Delete any empty dirs up to the ZAP root dir
while (currentFile != null && !currentFile.exists()) {
currentFile = currentFile.getParentFile();
}
String root = new File(Constant.getZapHome()).getAbsolutePath();
while (currentFile != null && currentFile.exists()) {
if (currentFile.getAbsolutePath().startsWith(root) && currentFile.getAbsolutePath().length() > root.length()) {
deleteEmptyDirs(currentFile);
currentFile = currentFile.getParentFile();
} else {
// Gone above the ZAP home dir
break;
}
}
}
private static void deleteEmptyDirs(File dir) {
logger.debug("Deleting dir " + dir.getAbsolutePath());
for (File d : dir.listFiles()) {
if (d.isDirectory()) {
deleteEmptyDirs(d);
}
}
if (!dir.delete()) {
logger.debug("Failed to delete: " + dir.getAbsolutePath());
}
}
}