/* * * 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 package org.parosproxy.paros.model; import java.io.File; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; 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.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.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 Logger logger = Logger.getLogger(Model.class); private List<SessionListener> sessionListeners = new ArrayList<>(); private List<ContextDataFactory> contextDataFactories = new ArrayList<>(); 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); } /** * 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 { ParosDatabase parosDb = new ParosDatabase(); parosDb.setDatabaseParam(getOptionsParam().getDatabaseParam()); db = parosDb; } 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(); } } /** * @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 acrossing 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 { throw new FileNotFoundException("Required file not found: " + fileIn.getAbsolutePath()); } 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++; } public void addSessionListener(SessionListener listener) { this.sessionListeners.add(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); } public void loadContext(Context ctx) { for (ContextDataFactory cdf : this.contextDataFactories) { cdf.loadContextData(getSession(), ctx); } } public void saveContext(Context ctx) { for (ContextDataFactory cdf : this.contextDataFactories) { cdf.persistContextData(getSession(), ctx); } } /** * Import a context from the given configuration * @param ctx * @param config * @throws ConfigurationException */ public void importContext(Context ctx, Configuration config) throws ConfigurationException { for (ContextDataFactory cdf : this.contextDataFactories) { cdf.importContextData(ctx, config); } } /** * Export a context into the given configuration * @param ctx * @param config */ public void exportContext(Context ctx, Configuration config) { for (ContextDataFactory cdf : this.contextDataFactories) { cdf.exportContextData(ctx, config); } } /** * Notifies the model that the initialisation has been done. * <p> * <strong>Note:</strong> Should be called only by "core" code after the initialisation. * * @since 2.5.0 */ public void postInit() { postInitialisation = true; } }