/*
* 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.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
import org.apache.log4j.Logger;
import org.zaproxy.zap.Version;
import org.zaproxy.zap.utils.ZapXmlConfiguration;
/**
* Base class that reads common {@code ZapAddOn} XML data.
* <p>
* Reads:
* <ul>
* <li>name;</li>
* <li>status (since 2.6.0);</li>
* <li>version;</li>
* <li>semver;</li>
* <li>description;</li>
* <li>author;</li>
* <li>url;</li>
* <li>changes;</li>
* <li>not-before-version;</li>
* <li>not-from-version;</li>
* <li>dependencies:
* <ul>
* <li>javaversion;</li>
* <li>addon:
* <ul>
* <li>id;</li>
* <li>not-before-version;</li>
* <li>not-from-version;</li>
* <li>semver;</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>extensions
* <ul>
* <li>extension</li>
* <li>extension v=1:
* <ul>
* <li>classname;</li>
* <li>dependencies:
* <ul>
* <li>javaversion;</li>
* <li>addon:
* <ul>
* <li>id;</li>
* <li>not-before-version;</li>
* <li>not-from-version;</li>
* <li>semver.</li>
* </ul>
* </li>
* </ul>
* </li>
* </ul>
* </li>
* </ul>
* </li>
* </ul>
*
* @since 2.4.0
*/
public abstract class BaseZapAddOnXmlData {
private static final Logger LOGGER = Logger.getLogger(BaseZapAddOnXmlData.class);
private static final String NAME_ELEMENT = "name";
private static final String STATUS = "status";
private static final String VERSION_ELEMENT = "version";
private static final String SEM_VER_ELEMENT = "semver";
private static final String DESCRIPTION_ELEMENT = "description";
private static final String AUTHOR_ELEMENT = "author";
private static final String URL_ELEMENT = "url";
private static final String CHANGES_ELEMENT = "changes";
private static final String NOT_BEFORE_VERSION_ELEMENT = "not-before-version";
private static final String NOT_FROM_VERSION_ELEMENT = "not-from-version";
private static final String DEPENDENCIES_ELEMENT = "dependencies";
private static final String DEPENDENCIES_JAVA_VERSION_ELEMENT = "javaversion";
private static final String DEPENDENCIES_ADDONS_ALL_ELEMENTS = "addons/addon";
private static final String ZAPADDON_ID_ELEMENT = "id";
private static final String ZAPADDON_NOT_BEFORE_VERSION_ELEMENT = "not-before-version";
private static final String ZAPADDON_NOT_FROM_VERSION_ELEMENT = "not-from-version";
private static final String ZAPADDON_SEMVER_ELEMENT = "semver";
private static final String EXTENSION_ELEMENT = "extension";
private static final String EXTENSIONS_ALL_ELEMENTS = "extensions/" + EXTENSION_ELEMENT;
private static final String EXTENSIONS_V1_ALL_ELEMENTS = "extensions/" + EXTENSION_ELEMENT + "[@v='1']";
private static final String EXTENSION_CLASS_NAME = "classname";
private static final String EXTENSION_DEPENDENCIES = DEPENDENCIES_ELEMENT + "/" + DEPENDENCIES_ADDONS_ALL_ELEMENTS;
private static final String CLASSNAMES_ALLOWED_ELEMENT = "allowed";
private static final String CLASSNAMES_ALLOWED_ALL_ELEMENTS = "classnames/" + CLASSNAMES_ALLOWED_ELEMENT;
private static final String CLASSNAMES_RESTRICTED_ELEMENT = "restricted";
private static final String CLASSNAMES_RESTRICTED_ALL_ELEMENTS = "classnames/" + CLASSNAMES_RESTRICTED_ELEMENT;
private String name;
private String status;
private int packageVersion;
private Version version;
private String description;
private String author;
private String url;
private String changes;
private Dependencies dependencies;
private AddOnClassnames addOnClassnames;
private String notBeforeVersion;
private String notFromVersion;
private List<String> extensions;
private List<ExtensionWithDeps> extensionsWithDeps;
/**
* Constructs a {@code BaseZapAddOnXmlData} with the given {@code inputStream} as the source of the {@code ZapAddOn} XML
* data.
*
* @param inputStream the source of the {@code ZapAddOn} XML data.
* @throws IOException if an error occurs while reading the data
*/
public BaseZapAddOnXmlData(InputStream inputStream) throws IOException {
ZapXmlConfiguration zapAddOnXml = new ZapXmlConfiguration();
zapAddOnXml.setExpressionEngine(new XPathExpressionEngine());
try {
zapAddOnXml.load(inputStream);
} catch (ConfigurationException e) {
throw new IOException(e);
}
readDataImpl(zapAddOnXml);
}
/**
* Constructs a {@code BaseZapAddOnXmlData} with the given {@code zapAddOnXml} {@code HierarchicalConfiguration} as the
* source of the {@code ZapAddOn} XML data.
* <p>
* The given {@code HierarchicalConfiguration} must have a {@code XPathExpressionEngine} installed.
*
* @param zapAddOnXml the source of the {@code ZapAddOn} XML data.
* @see XPathExpressionEngine
*/
public BaseZapAddOnXmlData(HierarchicalConfiguration zapAddOnXml) {
readDataImpl(zapAddOnXml);
}
private void readDataImpl(HierarchicalConfiguration zapAddOnXml) {
name = zapAddOnXml.getString(NAME_ELEMENT, "");
packageVersion = zapAddOnXml.getInt(VERSION_ELEMENT, 0);
status = zapAddOnXml.getString(STATUS, "alpha");
version = createVersion(zapAddOnXml.getString(SEM_VER_ELEMENT, ""));
description = zapAddOnXml.getString(DESCRIPTION_ELEMENT, "");
author = zapAddOnXml.getString(AUTHOR_ELEMENT, "");
url = zapAddOnXml.getString(URL_ELEMENT, "");
changes = zapAddOnXml.getString(CHANGES_ELEMENT, "");
dependencies = readDependencies(zapAddOnXml, "zapaddon");
notBeforeVersion = zapAddOnXml.getString(NOT_BEFORE_VERSION_ELEMENT, "");
notFromVersion = zapAddOnXml.getString(NOT_FROM_VERSION_ELEMENT, "");
extensions = getStrings(zapAddOnXml, EXTENSIONS_ALL_ELEMENTS, EXTENSION_ELEMENT);
extensionsWithDeps = readExtensionsWithDeps(zapAddOnXml);
addOnClassnames = readAddOnClassnames(zapAddOnXml);
readAdditionalData(zapAddOnXml);
}
/**
* Reads additional data from the {@code ZapAddOn} XML.
* <p>
* Called after reading the common data.
*
* @param zapAddOnData the source of the {@code ZapAddOn} XML data.
*/
protected void readAdditionalData(HierarchicalConfiguration zapAddOnData) {
}
private static Version createVersion(String version) {
if (!version.isEmpty()) {
return new Version(version);
}
return null;
}
public String getName() {
return name;
}
/**
* Returns the status of the add-on, "alpha", "beta" or "release".
*
* @return the status of the add-on
* @since 2.6.0
*/
public String getStatus() {
return status;
}
public String getDescription() {
return description;
}
public String getAuthor() {
return author;
}
public int getPackageVersion() {
return packageVersion;
}
public Version getVersion() {
return version;
}
public String getChanges() {
return changes;
}
public String getUrl() {
return url;
}
public Dependencies getDependencies() {
return dependencies;
}
public AddOnClassnames getAddOnClassnames() {
return addOnClassnames;
}
public String getNotBeforeVersion() {
return notBeforeVersion;
}
public String getNotFromVersion() {
return notFromVersion;
}
public List<String> getExtensions() {
return extensions;
}
public List<ExtensionWithDeps> getExtensionsWithDeps() {
return extensionsWithDeps;
}
protected List<String> getStrings(HierarchicalConfiguration zapAddOnXml, String element, String elementName) {
String[] fields = zapAddOnXml.getStringArray(element);
if (fields.length == 0) {
return Collections.emptyList();
}
ArrayList<String> strings = new ArrayList<>(fields.length);
for (String field : fields) {
if (!field.isEmpty()) {
strings.add(field);
} else {
LOGGER.warn("Ignoring empty \"" + elementName + "\" entry in add-on \"" + name + "\".");
}
}
if (strings.isEmpty()) {
return Collections.emptyList();
}
strings.trimToSize();
return strings;
}
private Dependencies readDependencies(HierarchicalConfiguration currentNode, String element) {
List<HierarchicalConfiguration> dependencies = currentNode.configurationsAt(DEPENDENCIES_ELEMENT);
if (dependencies.isEmpty()) {
return null;
}
if (dependencies.size() > 1) {
malformedFile("expected at most one \"dependencies\" element for \"" + element + "\" element, found "
+ dependencies.size() + ".");
}
HierarchicalConfiguration node = dependencies.get(0);
String javaVersion = node.getString(DEPENDENCIES_JAVA_VERSION_ELEMENT, "");
List<HierarchicalConfiguration> fields = node.configurationsAt(DEPENDENCIES_ADDONS_ALL_ELEMENTS);
if (fields.isEmpty()) {
return new Dependencies(javaVersion, Collections.<AddOnDep> emptyList());
}
List<AddOnDep> addOns = readAddOnDependencies(fields);
return new Dependencies(javaVersion, addOns);
}
private List<AddOnDep> readAddOnDependencies(List<HierarchicalConfiguration> fields) {
List<AddOnDep> addOns = new ArrayList<>(fields.size());
for (HierarchicalConfiguration sub : fields) {
String id = sub.getString(ZAPADDON_ID_ELEMENT, "");
if (id.isEmpty()) {
malformedFile("an add-on dependency has empty \"" + ZAPADDON_ID_ELEMENT + "\".");
}
AddOnDep addOnDep = new AddOnDep(id, sub.getString(ZAPADDON_NOT_BEFORE_VERSION_ELEMENT, ""), sub.getString(
ZAPADDON_NOT_FROM_VERSION_ELEMENT,
""), sub.getString(ZAPADDON_SEMVER_ELEMENT, ""));
addOns.add(addOnDep);
}
return addOns;
}
private List<ExtensionWithDeps> readExtensionsWithDeps(HierarchicalConfiguration currentNode) {
List<HierarchicalConfiguration> extensions = currentNode.configurationsAt(EXTENSIONS_V1_ALL_ELEMENTS);
if (extensions.isEmpty()) {
return Collections.emptyList();
}
List<ExtensionWithDeps> extensionsWithDeps = new ArrayList<>(extensions.size());
for (HierarchicalConfiguration extensionNode : extensions) {
String classname = extensionNode.getString(EXTENSION_CLASS_NAME, "");
if (classname.isEmpty()) {
malformedFile("a v1 extension has empty \"" + EXTENSION_CLASS_NAME + "\".");
}
List<HierarchicalConfiguration> fields = extensionNode.configurationsAt(EXTENSION_DEPENDENCIES);
if (fields.isEmpty()) {
// Extension v1 without dependencies, handle as normal extension.
if (this.extensions.isEmpty()) {
// Empty thus Collections.emptyList(), create and use a mutable list.
this.extensions = new ArrayList<>(5);
}
this.extensions.add(classname);
continue;
}
List<AddOnDep> addOnDeps = readAddOnDependencies(fields);
AddOnClassnames classnames = readAddOnClassnames(extensionNode);
extensionsWithDeps.add(new ExtensionWithDeps(classname, addOnDeps, classnames));
}
return extensionsWithDeps;
}
private AddOnClassnames readAddOnClassnames(HierarchicalConfiguration node) {
List<String> allowed = getStrings(node, CLASSNAMES_ALLOWED_ALL_ELEMENTS, CLASSNAMES_ALLOWED_ELEMENT);
List<String> restricted = getStrings(node, CLASSNAMES_RESTRICTED_ALL_ELEMENTS, CLASSNAMES_RESTRICTED_ELEMENT);
if (allowed.isEmpty() && restricted.isEmpty()) {
return AddOnClassnames.ALL_ALLOWED;
}
return new AddOnClassnames(allowed, restricted);
}
private void malformedFile(String reason) {
throw new IllegalArgumentException(
"Add-on \"" + name + "\" contains malformed " + AddOn.MANIFEST_FILE_NAME + " file, " + reason);
}
public static class Dependencies {
private final String javaVersion;
private final List<AddOnDep> addOnDependencies;
public Dependencies(String javaVersion, List<AddOnDep> addOnDependencies) {
this.javaVersion = javaVersion;
this.addOnDependencies = addOnDependencies;
}
public String getJavaVersion() {
return javaVersion;
}
public List<AddOnDep> getAddOns() {
return addOnDependencies;
}
}
public static class AddOnDep {
private final String id;
private final int notBeforeVersion;
private final int notFromVersion;
private final String semVer;
public AddOnDep(String id, String notBeforeVersion, String notFromVersion, String semVer) {
this.id = id;
this.notBeforeVersion = convertToInt(notBeforeVersion, -1, "not-before-version");
this.notFromVersion = convertToInt(notFromVersion, -1, "not-from-version");
this.semVer = semVer;
}
private static int convertToInt(String value, int defaultValue, String element) {
if (value == null || value.isEmpty()) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Expected integer for element \"" + element + "\" but was: " + value);
}
}
public String getId() {
return id;
}
public int getNotBeforeVersion() {
return notBeforeVersion;
}
public int getNotFromVersion() {
return notFromVersion;
}
public String getSemVer() {
return semVer;
}
}
public static class ExtensionWithDeps {
private final String classname;
private final List<AddOnDep> addOnDependencies;
private final AddOnClassnames addOnClassnames;
public ExtensionWithDeps(String classname, List<AddOnDep> addOnDependencies, AddOnClassnames addOnClassnames) {
this.classname = classname;
this.addOnDependencies = addOnDependencies;
this.addOnClassnames = addOnClassnames;
}
public String getClassname() {
return classname;
}
public List<AddOnDep> getDependencies() {
return addOnDependencies;
}
public AddOnClassnames getAddOnClassnames() {
return addOnClassnames;
}
}
}