org.parosproxy.paros.model.Model Maven / Gradle / Ivy
Show all versions of zap Show documentation
/*
*
* Paros and its related class files.
*
* Paros is an HTTP/HTTPS proxy for assessing web application security.
* Copyright (C) 2003-2004 Chinotec Technologies Company
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Clarified Artistic License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Clarified Artistic License for more details.
*
* You should have received a copy of the Clarified Artistic License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
// ZAP: 2012/02/18 Rationalised session handling
// ZAP: 2012/04/23 Added @Override annotation to the appropriate method.
// ZAP: 2012/05/02 Added the method createSingleton and changed the method
// getSingleton to use it.
// ZAP: 2012/06/11 Changed the method copySessionDb to call the method
// Database.close(boolean, boolean).
// ZAP: 2012/08/08 Check if file exist.
// ZAP: 2012/10/02 Issue 385: Added support for Contexts
// ZAP: 2013/03/03 Issue 546: Remove all template Javadoc comments
// ZAP: 2013/04/16 Issue 638: Persist and snapshot sessions instead of saving them
// ZAP: 2013/08/27 Issue 772: Restructuring of Saving/Loading Context Data
// ZAP: 2013/11/16 Issue 881: Fail immediately if zapdb.script file is not found
// ZAP: 2013/12/03 Issue 933: Automatically determine install dir
// ZAP: 2014/01/17 Issue 987: Allow arbitrary config file values to be set via the command line
// ZAP: 2014/07/15 Issue 1265: Context import and export
// ZAP: 2015/02/09 Issue 1525: Introduce a database interface layer to allow for alternative
// implementations
// ZAP: 2015/04/02 Issue 321: Support multiple databases
// ZAP: 2016/02/10 Issue 1958: Allow to disable database (HSQLDB) log
// ZAP: 2016/03/22 Allow to remove ContextDataFactory
// ZAP: 2016/03/23 Issue 2331: Custom Context Panels not show in existing contexts after
// installation of add-on
// ZAP: 2016/06/10 Do not clean up the database if the current session does not require it
// ZAP: 2016/07/05 Issue 2218: Persisted Sessions don't save unconfigured Default Context
// ZAP: 2017/06/07 Allow to persist the session properties (e.g. name, description).
// ZAP: 2018/03/27 Validate that context and configurations for ContextDataFactory are not null.
// ZAP: 2018/07/19 Fallback to bundled zapdb.script file.
// ZAP: 2018/08/15 Deprecated addSessionListener
// ZAP: 2019/06/01 Normalise line endings.
// ZAP: 2019/06/05 Normalise format/style.
// ZAP: 2020/09/15 Added the VariantFactory
// ZAP: 2020/10/14 Allow to set a singleton Model for tests.
// ZAP: 2020/11/26 Use Log4j 2 classes for logging.
// ZAP: 2022/09/21 Use format specifiers instead of concatenation when logging.
// ZAP: 2023/01/10 Tidy up logger.
package org.parosproxy.paros.model;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.db.Database;
import org.parosproxy.paros.db.paros.ParosDatabase;
import org.xml.sax.SAXException;
import org.zaproxy.zap.control.ControlOverrides;
import org.zaproxy.zap.db.sql.DbSQL;
import org.zaproxy.zap.extension.ascan.VariantFactory;
import org.zaproxy.zap.model.Context;
import org.zaproxy.zap.model.ContextDataFactory;
public class Model {
private static Model model = null;
private static final String DBNAME_TEMPLATE = Constant.DBNAME_TEMPLATE;
// private static final String DBNAME_UNTITLED = Constant.DBNAME_UNTITLED;
private String DBNAME_UNTITLED = Constant.getInstance().DBNAME_UNTITLED;
private static int DBNAME_COPY = 1;
private Session session = null;
private OptionsParam optionsParam = null;
private Database db = null;
private String currentDBNameUntitled = "";
// ZAP: Added logger
private static final Logger LOGGER = LogManager.getLogger(Model.class);
private List contextDataFactories = new ArrayList<>();
private VariantFactory variantFactory = new VariantFactory();
private boolean postInitialisation;
public Model() {
// make sure the variable here will not refer back to model itself.
// DO it in init or respective getter.
session = new Session(this);
optionsParam = new OptionsParam();
}
/**
* @return Returns the optionsParam.
*/
public OptionsParam getOptionsParam() {
if (optionsParam == null) {
optionsParam = new OptionsParam();
}
return optionsParam;
}
/**
* @param param The optionsParam to set.
*/
public void setOptionsParam(OptionsParam param) {
optionsParam = param;
}
/**
* @return Returns the session.
*/
public Session getSession() {
if (session == null) {
session = new Session(this);
}
return session;
}
/**
* This method should typically only be called from the Control class
*
* @return Returns the session.
*/
public Session newSession() {
session = new Session(this);
// Always start with one context
session.saveContext(
session.getNewContext(Constant.messages.getString("context.default.name")));
return session;
}
/** This method should typically only be called from the Control class */
public void openSession(String fileName)
throws SQLException, SAXException, IOException, Exception {
getSession().open(fileName);
}
public void openSession(String fileName, final SessionListener callback) {
getSession().open(fileName, callback);
}
/** This method should typically only be called from the Control class */
public void openSession(final File file, final SessionListener callback) {
getSession().open(file, callback);
}
/** This method should typically only be called from the Control class */
public void saveSession(final String fileName, final SessionListener callback) {
getSession().save(fileName, callback);
}
/** This method should typically only be called from the Control class */
public void saveSession(String fileName) throws Exception {
getSession().save(fileName);
}
/**
* Persists the properties (e.g. name, description) of the current session.
*
* Should be called only by "core" classes.
*
* @throws Exception if an error occurred while persisting the properties.
* @since 2.7.0
*/
public void persistSessionProperties() throws Exception {
getSession().persistProperties();
}
/** This method should typically only be called from the Control class */
public void snapshotSession(final String fileName, final SessionListener callback) {
getSession().snapshot(fileName, callback);
}
/** This method should typically only be called from the Control class */
public void discardSession() {
getSession().discard();
}
/** This method should typically only be called from the Control class */
public void closeSession() {
getSession().close();
}
public void init(ControlOverrides overrides) throws SAXException, IOException, Exception {
getOptionsParam().load(Constant.getInstance().FILE_CONFIG, overrides);
if (overrides.isExperimentalDb()) {
LOGGER.info("Using experimental database :/");
db = DbSQL.getSingleton().initDatabase();
} else {
db = new ParosDatabase();
}
db.setDatabaseOptions(getOptionsParam().getDatabaseParam());
createAndOpenUntitledDb();
HistoryReference.setTableHistory(getDb().getTableHistory());
HistoryReference.setTableTag(getDb().getTableTag());
HistoryReference.setTableAlert(getDb().getTableAlert());
}
public static Model getSingleton() {
if (model == null) {
// ZAP: Changed to use the method createSingleton().
createSingleton();
}
return model;
}
// ZAP: Added method.
private static synchronized void createSingleton() {
if (model == null) {
model = new Model();
}
}
/**
* Sets the given {@code Model} as the singleton.
*
*
Note: Not part of the public API.
*
* @param testModel the {@code Model} to test with.
*/
public static void setSingletonForTesting(Model testModel) {
model = testModel;
model.contextDataFactories = new ArrayList<>();
}
/**
* @return Returns the db.
*/
public Database getDb() {
return db;
}
// TODO disable for non file based sessions
public void moveSessionDb(String destFile) throws Exception {
// always use copySession because moving file does not work in Debian,
// and for Windows renaming file across different drives does not work.
copySessionDb(currentDBNameUntitled, destFile);
// getDb().close();
//
// boolean result = false;
// File fileIn1 = new File(currentDBNameUntitled + ".data");
// File fileIn2 = new File(currentDBNameUntitled + ".script");
// File fileIn3 = new File(currentDBNameUntitled + ".properties");
// File fileIn4 = new File(currentDBNameUntitled + ".backup");
//
// File fileOut1 = new File(destFile + ".data");
// File fileOut2 = new File(destFile + ".script");
// File fileOut3 = new File(destFile + ".properties");
// File fileOut4 = new File(destFile + ".backup");
//
// if (fileOut1.exists()) fileOut1.delete();
// if (fileOut2.exists()) fileOut2.delete();
// if (fileOut3.exists()) fileOut3.delete();
// if (fileOut4.exists()) fileOut4.delete();
//
// result = fileIn1.renameTo(fileOut1);
// result = fileIn2.renameTo(fileOut2);
// result = fileIn3.renameTo(fileOut3);
// if (fileIn4.exists()) {
// result = fileIn4.renameTo(fileOut4);
// }
//
// getDb().open(destFile);
}
// TODO disable for non file based sessions
protected void copySessionDb(String currentFile, String destFile) throws Exception {
// ZAP: Changed to call the method close(boolean, boolean).
getDb().close(false, false);
// copy session related files to the path specified
FileCopier copier = new FileCopier();
// ZAP: Check if files exist.
File fileIn1 = new File(currentFile + ".data");
if (fileIn1.exists()) {
File fileOut1 = new File(destFile + ".data");
copier.copy(fileIn1, fileOut1);
}
File fileIn2 = new File(currentFile + ".script");
if (fileIn2.exists()) {
File fileOut2 = new File(destFile + ".script");
copier.copy(fileIn2, fileOut2);
}
File fileIn3 = new File(currentFile + ".properties");
if (fileIn3.exists()) {
File fileOut3 = new File(destFile + ".properties");
copier.copy(fileIn3, fileOut3);
}
File fileIn4 = new File(currentFile + ".backup");
if (fileIn4.exists()) {
File fileOut4 = new File(destFile + ".backup");
copier.copy(fileIn4, fileOut4);
}
// ZAP: Handle the "lobs" file.
File lobsFile = new File(currentFile + ".lobs");
if (lobsFile.exists()) {
File newLobsFile = new File(destFile + ".lobs");
copier.copy(lobsFile, newLobsFile);
}
getDb().open(destFile);
}
// TODO disable for non file based sessions
protected void snapshotSessionDb(String currentFile, String destFile) throws Exception {
LOGGER.debug("snapshotSessionDb {} -> {}", currentFile, destFile);
// ZAP: Changed to call the method close(boolean, boolean).
getDb().close(false, false);
// copy session related files to the path specified
FileCopier copier = new FileCopier();
// ZAP: Check if files exist.
File fileIn1 = new File(currentFile + ".data");
if (fileIn1.exists()) {
File fileOut1 = new File(destFile + ".data");
copier.copy(fileIn1, fileOut1);
}
File fileIn2 = new File(currentFile + ".script");
if (fileIn2.exists()) {
File fileOut2 = new File(destFile + ".script");
copier.copy(fileIn2, fileOut2);
}
File fileIn3 = new File(currentFile + ".properties");
if (fileIn3.exists()) {
File fileOut3 = new File(destFile + ".properties");
copier.copy(fileIn3, fileOut3);
}
File fileIn4 = new File(currentFile + ".backup");
if (fileIn4.exists()) {
File fileOut4 = new File(destFile + ".backup");
copier.copy(fileIn4, fileOut4);
}
// ZAP: Handle the "lobs" file.
File lobsFile = new File(currentFile + ".lobs");
if (lobsFile.exists()) {
File newLobsFile = new File(destFile + ".lobs");
copier.copy(lobsFile, newLobsFile);
}
if (currentFile.length() == 0) {
LOGGER.debug("snapshotSessionDb using {} -> {}", currentDBNameUntitled, destFile);
currentFile = currentDBNameUntitled;
}
getDb().open(currentFile);
}
/** This method should typically only be called from the Control class */
// TODO disable for non file based sessions
public void createAndOpenUntitledDb() throws ClassNotFoundException, Exception {
getDb().close(false, session.isCleanUpRequired());
// delete all untitled session db in "session" directory
File dir = new File(getSession().getSessionFolder());
File[] listFile =
dir.listFiles(
new FilenameFilter() {
@Override
public boolean accept(File dir1, String fileName) {
if (fileName.startsWith("untitled")) {
return true;
}
return false;
}
});
for (int i = 0; i < listFile.length; i++) {
if (!listFile[i].delete()) {
// ZAP: Log failure to delete file
LOGGER.error("Failed to delete file {}", listFile[i].getAbsolutePath());
}
}
// ZAP: Check if files exist.
// copy and create new template db
currentDBNameUntitled = DBNAME_UNTITLED + DBNAME_COPY;
FileCopier copier = new FileCopier();
File fileIn = new File(Constant.getZapInstall(), DBNAME_TEMPLATE + ".data");
if (fileIn.exists()) {
File fileOut = new File(currentDBNameUntitled + ".data");
if (fileOut.exists() && !fileOut.delete()) {
// ZAP: Log failure to delete file
LOGGER.error("Failed to delete file {}", fileOut.getAbsolutePath());
}
copier.copy(fileIn, fileOut);
}
fileIn = new File(Constant.getZapInstall(), DBNAME_TEMPLATE + ".properties");
if (fileIn.exists()) {
File fileOut = new File(currentDBNameUntitled + ".properties");
if (fileOut.exists() && !fileOut.delete()) {
// ZAP: Log failure to delete file
LOGGER.error("Failed to delete file {}", fileOut.getAbsolutePath());
}
copier.copy(fileIn, fileOut);
}
fileIn = new File(Constant.getZapInstall(), DBNAME_TEMPLATE + ".script");
if (fileIn.exists()) {
File fileOut = new File(currentDBNameUntitled + ".script");
if (fileOut.exists() && !fileOut.delete()) {
// ZAP: Log failure to delete file
LOGGER.error("Failed to delete file {}", fileOut.getAbsolutePath());
}
copier.copy(fileIn, fileOut);
} else {
String fallbackResource = "/org/zaproxy/zap/resources/zapdb.script";
try (InputStream is = Model.class.getResourceAsStream(fallbackResource)) {
if (is == null) {
throw new FileNotFoundException(
"Bundled resource not found: " + fallbackResource);
}
Files.copy(is, Paths.get(currentDBNameUntitled + ".script"));
}
}
fileIn = new File(currentDBNameUntitled + ".backup");
if (fileIn.exists()) {
if (!fileIn.delete()) {
// ZAP: Log failure to delete file
LOGGER.error("Failed to delete file {}", fileIn.getAbsolutePath());
}
}
// ZAP: Handle the "lobs" file.
fileIn = new File(currentDBNameUntitled + ".lobs");
if (fileIn.exists()) {
if (!fileIn.delete()) {
LOGGER.error("Failed to delete file {}", fileIn.getAbsolutePath());
}
}
getDb().open(currentDBNameUntitled);
DBNAME_COPY++;
}
@Deprecated
public void addSessionListener(SessionListener listener) {}
/**
* Adds the given context data factory to the model.
*
* @param contextDataFactory the context data factory that will be added.
* @throws IllegalArgumentException if the given parameter is {@code null}.
* @see #removeContextDataFactory(ContextDataFactory)
*/
public void addContextDataFactory(ContextDataFactory contextDataFactory) {
if (contextDataFactory == null) {
throw new IllegalArgumentException("Parameter contextDataFactory must not be null.");
}
this.contextDataFactories.add(contextDataFactory);
if (postInitialisation) {
for (Context context : getSession().getContexts()) {
contextDataFactory.loadContextData(getSession(), context);
}
}
}
/**
* Removes the given context data factory from the model.
*
* @param contextDataFactory the context data factory that will be removed.
* @throws IllegalArgumentException if the given parameter is {@code null}.
* @since 2.5.0
* @see #addContextDataFactory(ContextDataFactory)
*/
public void removeContextDataFactory(ContextDataFactory contextDataFactory) {
if (contextDataFactory == null) {
throw new IllegalArgumentException("Parameter contextDataFactory must not be null.");
}
contextDataFactories.remove(contextDataFactory);
}
/**
* Loads the given context, by calling all the {@link ContextDataFactory}ies.
*
* @param ctx the context to load.
* @throws IllegalArgumentException (since 2.8.0) if the given context is {@code null}.
* @see ContextDataFactory#loadContextData(Session, Context)
* @since 2.0.0
*/
public void loadContext(Context ctx) {
validateContextNotNull(ctx);
for (ContextDataFactory cdf : this.contextDataFactories) {
cdf.loadContextData(getSession(), ctx);
}
}
/**
* Validates that the given context is not {@code null}, throwing an {@code
* IllegalArgumentException} if it is.
*
* @param context the context to be validated.
* @throws IllegalArgumentException if the context is {@code null}.
*/
private static void validateContextNotNull(Context context) {
if (context == null) {
throw new IllegalArgumentException("The context must not be null.");
}
}
/**
* Saves the given context, by calling all the {@link ContextDataFactory}ies.
*
* @param ctx the context to save.
* @throws IllegalArgumentException (since 2.8.0) if the given context is {@code null}.
* @since 2.0.0
* @see ContextDataFactory#persistContextData(Session, Context)
*/
public void saveContext(Context ctx) {
validateContextNotNull(ctx);
for (ContextDataFactory cdf : this.contextDataFactories) {
cdf.persistContextData(getSession(), ctx);
}
}
/**
* Import a context from the given configuration
*
* @param ctx the context to import the context data to.
* @param config the {@code Configuration} containing the context data.
* @throws ConfigurationException if an error occurred while reading the context data from the
* {@code Configuration}.
* @throws IllegalArgumentException (since 2.8.0) if the given context or configuration is
* {@code null}.
* @since 2.4.0
*/
public void importContext(Context ctx, Configuration config) throws ConfigurationException {
validateContextNotNull(ctx);
validateConfigNotNull(config);
for (ContextDataFactory cdf : this.contextDataFactories) {
cdf.importContextData(ctx, config);
}
}
/**
* Validates that the given configuration is not {@code null}, throwing an {@code
* IllegalArgumentException} if it is.
*
* @param config the config to be validated.
* @throws IllegalArgumentException if the config is {@code null}.
*/
private static void validateConfigNotNull(Configuration config) {
if (config == null) {
throw new IllegalArgumentException("The configuration must not be null.");
}
}
/**
* Export a context into the given configuration
*
* @param ctx the context to export.
* @param config the {@code Configuration} where to export the context data.
* @throws IllegalArgumentException (since 2.8.0) if the given context is {@code null}.
* @since 2.4.0
*/
public void exportContext(Context ctx, Configuration config) {
validateContextNotNull(ctx);
validateConfigNotNull(config);
for (ContextDataFactory cdf : this.contextDataFactories) {
cdf.exportContextData(ctx, config);
}
}
/**
* Notifies the model that the initialisation has been done.
*
*
Note: Should be called only by "core" code after the initialisation.
*
* @since 2.5.0
*/
public void postInit() {
postInitialisation = true;
}
/**
* Returns the VariantFactory
*
* @return the VariantFactory
* @since 2.10.0
*/
public VariantFactory getVariantFactory() {
return this.variantFactory;
}
}