1 /*
2  *
3  * Paros and its related class files.
4  *
5  * Paros is an HTTP/HTTPS proxy for assessing web application security.
6  * Copyright (C) 2003-2004 Chinotec Technologies Company
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the Clarified Artistic License
10  * as published by the Free Software Foundation.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * Clarified Artistic License for more details.
16  *
17  * You should have received a copy of the Clarified Artistic License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
20  */
21 // ZAP: 2012/02/18 Rationalised session handling
22 // ZAP: 2012/04/23 Added @Override annotation to the appropriate method.
23 // ZAP: 2012/05/02 Added the method createSingleton and changed the method
24 // getSingleton to use it.
25 // ZAP: 2012/06/11 Changed the method copySessionDb to call the method
26 // Database.close(boolean, boolean).
27 // ZAP: 2012/08/08 Check if file exist.
28 // ZAP: 2012/10/02 Issue 385: Added support for Contexts
29 // ZAP: 2013/03/03 Issue 546: Remove all template Javadoc comments
30 // ZAP: 2013/04/16 Issue 638: Persist and snapshot sessions instead of saving them
31 // ZAP: 2013/08/27 Issue 772: Restructuring of Saving/Loading Context Data
32 // ZAP: 2013/11/16 Issue 881: Fail immediately if zapdb.script file is not found
33 // ZAP: 2013/12/03 Issue 933: Automatically determine install dir
34 // ZAP: 2014/01/17 Issue 987: Allow arbitrary config file values to be set via the command line
35 // ZAP: 2014/07/15 Issue 1265: Context import and export
36 // ZAP: 2015/02/09 Issue 1525: Introduce a database interface layer to allow for alternative
37 // implementations
38 // ZAP: 2015/04/02 Issue 321: Support multiple databases
39 // ZAP: 2016/02/10 Issue 1958: Allow to disable database (HSQLDB) log
40 // ZAP: 2016/03/22 Allow to remove ContextDataFactory
41 // ZAP: 2016/03/23 Issue 2331: Custom Context Panels not show in existing contexts after
42 // installation of add-on
43 // ZAP: 2016/06/10 Do not clean up the database if the current session does not require it
44 // ZAP: 2016/07/05 Issue 2218: Persisted Sessions don't save unconfigured Default Context
45 // ZAP: 2017/06/07 Allow to persist the session properties (e.g. name, description).
46 // ZAP: 2018/03/27 Validate that context and configurations for ContextDataFactory are not null.
47 // ZAP: 2018/07/19 Fallback to bundled zapdb.script file.
48 // ZAP: 2018/08/15 Deprecated addSessionListener
49 // ZAP: 2019/06/01 Normalise line endings.
50 // ZAP: 2019/06/05 Normalise format/style.
51 // ZAP: 2020/09/15 Added the VariantFactory
52 // ZAP: 2020/10/14 Allow to set a singleton Model for tests.
53 // ZAP: 2020/11/26 Use Log4j 2 classes for logging.
54 package org.parosproxy.paros.model;
55 
56 import java.io.File;
57 import java.io.FileNotFoundException;
58 import java.io.FilenameFilter;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.nio.file.Files;
62 import java.nio.file.Paths;
63 import java.sql.SQLException;
64 import java.util.ArrayList;
65 import java.util.List;
66 import org.apache.commons.configuration.Configuration;
67 import org.apache.commons.configuration.ConfigurationException;
68 import org.apache.logging.log4j.LogManager;
69 import org.apache.logging.log4j.Logger;
70 import org.parosproxy.paros.Constant;
71 import org.parosproxy.paros.db.Database;
72 import org.parosproxy.paros.db.paros.ParosDatabase;
73 import org.xml.sax.SAXException;
74 import org.zaproxy.zap.control.ControlOverrides;
75 import org.zaproxy.zap.db.sql.DbSQL;
76 import org.zaproxy.zap.extension.ascan.VariantFactory;
77 import org.zaproxy.zap.model.Context;
78 import org.zaproxy.zap.model.ContextDataFactory;
79 
80 public class Model {
81 
82     private static Model model = null;
83 
84     private static final String DBNAME_TEMPLATE = Constant.DBNAME_TEMPLATE;
85     // private static final String DBNAME_UNTITLED = Constant.DBNAME_UNTITLED;
86     private String DBNAME_UNTITLED = Constant.getInstance().DBNAME_UNTITLED;
87     private static int DBNAME_COPY = 1;
88 
89     private Session session = null;
90     private OptionsParam optionsParam = null;
91     private Database db = null;
92     private String currentDBNameUntitled = "";
93     // ZAP: Added logger
94     private Logger logger = LogManager.getLogger(Model.class);
95     private List<ContextDataFactory> contextDataFactories = new ArrayList<>();
96     private VariantFactory variantFactory = new VariantFactory();
97 
98     private boolean postInitialisation;
99 
Model()100     public Model() {
101         // make sure the variable here will not refer back to model itself.
102         // DO it in init or respective getter.
103 
104         session = new Session(this);
105         optionsParam = new OptionsParam();
106     }
107 
108     /** @return Returns the optionsParam. */
getOptionsParam()109     public OptionsParam getOptionsParam() {
110         if (optionsParam == null) {
111             optionsParam = new OptionsParam();
112         }
113         return optionsParam;
114     }
115 
116     /** @param param The optionsParam to set. */
setOptionsParam(OptionsParam param)117     public void setOptionsParam(OptionsParam param) {
118         optionsParam = param;
119     }
120 
121     /** @return Returns the session. */
getSession()122     public Session getSession() {
123         if (session == null) {
124             session = new Session(this);
125         }
126         return session;
127     }
128 
129     /**
130      * This method should typically only be called from the Control class
131      *
132      * @return Returns the session.
133      */
newSession()134     public Session newSession() {
135         session = new Session(this);
136         // Always start with one context
137         session.saveContext(
138                 session.getNewContext(Constant.messages.getString("context.default.name")));
139         return session;
140     }
141 
142     /** This method should typically only be called from the Control class */
openSession(String fileName)143     public void openSession(String fileName)
144             throws SQLException, SAXException, IOException, Exception {
145         getSession().open(fileName);
146     }
147 
openSession(String fileName, final SessionListener callback)148     public void openSession(String fileName, final SessionListener callback) {
149         getSession().open(fileName, callback);
150     }
151 
152     /** This method should typically only be called from the Control class */
openSession(final File file, final SessionListener callback)153     public void openSession(final File file, final SessionListener callback) {
154         getSession().open(file, callback);
155     }
156 
157     /** This method should typically only be called from the Control class */
saveSession(final String fileName, final SessionListener callback)158     public void saveSession(final String fileName, final SessionListener callback) {
159         getSession().save(fileName, callback);
160     }
161 
162     /** This method should typically only be called from the Control class */
saveSession(String fileName)163     public void saveSession(String fileName) throws Exception {
164         getSession().save(fileName);
165     }
166 
167     /**
168      * Persists the properties (e.g. name, description) of the current session.
169      *
170      * <p>Should be called only by "core" classes.
171      *
172      * @throws Exception if an error occurred while persisting the properties.
173      * @since 2.7.0
174      */
persistSessionProperties()175     public void persistSessionProperties() throws Exception {
176         getSession().persistProperties();
177     }
178 
179     /** This method should typically only be called from the Control class */
snapshotSession(final String fileName, final SessionListener callback)180     public void snapshotSession(final String fileName, final SessionListener callback) {
181         getSession().snapshot(fileName, callback);
182     }
183 
184     /** This method should typically only be called from the Control class */
discardSession()185     public void discardSession() {
186         getSession().discard();
187     }
188 
189     /** This method should typically only be called from the Control class */
closeSession()190     public void closeSession() {
191         getSession().close();
192     }
193 
init(ControlOverrides overrides)194     public void init(ControlOverrides overrides) throws SAXException, IOException, Exception {
195         getOptionsParam().load(Constant.getInstance().FILE_CONFIG, overrides);
196 
197         if (overrides.isExperimentalDb()) {
198             logger.info("Using experimental database :/");
199             db = DbSQL.getSingleton().initDatabase();
200         } else {
201             ParosDatabase parosDb = new ParosDatabase();
202             parosDb.setDatabaseParam(getOptionsParam().getDatabaseParam());
203             db = parosDb;
204         }
205 
206         createAndOpenUntitledDb();
207 
208         HistoryReference.setTableHistory(getDb().getTableHistory());
209         HistoryReference.setTableTag(getDb().getTableTag());
210         HistoryReference.setTableAlert(getDb().getTableAlert());
211     }
212 
getSingleton()213     public static Model getSingleton() {
214         if (model == null) {
215             // ZAP: Changed to use the method createSingleton().
216             createSingleton();
217         }
218         return model;
219     }
220 
221     // ZAP: Added method.
createSingleton()222     private static synchronized void createSingleton() {
223         if (model == null) {
224             model = new Model();
225         }
226     }
227 
228     /**
229      * Sets the given {@code Model} as the singleton.
230      *
231      * <p><strong>Note:</strong> Not part of the public API.
232      *
233      * @param testModel the {@code Model} to test with.
234      */
setSingletonForTesting(Model testModel)235     public static void setSingletonForTesting(Model testModel) {
236         model = testModel;
237         model.contextDataFactories = new ArrayList<>();
238     }
239 
240     /** @return Returns the db. */
getDb()241     public Database getDb() {
242         return db;
243     }
244 
245     // TODO disable for non file based sessions
moveSessionDb(String destFile)246     public void moveSessionDb(String destFile) throws Exception {
247 
248         // always use copySession because moving file does not work in Debian,
249         // and for Windows renaming file across different drives does not work.
250 
251         copySessionDb(currentDBNameUntitled, destFile);
252 
253         // getDb().close();
254         //
255         // boolean result = false;
256         // File fileIn1 = new File(currentDBNameUntitled + ".data");
257         // File fileIn2 = new File(currentDBNameUntitled + ".script");
258         // File fileIn3 = new File(currentDBNameUntitled + ".properties");
259         // File fileIn4 = new File(currentDBNameUntitled + ".backup");
260         //
261         // File fileOut1 = new File(destFile + ".data");
262         // File fileOut2 = new File(destFile + ".script");
263         // File fileOut3 = new File(destFile + ".properties");
264         // File fileOut4 = new File(destFile + ".backup");
265         //
266         // if (fileOut1.exists()) fileOut1.delete();
267         // if (fileOut2.exists()) fileOut2.delete();
268         // if (fileOut3.exists()) fileOut3.delete();
269         // if (fileOut4.exists()) fileOut4.delete();
270         //
271         // result = fileIn1.renameTo(fileOut1);
272         // result = fileIn2.renameTo(fileOut2);
273         // result = fileIn3.renameTo(fileOut3);
274         // if (fileIn4.exists()) {
275         // result = fileIn4.renameTo(fileOut4);
276         // }
277         //
278         // getDb().open(destFile);
279 
280     }
281 
282     // TODO disable for non file based sessions
copySessionDb(String currentFile, String destFile)283     protected void copySessionDb(String currentFile, String destFile) throws Exception {
284 
285         // ZAP: Changed to call the method close(boolean, boolean).
286         getDb().close(false, false);
287 
288         // copy session related files to the path specified
289         FileCopier copier = new FileCopier();
290 
291         // ZAP: Check if files exist.
292         File fileIn1 = new File(currentFile + ".data");
293         if (fileIn1.exists()) {
294             File fileOut1 = new File(destFile + ".data");
295             copier.copy(fileIn1, fileOut1);
296         }
297 
298         File fileIn2 = new File(currentFile + ".script");
299         if (fileIn2.exists()) {
300             File fileOut2 = new File(destFile + ".script");
301             copier.copy(fileIn2, fileOut2);
302         }
303 
304         File fileIn3 = new File(currentFile + ".properties");
305         if (fileIn3.exists()) {
306             File fileOut3 = new File(destFile + ".properties");
307             copier.copy(fileIn3, fileOut3);
308         }
309 
310         File fileIn4 = new File(currentFile + ".backup");
311         if (fileIn4.exists()) {
312             File fileOut4 = new File(destFile + ".backup");
313             copier.copy(fileIn4, fileOut4);
314         }
315 
316         // ZAP: Handle the "lobs" file.
317         File lobsFile = new File(currentFile + ".lobs");
318         if (lobsFile.exists()) {
319             File newLobsFile = new File(destFile + ".lobs");
320             copier.copy(lobsFile, newLobsFile);
321         }
322 
323         getDb().open(destFile);
324     }
325 
326     // TODO disable for non file based sessions
snapshotSessionDb(String currentFile, String destFile)327     protected void snapshotSessionDb(String currentFile, String destFile) throws Exception {
328         logger.debug("snapshotSessionDb " + currentFile + " -> " + destFile);
329 
330         // ZAP: Changed to call the method close(boolean, boolean).
331         getDb().close(false, false);
332 
333         // copy session related files to the path specified
334         FileCopier copier = new FileCopier();
335 
336         // ZAP: Check if files exist.
337         File fileIn1 = new File(currentFile + ".data");
338         if (fileIn1.exists()) {
339             File fileOut1 = new File(destFile + ".data");
340             copier.copy(fileIn1, fileOut1);
341         }
342 
343         File fileIn2 = new File(currentFile + ".script");
344         if (fileIn2.exists()) {
345             File fileOut2 = new File(destFile + ".script");
346             copier.copy(fileIn2, fileOut2);
347         }
348 
349         File fileIn3 = new File(currentFile + ".properties");
350         if (fileIn3.exists()) {
351             File fileOut3 = new File(destFile + ".properties");
352             copier.copy(fileIn3, fileOut3);
353         }
354 
355         File fileIn4 = new File(currentFile + ".backup");
356         if (fileIn4.exists()) {
357             File fileOut4 = new File(destFile + ".backup");
358             copier.copy(fileIn4, fileOut4);
359         }
360 
361         // ZAP: Handle the "lobs" file.
362         File lobsFile = new File(currentFile + ".lobs");
363         if (lobsFile.exists()) {
364             File newLobsFile = new File(destFile + ".lobs");
365             copier.copy(lobsFile, newLobsFile);
366         }
367 
368         if (currentFile.length() == 0) {
369             logger.debug("snapshotSessionDb using " + currentDBNameUntitled + " -> " + destFile);
370             currentFile = currentDBNameUntitled;
371         }
372 
373         getDb().open(currentFile);
374     }
375 
376     /** This method should typically only be called from the Control class */
377     // TODO disable for non file based sessions
createAndOpenUntitledDb()378     public void createAndOpenUntitledDb() throws ClassNotFoundException, Exception {
379 
380         getDb().close(false, session.isCleanUpRequired());
381 
382         // delete all untitled session db in "session" directory
383         File dir = new File(getSession().getSessionFolder());
384         File[] listFile =
385                 dir.listFiles(
386                         new FilenameFilter() {
387                             @Override
388                             public boolean accept(File dir1, String fileName) {
389                                 if (fileName.startsWith("untitled")) {
390                                     return true;
391                                 }
392                                 return false;
393                             }
394                         });
395         for (int i = 0; i < listFile.length; i++) {
396             if (!listFile[i].delete()) {
397                 // ZAP: Log failure to delete file
398                 logger.error("Failed to delete file " + listFile[i].getAbsolutePath());
399             }
400         }
401 
402         // ZAP: Check if files exist.
403         // copy and create new template db
404         currentDBNameUntitled = DBNAME_UNTITLED + DBNAME_COPY;
405         FileCopier copier = new FileCopier();
406         File fileIn = new File(Constant.getZapInstall(), DBNAME_TEMPLATE + ".data");
407         if (fileIn.exists()) {
408             File fileOut = new File(currentDBNameUntitled + ".data");
409             if (fileOut.exists() && !fileOut.delete()) {
410                 // ZAP: Log failure to delete file
411                 logger.error("Failed to delete file " + fileOut.getAbsolutePath());
412             }
413 
414             copier.copy(fileIn, fileOut);
415         }
416 
417         fileIn = new File(Constant.getZapInstall(), DBNAME_TEMPLATE + ".properties");
418         if (fileIn.exists()) {
419             File fileOut = new File(currentDBNameUntitled + ".properties");
420             if (fileOut.exists() && !fileOut.delete()) {
421                 // ZAP: Log failure to delete file
422                 logger.error("Failed to delete file " + fileOut.getAbsolutePath());
423             }
424 
425             copier.copy(fileIn, fileOut);
426         }
427 
428         fileIn = new File(Constant.getZapInstall(), DBNAME_TEMPLATE + ".script");
429         if (fileIn.exists()) {
430             File fileOut = new File(currentDBNameUntitled + ".script");
431             if (fileOut.exists() && !fileOut.delete()) {
432                 // ZAP: Log failure to delete file
433                 logger.error("Failed to delete file " + fileOut.getAbsolutePath());
434             }
435 
436             copier.copy(fileIn, fileOut);
437         } else {
438             String fallbackResource = "/org/zaproxy/zap/resources/zapdb.script";
439             try (InputStream is = Model.class.getResourceAsStream(fallbackResource)) {
440                 if (is == null) {
441                     throw new FileNotFoundException(
442                             "Bundled resource not found: " + fallbackResource);
443                 }
444                 Files.copy(is, Paths.get(currentDBNameUntitled + ".script"));
445             }
446         }
447 
448         fileIn = new File(currentDBNameUntitled + ".backup");
449         if (fileIn.exists()) {
450             if (!fileIn.delete()) {
451                 // ZAP: Log failure to delete file
452                 logger.error("Failed to delete file " + fileIn.getAbsolutePath());
453             }
454         }
455 
456         // ZAP: Handle the "lobs" file.
457         fileIn = new File(currentDBNameUntitled + ".lobs");
458         if (fileIn.exists()) {
459             if (!fileIn.delete()) {
460                 logger.error("Failed to delete file " + fileIn.getAbsolutePath());
461             }
462         }
463 
464         getDb().open(currentDBNameUntitled);
465         DBNAME_COPY++;
466     }
467 
468     @Deprecated
addSessionListener(SessionListener listener)469     public void addSessionListener(SessionListener listener) {}
470 
471     /**
472      * Adds the given context data factory to the model.
473      *
474      * @param contextDataFactory the context data factory that will be added.
475      * @throws IllegalArgumentException if the given parameter is {@code null}.
476      * @see #removeContextDataFactory(ContextDataFactory)
477      */
addContextDataFactory(ContextDataFactory contextDataFactory)478     public void addContextDataFactory(ContextDataFactory contextDataFactory) {
479         if (contextDataFactory == null) {
480             throw new IllegalArgumentException("Parameter contextDataFactory must not be null.");
481         }
482         this.contextDataFactories.add(contextDataFactory);
483 
484         if (postInitialisation) {
485             for (Context context : getSession().getContexts()) {
486                 contextDataFactory.loadContextData(getSession(), context);
487             }
488         }
489     }
490 
491     /**
492      * Removes the given context data factory from the model.
493      *
494      * @param contextDataFactory the context data factory that will be removed.
495      * @throws IllegalArgumentException if the given parameter is {@code null}.
496      * @since 2.5.0
497      * @see #addContextDataFactory(ContextDataFactory)
498      */
removeContextDataFactory(ContextDataFactory contextDataFactory)499     public void removeContextDataFactory(ContextDataFactory contextDataFactory) {
500         if (contextDataFactory == null) {
501             throw new IllegalArgumentException("Parameter contextDataFactory must not be null.");
502         }
503         contextDataFactories.remove(contextDataFactory);
504     }
505 
506     /**
507      * Loads the given context, by calling all the {@link ContextDataFactory}ies.
508      *
509      * @param ctx the context to load.
510      * @throws IllegalArgumentException (since 2.8.0) if the given context is {@code null}.
511      * @see ContextDataFactory#loadContextData(Session, Context)
512      * @since 2.0.0
513      */
loadContext(Context ctx)514     public void loadContext(Context ctx) {
515         validateContextNotNull(ctx);
516 
517         for (ContextDataFactory cdf : this.contextDataFactories) {
518             cdf.loadContextData(getSession(), ctx);
519         }
520     }
521 
522     /**
523      * Validates that the given context is not {@code null}, throwing an {@code
524      * IllegalArgumentException} if it is.
525      *
526      * @param context the context to be validated.
527      * @throws IllegalArgumentException if the context is {@code null}.
528      */
validateContextNotNull(Context context)529     private static void validateContextNotNull(Context context) {
530         if (context == null) {
531             throw new IllegalArgumentException("The context must not be null.");
532         }
533     }
534 
535     /**
536      * Saves the given context, by calling all the {@link ContextDataFactory}ies.
537      *
538      * @param ctx the context to save.
539      * @throws IllegalArgumentException (since 2.8.0) if the given context is {@code null}.
540      * @since 2.0.0
541      * @see ContextDataFactory#persistContextData(Session, Context)
542      */
saveContext(Context ctx)543     public void saveContext(Context ctx) {
544         validateContextNotNull(ctx);
545 
546         for (ContextDataFactory cdf : this.contextDataFactories) {
547             cdf.persistContextData(getSession(), ctx);
548         }
549     }
550 
551     /**
552      * Import a context from the given configuration
553      *
554      * @param ctx the context to import the context data to.
555      * @param config the {@code Configuration} containing the context data.
556      * @throws ConfigurationException if an error occurred while reading the context data from the
557      *     {@code Configuration}.
558      * @throws IllegalArgumentException (since 2.8.0) if the given context or configuration is
559      *     {@code null}.
560      * @since 2.4.0
561      */
importContext(Context ctx, Configuration config)562     public void importContext(Context ctx, Configuration config) throws ConfigurationException {
563         validateContextNotNull(ctx);
564         validateConfigNotNull(config);
565 
566         for (ContextDataFactory cdf : this.contextDataFactories) {
567             cdf.importContextData(ctx, config);
568         }
569     }
570 
571     /**
572      * Validates that the given configuration is not {@code null}, throwing an {@code
573      * IllegalArgumentException} if it is.
574      *
575      * @param config the config to be validated.
576      * @throws IllegalArgumentException if the config is {@code null}.
577      */
validateConfigNotNull(Configuration config)578     private static void validateConfigNotNull(Configuration config) {
579         if (config == null) {
580             throw new IllegalArgumentException("The configuration must not be null.");
581         }
582     }
583 
584     /**
585      * Export a context into the given configuration
586      *
587      * @param ctx the context to export.
588      * @param config the {@code Configuration} where to export the context data.
589      * @throws IllegalArgumentException (since 2.8.0) if the given context is {@code null}.
590      * @since 2.4.0
591      */
exportContext(Context ctx, Configuration config)592     public void exportContext(Context ctx, Configuration config) {
593         validateContextNotNull(ctx);
594         validateConfigNotNull(config);
595 
596         for (ContextDataFactory cdf : this.contextDataFactories) {
597             cdf.exportContextData(ctx, config);
598         }
599     }
600 
601     /**
602      * Notifies the model that the initialisation has been done.
603      *
604      * <p><strong>Note:</strong> Should be called only by "core" code after the initialisation.
605      *
606      * @since 2.5.0
607      */
postInit()608     public void postInit() {
609         postInitialisation = true;
610     }
611 
612     /**
613      * Returns the VariantFactory
614      *
615      * @return the VariantFactory
616      * @since 2.10.0
617      */
getVariantFactory()618     public VariantFactory getVariantFactory() {
619         return this.variantFactory;
620     }
621 }
622