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