1 /* 2 * Zed Attack Proxy (ZAP) and its related class files. 3 * 4 * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 * 6 * Copyright 2012 The ZAP Development Team 7 * 8 * Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 package org.zaproxy.zap.control; 21 22 import java.awt.EventQueue; 23 import java.io.File; 24 import java.io.FileFilter; 25 import java.io.FilenameFilter; 26 import java.lang.reflect.Constructor; 27 import java.lang.reflect.Modifier; 28 import java.net.MalformedURLException; 29 import java.net.URI; 30 import java.net.URISyntaxException; 31 import java.net.URL; 32 import java.net.URLClassLoader; 33 import java.util.ArrayList; 34 import java.util.Collection; 35 import java.util.Collections; 36 import java.util.Enumeration; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.Iterator; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 import java.util.ResourceBundle; 44 import java.util.Set; 45 import java.util.concurrent.locks.Lock; 46 import java.util.concurrent.locks.ReentrantLock; 47 import java.util.jar.JarEntry; 48 import java.util.jar.JarFile; 49 import java.util.stream.Collectors; 50 import java.util.zip.ZipEntry; 51 import org.apache.commons.configuration.Configuration; 52 import org.apache.commons.configuration.ConfigurationException; 53 import org.apache.commons.configuration.FileConfiguration; 54 import org.apache.commons.configuration.HierarchicalConfiguration; 55 import org.apache.logging.log4j.LogManager; 56 import org.apache.logging.log4j.Logger; 57 import org.parosproxy.paros.Constant; 58 import org.parosproxy.paros.control.Control; 59 import org.parosproxy.paros.core.scanner.AbstractPlugin; 60 import org.parosproxy.paros.extension.Extension; 61 import org.parosproxy.paros.model.Model; 62 import org.parosproxy.paros.view.View; 63 import org.zaproxy.zap.Version; 64 import org.zaproxy.zap.control.AddOn.AddOnRunRequirements; 65 import org.zaproxy.zap.control.AddOn.ExtensionRunRequirements; 66 import org.zaproxy.zap.extension.pscan.PluginPassiveScanner; 67 import org.zaproxy.zap.utils.ZapXmlConfiguration; 68 69 /** 70 * This class is heavily based on the original Paros class org.parosproxy.paros.common.DynamicLoader 71 * However its been restructured and enhanced to support multiple directories or versioned ZAP 72 * addons. The constructor takes an array of directories. All of the generic jars in the directories 73 * are loaded. Only the latest ZAP addons are loaded, so if the following addons are found: 74 * zap-ext-test-alpha-1.zap zap-ext-test-beta-2.zap zap-ext-test-alpha-3.zap then only the latest 75 * one (zap-ext-test-alpha-3.zap) will be loaded - this is entirely based on the version number. The 76 * status (alpha/beta/release) is for informational purposes only. 77 */ 78 public class AddOnLoader extends URLClassLoader { 79 80 public static final String ADDONS_BLOCK_LIST = "addons.block"; 81 82 private static final String ADDONS_RUNNABLE_BASE_KEY = "runnableAddOns"; 83 private static final String ADDONS_RUNNABLE_KEY = ADDONS_RUNNABLE_BASE_KEY + ".addon"; 84 private static final String ADDON_RUNNABLE_ID_KEY = "id"; 85 private static final String ADDON_RUNNABLE_VERSION_KEY = "version"; 86 private static final String ADDON_RUNNABLE_FULL_VERSION_KEY = "fullversion"; 87 private static final String ADDON_RUNNABLE_ALL_EXTENSIONS_KEY = "extensions.extension"; 88 89 /** A "null" object, for use when no callback is given during the uninstallation process. */ 90 private static final AddOnUninstallationProgressCallback NULL_CALLBACK = 91 new NullUninstallationProgressCallBack(); 92 93 private static final Logger logger = LogManager.getLogger(AddOnLoader.class); 94 95 static { ClassLoader.registerAsParallelCapable()96 ClassLoader.registerAsParallelCapable(); 97 } 98 99 private Lock installationLock = new ReentrantLock(); 100 private AddOnCollection aoc = null; 101 private List<File> jars = new ArrayList<>(); 102 /** 103 * Addons can be included in the ZAP release, in which case the user might not have permissions 104 * to delete the files. To support the removal of such addons we just maintain a 'block list' in 105 * the configs which is a comma separated list of the addon ids that the user has uninstalled 106 */ 107 private List<String> blockList = new ArrayList<>(); 108 109 /** 110 * The runnable add-ons and its extensions. 111 * 112 * <p>The key is the add-on itself and the value its runnable extensions. 113 */ 114 private Map<AddOn, List<String>> runnableAddOns; 115 116 /** 117 * The list of add-ons' IDs that have running issues (either the add-on itself or one of its 118 * extensions) since last run because of changes in the dependencies. 119 */ 120 private List<String> idsAddOnsWithRunningIssuesSinceLastRun; 121 122 /* 123 * Using sub-classloaders means we can unload and reload addons 124 */ 125 private Map<String, AddOnClassLoader> addOnLoaders = new HashMap<>(); 126 127 /** File where the data of runnable state and blocked add-ons is saved. */ 128 private ZapXmlConfiguration addOnsStateConfig; 129 AddOnLoader(File[] dirs)130 public AddOnLoader(File[] dirs) { 131 super(new URL[0], AddOnLoader.class.getClassLoader()); 132 133 addOnsStateConfig = new ZapXmlConfiguration(); 134 addOnsStateConfig.setRootElementName("addonsstate"); 135 File configFile = new File(Constant.getZapHome(), "add-ons-state.xml"); 136 addOnsStateConfig.setFile(configFile); 137 if (!migrateOldAddOnsState(addOnsStateConfig) && configFile.exists()) { 138 try { 139 addOnsStateConfig.load(); 140 } catch (ConfigurationException e) { 141 logger.warn("Failed to read add-ons' state file:", e); 142 } 143 } 144 145 this.loadBlockList(); 146 147 this.aoc = new AddOnCollection(dirs); 148 loadAllAddOns(); 149 150 if (dirs != null) { 151 for (File dir : dirs) { 152 try { 153 this.addDirectory(dir); 154 } catch (Exception e) { 155 logger.error(e.getMessage(), e); 156 } 157 } 158 } 159 160 for (File jar : jars) { 161 try { 162 this.addURL(jar.toURI().toURL()); 163 } catch (MalformedURLException e) { 164 logger.error(e.getMessage(), e); 165 } 166 } 167 168 // Install any files that are not already present 169 for (Entry<String, AddOnClassLoader> entry : addOnLoaders.entrySet()) { 170 AddOnInstaller.installMissingAddOnFiles( 171 entry.getValue(), getAddOnCollection().getAddOn(entry.getKey())); 172 } 173 } 174 175 /** 176 * Returns a list with the IDs of add-ons that have running issues since last run, either Java 177 * version was changed, or add-on dependencies are no longer met for the add-on or one of its 178 * extensions. 179 * 180 * @return a list with the add-ons that are not longer runnable 181 * @since 2.4.0 182 */ getIdsAddOnsWithRunningIssuesSinceLastRun()183 public List<String> getIdsAddOnsWithRunningIssuesSinceLastRun() { 184 return Collections.unmodifiableList(idsAddOnsWithRunningIssuesSinceLastRun); 185 } 186 loadAllAddOns()187 private void loadAllAddOns() { 188 for (Iterator<AddOn> iterator = aoc.getAddOns().iterator(); iterator.hasNext(); ) { 189 AddOn addOn = iterator.next(); 190 if (canLoadAddOn(addOn)) { 191 AddOnInstaller.installMissingAddOnLibs(addOn); 192 } else { 193 iterator.remove(); 194 } 195 } 196 197 runnableAddOns = new HashMap<>(); 198 idsAddOnsWithRunningIssuesSinceLastRun = new ArrayList<>(); 199 Map<AddOn, AddOnRunState> oldRunnableAddOns = loadAddOnsRunState(addOnsStateConfig, aoc); 200 List<AddOn> runAddons = new ArrayList<>(); 201 Set<AddOn> updatedAddOns = new HashSet<>(); 202 Set<AddOn> nonRunnableAddOns = new HashSet<>(); 203 for (Iterator<AddOn> iterator = aoc.getAddOns().iterator(); iterator.hasNext(); ) { 204 AddOn addOn = iterator.next(); 205 AddOnRunRequirements reqs = calculateRunRequirements(addOn, aoc.getAddOns()); 206 if (reqs.isRunnable()) { 207 AddOnRunState runState = oldRunnableAddOns.get(addOn); 208 List<String> runnableExtensions; 209 if (addOn.hasExtensionsWithDeps()) { 210 runnableExtensions = getRunnableExtensionsWithDeps(reqs); 211 List<String> oldRunnableExtensions = 212 runState != null ? runState.getExtensions() : Collections.emptyList(); 213 if (!oldRunnableExtensions.isEmpty()) { 214 oldRunnableExtensions.removeAll(runnableExtensions); 215 if (!oldRunnableExtensions.isEmpty()) { 216 idsAddOnsWithRunningIssuesSinceLastRun.add(addOn.getId()); 217 } 218 } 219 } else { 220 runnableExtensions = Collections.emptyList(); 221 } 222 223 runnableAddOns.put(addOn, runnableExtensions); 224 runAddons.add(addOn); 225 if (runState != null && runState.hasNewerVersion()) { 226 updatedAddOns.add(addOn); 227 } 228 } else { 229 nonRunnableAddOns.add(addOn); 230 } 231 } 232 233 nonRunnableAddOns.stream() 234 .filter(oldRunnableAddOns::containsKey) 235 .map(AddOn::getId) 236 .forEach(idsAddOnsWithRunningIssuesSinceLastRun::add); 237 238 saveAddOnsRunState(runnableAddOns); 239 240 for (AddOn addOn : runAddons) { 241 addOn.setInstallationStatus(AddOn.InstallationStatus.INSTALLED); 242 AddOnClassLoader addOnClassLoader = createAndAddAddOnClassLoader(addOn); 243 if (updatedAddOns.contains(addOn)) { 244 AddOnInstaller.updateAddOnFiles(addOnClassLoader, addOn); 245 } 246 AddOnInstaller.installResourceBundle(addOnClassLoader, addOn); 247 } 248 } 249 getRunnableExtensionsWithDeps( AddOnRunRequirements runRequirements)250 private static List<String> getRunnableExtensionsWithDeps( 251 AddOnRunRequirements runRequirements) { 252 List<String> runnableExtensions = new ArrayList<>(); 253 for (ExtensionRunRequirements extReqs : runRequirements.getExtensionRequirements()) { 254 if (extReqs.isRunnable()) { 255 runnableExtensions.add(extReqs.getClassname()); 256 } 257 } 258 return runnableExtensions; 259 } 260 canLoadAddOn(AddOn ao)261 private boolean canLoadAddOn(AddOn ao) { 262 if (blockList.contains(ao.getId())) { 263 if (logger.isDebugEnabled()) { 264 logger.debug( 265 "Can't load add-on " 266 + ao.getName() 267 + " it's on the block list (add-on uninstalled but the file couldn't be removed)."); 268 } 269 return false; 270 } 271 272 if (!ao.canLoadInCurrentVersion()) { 273 if (logger.isDebugEnabled()) { 274 logger.debug( 275 "Can't load add-on " 276 + ao.getName() 277 + " because of ZAP version constraints; Not before=" 278 + ao.getNotBeforeVersion() 279 + " Not from=" 280 + ao.getNotFromVersion() 281 + " Current Version=" 282 + Constant.PROGRAM_VERSION); 283 } 284 return false; 285 } 286 return true; 287 } 288 calculateRunRequirements( AddOn ao, Collection<AddOn> availableAddOns)289 private static AddOnRunRequirements calculateRunRequirements( 290 AddOn ao, Collection<AddOn> availableAddOns) { 291 AddOnRunRequirements reqs = ao.calculateRunRequirements(availableAddOns); 292 if (!reqs.isRunnable()) { 293 if (logger.isDebugEnabled()) { 294 logger.debug( 295 "Can't run add-on " 296 + ao.getName() 297 + " because of missing requirements: " 298 + AddOnRunIssuesUtils.getRunningIssues(reqs)); 299 } 300 } 301 return reqs; 302 } 303 createAndAddAddOnClassLoader(AddOn ao)304 private AddOnClassLoader createAndAddAddOnClassLoader(AddOn ao) { 305 try { 306 AddOnClassLoader addOnClassLoader = addOnLoaders.get(ao.getId()); 307 if (addOnClassLoader != null) { 308 return addOnClassLoader; 309 } 310 311 List<String> idsAddOnDependencies = ao.getIdsAddOnDependencies(); 312 if (idsAddOnDependencies.isEmpty()) { 313 addOnClassLoader = 314 new AddOnClassLoader( 315 ao.getFile().toURI().toURL(), this, ao.getAddOnClassnames()); 316 putAddOnClassLoader(ao, addOnClassLoader); 317 return addOnClassLoader; 318 } 319 320 List<AddOnClassLoader> dependencies = new ArrayList<>(idsAddOnDependencies.size()); 321 for (String addOnId : idsAddOnDependencies) { 322 addOnClassLoader = addOnLoaders.get(addOnId); 323 if (addOnClassLoader == null) { 324 addOnClassLoader = createAndAddAddOnClassLoader(aoc.getAddOn(addOnId)); 325 } 326 dependencies.add(addOnClassLoader); 327 } 328 329 addOnClassLoader = 330 new AddOnClassLoader( 331 ao.getFile().toURI().toURL(), 332 this, 333 dependencies, 334 ao.getAddOnClassnames()); 335 putAddOnClassLoader(ao, addOnClassLoader); 336 return addOnClassLoader; 337 } catch (MalformedURLException e) { 338 logger.error(e.getMessage(), e); 339 throw new RuntimeException( 340 "Failed to convert URL for AddOnClassLoader " + ao.getFile().toURI(), e); 341 } 342 } 343 344 /** 345 * Puts the given add-on class loader into the {@link #addOnLoaders} map and {@link 346 * AddOn#setClassLoader(ClassLoader) sets it into the add-on}. 347 * 348 * <p>The add-on libraries are added to the add-on class loader before that. 349 * 350 * @param ao the add-on to put in the map. 351 * @param addOnClassLoader the class loader of the add-on. 352 */ putAddOnClassLoader(AddOn ao, AddOnClassLoader addOnClassLoader)353 private void putAddOnClassLoader(AddOn ao, AddOnClassLoader addOnClassLoader) { 354 if (!ao.getLibs().isEmpty()) { 355 addOnClassLoader.addUrls( 356 ao.getLibs().stream() 357 .map(AddOn.Lib::getFileSystemUrl) 358 .collect(Collectors.toList())); 359 } 360 ao.setClassLoader(addOnClassLoader); 361 addOnLoaders.put(ao.getId(), addOnClassLoader); 362 } 363 364 @Override loadClass(String name)365 public Class<?> loadClass(String name) throws ClassNotFoundException { 366 synchronized (getClassLoadingLock(name)) { 367 try { 368 return loadClass(name, false); 369 } catch (ClassNotFoundException e) { 370 // Continue for now 371 } 372 for (AddOnClassLoader loader : addOnLoaders.values()) { 373 try { 374 return loader.loadClass(name); 375 } catch (ClassNotFoundException e) { 376 // Continue for now 377 } 378 for (AddOnClassLoader childLoader : loader.getChildClassLoaders()) { 379 try { 380 return childLoader.loadClass(name); 381 } catch (ClassNotFoundException e) { 382 // Continue for now 383 } 384 } 385 } 386 throw new ClassNotFoundException(name); 387 } 388 } 389 390 @Override getClassLoadingLock(String className)391 protected Object getClassLoadingLock(String className) { 392 // Allow AddOnClassLoader to use the same locks. 393 return super.getClassLoadingLock(className); 394 } 395 396 @Override getResource(String name)397 public URL getResource(String name) { 398 URL url = super.getResource(name); 399 if (url != null) { 400 return url; 401 } 402 for (AddOnClassLoader loader : addOnLoaders.values()) { 403 url = loader.findResourceInAddOn(name); 404 if (url != null) { 405 return url; 406 } 407 } 408 return url; 409 } 410 getAddOnCollection()411 public AddOnCollection getAddOnCollection() { 412 return this.aoc; 413 } 414 addDirectory(File dir)415 private void addDirectory(File dir) { 416 if (dir == null) { 417 logger.error("Null directory supplied"); 418 return; 419 } 420 if (!dir.exists()) { 421 logger.debug("No such directory: " + dir.getAbsolutePath()); 422 return; 423 } 424 if (!dir.isDirectory()) { 425 logger.warn("Not a directory: " + dir.getAbsolutePath()); 426 return; 427 } 428 429 // Load the jar files 430 File[] listJars = dir.listFiles(new JarFilenameFilter()); 431 if (listJars != null) { 432 for (File jar : listJars) { 433 this.jars.add(jar); 434 } 435 } 436 } 437 addAddon(AddOn ao)438 public void addAddon(AddOn ao) { 439 if (!ao.canLoadInCurrentVersion()) { 440 throw new IllegalArgumentException( 441 "Cant load add-on " 442 + ao.getName() 443 + " Not before=" 444 + ao.getNotBeforeVersion() 445 + " Not from=" 446 + ao.getNotFromVersion() 447 + " Version=" 448 + Constant.PROGRAM_VERSION); 449 } 450 451 installationLock.lock(); 452 try { 453 if (!this.aoc.addAddOn(ao)) { 454 return; 455 } 456 457 addAddOnImpl(ao); 458 } finally { 459 installationLock.unlock(); 460 } 461 } 462 addAddOnImpl(AddOn ao)463 private void addAddOnImpl(AddOn ao) { 464 if (AddOn.InstallationStatus.INSTALLED == ao.getInstallationStatus()) { 465 return; 466 } 467 468 if (this.blockList.contains(ao.getId())) { 469 // Explicitly being added back, so remove from the block list 470 this.blockList.remove(ao.getId()); 471 this.saveBlockList(); 472 } 473 474 if (!isDynamicallyInstallable(ao)) { 475 return; 476 } 477 478 if (!AddOnInstaller.installAddOnLibs(ao)) { 479 ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED); 480 return; 481 } 482 483 AddOnRunRequirements reqs = calculateRunRequirements(ao, aoc.getInstalledAddOns()); 484 if (!reqs.isRunnable()) { 485 ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED); 486 return; 487 } 488 489 AddOnInstaller.install(createAndAddAddOnClassLoader(ao), ao); 490 ao.setInstallationStatus(AddOn.InstallationStatus.INSTALLED); 491 Control.getSingleton().getExtensionLoader().addOnInstalled(ao); 492 493 if (runnableAddOns.get(ao) == null) { 494 runnableAddOns.put(ao, getRunnableExtensionsWithDeps(reqs)); 495 saveAddOnsRunState(runnableAddOns); 496 } 497 498 checkAndLoadDependentExtensions(); 499 checkAndInstallAddOnsNotInstalled(); 500 501 if (View.isInitialised()) { 502 EventQueue.invokeLater( 503 new Runnable() { 504 505 @Override 506 public void run() { 507 View.getSingleton().refreshTabViewMenus(); 508 } 509 }); 510 } 511 } 512 513 /** 514 * Checks and installs all the add-ons whose installation status is {@code NOT_INSTALLED} that 515 * have (now) all required dependencies fulfilled. 516 * 517 * <p>Should be called after an installation of an add-on. 518 * 519 * @see #addAddOnImpl(AddOn) 520 * @see AddOn.InstallationStatus#NOT_INSTALLED 521 * @since 2.4.0 522 */ checkAndInstallAddOnsNotInstalled()523 private void checkAndInstallAddOnsNotInstalled() { 524 List<AddOn> runnableAddOns = new ArrayList<>(); 525 for (AddOn addOn : aoc.getAddOns()) { 526 if (AddOn.InstallationStatus.NOT_INSTALLED == addOn.getInstallationStatus() 527 && addOnLoaders.get(addOn.getId()) == null) { 528 AddOnRunRequirements reqs = 529 addOn.calculateRunRequirements(aoc.getInstalledAddOns()); 530 if (reqs.isRunnable()) { 531 runnableAddOns.add(addOn); 532 } 533 } 534 } 535 536 for (AddOn addOn : runnableAddOns) { 537 addAddOnImpl(addOn); 538 } 539 } 540 541 /** 542 * Checks and loads all the extensions that have (now) all required dependencies fulfilled. 543 * 544 * <p>Should be called after an installation of an add-on. 545 * 546 * @see #addAddOnImpl(AddOn) 547 * @since 2.4.0 548 */ checkAndLoadDependentExtensions()549 private void checkAndLoadDependentExtensions() { 550 boolean changed = false; 551 for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) { 552 AddOn runningAddOn = aoc.getAddOn(entry.getKey()); 553 if (runningAddOn.getInstallationStatus() 554 == AddOn.InstallationStatus.UNINSTALLATION_FAILED) { 555 continue; 556 } 557 for (String extClassName : runningAddOn.getExtensionsWithDeps()) { 558 if (!runningAddOn.isExtensionLoaded(extClassName)) { 559 AddOn.AddOnRunRequirements reqs = 560 runningAddOn.calculateExtensionRunRequirements( 561 extClassName, aoc.getInstalledAddOns()); 562 ExtensionRunRequirements extReqs = reqs.getExtensionRequirements().get(0); 563 if (extReqs.isRunnable()) { 564 List<AddOnClassLoader> dependencies = 565 new ArrayList<>(extReqs.getDependencies().size()); 566 for (AddOn addOnDep : extReqs.getDependencies()) { 567 dependencies.add(addOnLoaders.get(addOnDep.getId())); 568 } 569 AddOnClassLoader extAddOnClassLoader = 570 new AddOnClassLoader( 571 entry.getValue(), 572 dependencies, 573 runningAddOn.getExtensionAddOnClassnames(extClassName)); 574 Extension ext = 575 loadAddOnExtension( 576 runningAddOn, extReqs.getClassname(), extAddOnClassLoader); 577 AddOnInstaller.installAddOnExtension(runningAddOn, ext); 578 runnableAddOns.get(runningAddOn).add(extReqs.getClassname()); 579 changed = true; 580 } 581 } 582 } 583 } 584 585 if (changed) { 586 saveAddOnsRunState(runnableAddOns); 587 } 588 } 589 590 /** 591 * Tells whether or not the given {@code addOn} is dynamically installable. 592 * 593 * <p>It checks if the given {@code addOn} is dynamically installable by calling the method 594 * {@code AddOn#hasZapAddOnEntry()}. 595 * 596 * @param addOn the add-on that will be checked 597 * @return {@code true} if the given add-on is dynamically installable, {@code false} otherwise. 598 * @see AddOn#hasZapAddOnEntry() 599 * @since 2.3.0 600 */ isDynamicallyInstallable(AddOn addOn)601 private static boolean isDynamicallyInstallable(AddOn addOn) { 602 return addOn.hasZapAddOnEntry(); 603 } 604 removeAddOn( AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback progressCallback)605 public boolean removeAddOn( 606 AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback progressCallback) { 607 installationLock.lock(); 608 try { 609 AddOnUninstallationProgressCallback callback = 610 (progressCallback == null) ? NULL_CALLBACK : progressCallback; 611 612 callback.uninstallingAddOn(ao, upgrading); 613 boolean removed = removeAddOnImpl(ao, upgrading, callback); 614 callback.addOnUninstalled(removed); 615 616 return removed; 617 } finally { 618 installationLock.unlock(); 619 } 620 } 621 removeAddOnImpl( AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback callback)622 private boolean removeAddOnImpl( 623 AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback callback) { 624 if (!isDynamicallyInstallable(ao)) { 625 return false; 626 } 627 628 if (AddOn.InstallationStatus.SOFT_UNINSTALLATION_FAILED == ao.getInstallationStatus()) { 629 if (runnableAddOns.remove(ao) != null) { 630 saveAddOnsRunState(runnableAddOns); 631 } 632 AddOnInstaller.uninstallAddOnFiles(ao, NULL_CALLBACK, runnableAddOns.keySet()); 633 removeAddOnClassLoader(ao); 634 deleteAddOn(ao, upgrading); 635 ao.setInstallationStatus(AddOn.InstallationStatus.UNINSTALLATION_FAILED); 636 Control.getSingleton().getExtensionLoader().addOnUninstalled(ao, false); 637 return false; 638 } 639 640 if (!this.aoc.includesAddOn(ao.getId())) { 641 logger.warn("Trying to uninstall an add-on that is not installed: " + ao.getId()); 642 return false; 643 } 644 645 if (AddOn.InstallationStatus.NOT_INSTALLED == ao.getInstallationStatus()) { 646 if (runnableAddOns.remove(ao) != null) { 647 saveAddOnsRunState(runnableAddOns); 648 } 649 650 deleteAddOn(ao, upgrading); 651 652 return this.aoc.removeAddOn(ao); 653 } 654 655 unloadDependentExtensions(ao); 656 softUninstallDependentAddOns(ao); 657 658 boolean uninstalledWithoutErrors = 659 AddOnInstaller.uninstall(ao, callback, runnableAddOns.keySet()); 660 661 if (uninstalledWithoutErrors && !this.aoc.removeAddOn(ao)) { 662 uninstalledWithoutErrors = false; 663 } 664 665 if (uninstalledWithoutErrors) { 666 removeAddOnClassLoader(ao); 667 } 668 669 deleteAddOn(ao, upgrading); 670 671 if (runnableAddOns.remove(ao) != null) { 672 saveAddOnsRunState(runnableAddOns); 673 } 674 675 ao.setInstallationStatus( 676 uninstalledWithoutErrors 677 ? AddOn.InstallationStatus.AVAILABLE 678 : AddOn.InstallationStatus.UNINSTALLATION_FAILED); 679 680 Control.getSingleton().getExtensionLoader().addOnUninstalled(ao, uninstalledWithoutErrors); 681 return uninstalledWithoutErrors; 682 } 683 684 /** 685 * Deletes the file and libraries of the given add-on. 686 * 687 * <p>The add-on is added to the {@link #blockList block list} when not able to delete it and if 688 * not updating it. 689 * 690 * @param addOn the add-on to be deleted. 691 * @param upgrading {@code true} if the add-on is being updated, {@code false} otherwise. 692 * @see AddOnInstaller#uninstallAddOnLibs(AddOn) 693 */ deleteAddOn(AddOn addOn, boolean upgrading)694 private void deleteAddOn(AddOn addOn, boolean upgrading) { 695 AddOnInstaller.uninstallAddOnLibs(addOn); 696 697 if (addOn.getFile() != null && addOn.getFile().exists()) { 698 if (!addOn.getFile().delete() && !upgrading) { 699 logger.debug("Cant delete " + addOn.getFile().getAbsolutePath()); 700 this.blockList.add(addOn.getId()); 701 this.saveBlockList(); 702 } 703 } 704 } 705 removeAddOnClassLoader(AddOn addOn)706 private void removeAddOnClassLoader(AddOn addOn) { 707 if (this.addOnLoaders.containsKey(addOn.getId())) { 708 try (AddOnClassLoader addOnClassLoader = this.addOnLoaders.remove(addOn.getId())) { 709 if (!addOn.getIdsAddOnDependencies().isEmpty()) { 710 addOnClassLoader.clearDependencies(); 711 } 712 ResourceBundle.clearCache(addOnClassLoader); 713 } catch (Exception e) { 714 logger.error( 715 "Failure while closing class loader of " + addOn.getId() + " add-on:", e); 716 } 717 addOn.setClassLoader(null); 718 } 719 } 720 unloadDependentExtensions(AddOn ao)721 private void unloadDependentExtensions(AddOn ao) { 722 boolean changed = false; 723 for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) { 724 AddOn runningAddOn = aoc.getAddOn(entry.getKey()); 725 for (Extension ext : runningAddOn.getLoadedExtensionsWithDeps()) { 726 if (runningAddOn.dependsOn(ext, ao)) { 727 String classname = ext.getClass().getCanonicalName(); 728 AddOnInstaller.uninstallAddOnExtension(runningAddOn, ext, NULL_CALLBACK); 729 try (AddOnClassLoader extensionClassLoader = 730 (AddOnClassLoader) ext.getClass().getClassLoader()) { 731 ext = null; 732 entry.getValue().removeChildClassLoader(extensionClassLoader); 733 extensionClassLoader.clearDependencies(); 734 ResourceBundle.clearCache(extensionClassLoader); 735 } catch (Exception e) { 736 logger.error( 737 "Failure while closing class loader of extension '" 738 + classname 739 + "':", 740 e); 741 } 742 runnableAddOns.get(runningAddOn).remove(classname); 743 changed = true; 744 } 745 } 746 } 747 748 if (changed) { 749 saveAddOnsRunState(runnableAddOns); 750 } 751 } 752 softUninstallDependentAddOns(AddOn ao)753 private void softUninstallDependentAddOns(AddOn ao) { 754 for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) { 755 AddOn runningAddOn = aoc.getAddOn(entry.getKey()); 756 if (runningAddOn.dependsOn(ao)) { 757 softUninstallDependentAddOns(runningAddOn); 758 759 softUninstall(runningAddOn); 760 } 761 } 762 } 763 softUninstall(AddOn addOn)764 private void softUninstall(AddOn addOn) { 765 if (AddOn.InstallationStatus.INSTALLED != addOn.getInstallationStatus()) { 766 return; 767 } 768 769 AddOn.InstallationStatus status; 770 if (isDynamicallyInstallable(addOn) && AddOnInstaller.softUninstall(addOn, NULL_CALLBACK)) { 771 removeAddOnClassLoader(addOn); 772 status = AddOn.InstallationStatus.NOT_INSTALLED; 773 } else { 774 status = AddOn.InstallationStatus.SOFT_UNINSTALLATION_FAILED; 775 } 776 777 addOn.setInstallationStatus(status); 778 Control.getSingleton() 779 .getExtensionLoader() 780 .addOnSoftUninstalled(addOn, status == AddOn.InstallationStatus.NOT_INSTALLED); 781 } 782 loadBlockList()783 private void loadBlockList() { 784 blockList = loadList(addOnsStateConfig, ADDONS_BLOCK_LIST); 785 } 786 saveBlockList()787 private void saveBlockList() { 788 saveList(addOnsStateConfig, ADDONS_BLOCK_LIST, this.blockList); 789 } 790 getClassNames(String packageName, Class<T> classType)791 private <T> List<ClassNameWrapper> getClassNames(String packageName, Class<T> classType) { 792 List<ClassNameWrapper> listClassName = new ArrayList<>(); 793 794 listClassName.addAll(this.getLocalClassNames(packageName)); 795 for (String addOnId : this.addOnLoaders.keySet()) { 796 listClassName.addAll(this.getJarClassNames(aoc.getAddOn(addOnId), packageName)); 797 } 798 for (File jar : jars) { 799 listClassName.addAll( 800 this.getJarClassNames(this.getClass().getClassLoader(), jar, packageName)); 801 } 802 return listClassName; 803 } 804 805 /** 806 * Returns all the {@code Extension}s of all the installed add-ons. 807 * 808 * <p>The discovery of {@code Extension}s is done by resorting to the {@link 809 * AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-ons. 810 * 811 * <p>Extensions with unfulfilled dependencies are not be returned. 812 * 813 * @return a list containing all {@code Extension}s of all installed add-ons 814 * @since 2.4.0 815 * @see Extension 816 * @see #getExtensions(AddOn) 817 */ getExtensions()818 public List<Extension> getExtensions() { 819 List<Extension> list = new ArrayList<>(); 820 for (AddOn addOn : getAddOnCollection().getAddOns()) { 821 list.addAll(getExtensions(addOn)); 822 } 823 824 return list; 825 } 826 827 /** 828 * Returns all {@code Extension}s of the given {@code addOn}. 829 * 830 * <p>The discovery of {@code Extension}s is done by resorting to {@link 831 * AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-on. 832 * 833 * <p>Extensions with unfulfilled dependencies are not be returned. 834 * 835 * <p><strong>Note:</strong> If the add-on is not installed the method returns an empty list. 836 * 837 * @param addOn the add-on whose extensions will be returned 838 * @return a list containing the {@code Extension}s of the given {@code addOn} 839 * @since 2.4.0 840 * @see Extension 841 * @see #getExtensions() 842 */ getExtensions(AddOn addOn)843 public List<Extension> getExtensions(AddOn addOn) { 844 AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId()); 845 if (addOnClassLoader == null) { 846 return Collections.emptyList(); 847 } 848 849 List<Extension> extensions = new ArrayList<>(); 850 extensions.addAll(loadAddOnExtensions(addOn, addOn.getExtensions(), addOnClassLoader)); 851 852 if (addOn.hasExtensionsWithDeps()) { 853 AddOn.AddOnRunRequirements reqs = 854 addOn.calculateRunRequirements(aoc.getInstalledAddOns()); 855 for (ExtensionRunRequirements extReqs : reqs.getExtensionRequirements()) { 856 if (extReqs.isRunnable()) { 857 List<AddOnClassLoader> dependencies = 858 new ArrayList<>(extReqs.getDependencies().size()); 859 for (AddOn addOnDep : extReqs.getDependencies()) { 860 dependencies.add(addOnLoaders.get(addOnDep.getId())); 861 } 862 AddOnClassLoader extAddOnClassLoader = 863 new AddOnClassLoader( 864 addOnClassLoader, 865 dependencies, 866 addOn.getExtensionAddOnClassnames(extReqs.getClassname())); 867 Extension ext = 868 loadAddOnExtension(addOn, extReqs.getClassname(), extAddOnClassLoader); 869 if (ext != null) { 870 extensions.add(ext); 871 } 872 } else if (logger.isDebugEnabled()) { 873 logger.debug( 874 "Can't run extension '" 875 + extReqs.getClassname() 876 + "' of add-on '" 877 + addOn.getName() 878 + "' because of missing requirements: " 879 + AddOnRunIssuesUtils.getRunningIssues(extReqs)); 880 } 881 } 882 } 883 return extensions; 884 } 885 loadAddOnExtensions( AddOn addOn, List<String> extensions, AddOnClassLoader addOnClassLoader)886 private List<Extension> loadAddOnExtensions( 887 AddOn addOn, List<String> extensions, AddOnClassLoader addOnClassLoader) { 888 if (extensions == null || extensions.isEmpty()) { 889 return Collections.emptyList(); 890 } 891 892 List<Extension> list = new ArrayList<>(extensions.size()); 893 for (String extName : extensions) { 894 Extension ext = loadAddOnExtension(addOn, extName, addOnClassLoader); 895 if (ext != null) { 896 list.add(ext); 897 } 898 } 899 return list; 900 } 901 loadAddOnExtension( AddOn addOn, String classname, AddOnClassLoader addOnClassLoader)902 private static Extension loadAddOnExtension( 903 AddOn addOn, String classname, AddOnClassLoader addOnClassLoader) { 904 Extension extension = 905 AddOnLoaderUtils.loadAndInstantiateClass( 906 addOnClassLoader, classname, Extension.class, "extension"); 907 if (extension != null) { 908 addOn.addLoadedExtension(extension); 909 } 910 return extension; 911 } 912 913 /** 914 * Gets the active scan rules of all the loaded add-ons. 915 * 916 * <p>The discovery of active scan rules is done by resorting to {@link AddOn#MANIFEST_FILE_NAME 917 * manifest file} bundled in the add-ons. 918 * 919 * @return an unmodifiable {@code List} with all the active scan rules, never {@code null} 920 * @since 2.4.0 921 * @see AbstractPlugin 922 */ getActiveScanRules()923 public List<AbstractPlugin> getActiveScanRules() { 924 ArrayList<AbstractPlugin> list = new ArrayList<>(); 925 for (AddOn addOn : getAddOnCollection().getAddOns()) { 926 AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId()); 927 if (addOnClassLoader != null) { 928 list.addAll(AddOnLoaderUtils.getActiveScanRules(addOn, addOnClassLoader)); 929 } 930 } 931 list.trimToSize(); 932 return Collections.unmodifiableList(list); 933 } 934 935 /** 936 * Gets the passive scan rules of all the loaded add-ons. 937 * 938 * <p>The discovery of passive scan rules is done by resorting to {@link 939 * AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-ons. 940 * 941 * @return an unmodifiable {@code List} with all the passive scan rules, never {@code null} 942 * @since 2.4.0 943 * @see PluginPassiveScanner 944 */ getPassiveScanRules()945 public List<PluginPassiveScanner> getPassiveScanRules() { 946 ArrayList<PluginPassiveScanner> list = new ArrayList<>(); 947 for (AddOn addOn : getAddOnCollection().getAddOns()) { 948 AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId()); 949 if (addOnClassLoader != null) { 950 list.addAll(AddOnLoaderUtils.getPassiveScanRules(addOn, addOnClassLoader)); 951 } 952 } 953 list.trimToSize(); 954 return Collections.unmodifiableList(list); 955 } 956 957 /** 958 * Gets a list of classes that implement the given type in the given package. 959 * 960 * <p>It searches in the dependencies, add-ons, and the ZAP JAR. 961 * 962 * @param packageName the name of the package that the classes must be in. 963 * @param classType the type of the classes. 964 * @return a list with the classes that implement the given type, never {@code null}. 965 * @deprecated (2.8.0) The use of this method is discouraged (specially during ZAP startup, as 966 * it's delayed), it's preferable to provide means to register/declare the required classes 967 * instead of searching "everywhere". 968 */ 969 @Deprecated getImplementors(String packageName, Class<T> classType)970 public <T> List<T> getImplementors(String packageName, Class<T> classType) { 971 return this.getImplementors(null, packageName, classType); 972 } 973 974 /** 975 * Gets a list of classes that implement the given type in the given package. 976 * 977 * <p>It searches in the given add-on, if not {@code null}, otherwise it searches in the 978 * dependencies, add-ons, and the ZAP JAR. 979 * 980 * @param ao the add-on to search in, might be {@code null}. 981 * @param packageName the name of the package that the classes must be in. 982 * @param classType the type of the classes. 983 * @return a list with the classes that implement the given type, never {@code null}. 984 * @deprecated (2.8.0) The use of this method is discouraged (specially during ZAP startup, as 985 * it's delayed), it's preferable to provide means to register/declare the required classes 986 * instead of searching "everywhere". 987 */ 988 @Deprecated getImplementors(AddOn ao, String packageName, Class<T> classType)989 public <T> List<T> getImplementors(AddOn ao, String packageName, Class<T> classType) { 990 Class<?> cls = null; 991 List<T> listClass = new ArrayList<>(); 992 993 List<ClassNameWrapper> classNames; 994 if (ao != null) { 995 classNames = this.getJarClassNames(ao, packageName); 996 } else { 997 classNames = this.getClassNames(packageName, classType); 998 } 999 for (ClassNameWrapper classWrapper : classNames) { 1000 try { 1001 cls = classWrapper.getCl().loadClass(classWrapper.getClassName()); 1002 // abstract class or interface cannot be constructed. 1003 if (Modifier.isAbstract(cls.getModifiers()) 1004 || Modifier.isInterface(cls.getModifiers())) { 1005 continue; 1006 } 1007 if (classType.isAssignableFrom(cls)) { 1008 @SuppressWarnings("unchecked") 1009 Constructor<T> c = (Constructor<T>) cls.getConstructor(); 1010 listClass.add(c.newInstance()); 1011 } 1012 } catch (Throwable e) { 1013 // Often not an error 1014 logger.debug(e.getMessage(), e); 1015 } 1016 } 1017 return listClass; 1018 } 1019 1020 /** 1021 * Check local jar (zap.jar) or related package if any target file is found. 1022 * 1023 * @param packageName the package name that the class must belong too 1024 * @return a {@code List} with all the classes belonging to the given package 1025 */ getLocalClassNames(String packageName)1026 private List<ClassNameWrapper> getLocalClassNames(String packageName) { 1027 1028 if (packageName == null || packageName.equals("")) { 1029 return Collections.emptyList(); 1030 } 1031 1032 String folder = packageName.replace('.', '/'); 1033 URL local = AddOnLoader.class.getClassLoader().getResource(folder); 1034 if (local == null) { 1035 return Collections.emptyList(); 1036 } 1037 String jarFile = null; 1038 if (local.getProtocol().equals("jar")) { 1039 jarFile = local.toString().substring("jar:".length()); 1040 int pos = jarFile.indexOf("!"); 1041 jarFile = jarFile.substring(0, pos); 1042 1043 try { 1044 // ZAP: Changed to take into account the package name 1045 return getJarClassNames( 1046 this.getClass().getClassLoader(), new File(new URI(jarFile)), packageName); 1047 } catch (URISyntaxException e) { 1048 logger.error(e.getMessage(), e); 1049 } 1050 } else { 1051 try { 1052 // ZAP: Changed to pass a FileFilter (ClassRecurseDirFileFilter) 1053 // and to pass the "packageName" with the dots already replaced. 1054 return parseClassDir( 1055 this.getClass().getClassLoader(), 1056 new File(new URI(local.toString())), 1057 packageName.replace('.', File.separatorChar), 1058 new ClassRecurseDirFileFilter(true)); 1059 } catch (URISyntaxException e) { 1060 logger.error(e.getMessage(), e); 1061 } 1062 } 1063 return Collections.emptyList(); 1064 } 1065 1066 // ZAP: Changed to use only one FileFilter and the packageName is already 1067 // passed with the dots replaced. parseClassDir( ClassLoader cl, File file, String packageName, FileFilter fileFilter)1068 private List<ClassNameWrapper> parseClassDir( 1069 ClassLoader cl, File file, String packageName, FileFilter fileFilter) { 1070 List<ClassNameWrapper> classNames = new ArrayList<>(); 1071 File[] listFile = file.listFiles(fileFilter); 1072 1073 for (File entry : listFile) { 1074 if (entry.isDirectory()) { 1075 classNames.addAll(parseClassDir(cl, entry, packageName, fileFilter)); 1076 continue; 1077 } 1078 String fileName = entry.toString(); 1079 int pos = fileName.indexOf(packageName); 1080 if (pos > 0) { 1081 String className = 1082 fileName.substring(pos) 1083 .replaceAll("\\.class$", "") 1084 .replace(File.separatorChar, '.'); 1085 classNames.add(new ClassNameWrapper(cl, className)); 1086 } 1087 } 1088 return classNames; 1089 } 1090 1091 // ZAP: Added to take into account the package name getJarClassNames(ClassLoader cl, File file, String packageName)1092 private List<ClassNameWrapper> getJarClassNames(ClassLoader cl, File file, String packageName) { 1093 List<ClassNameWrapper> classNames = new ArrayList<>(); 1094 ZipEntry entry = null; 1095 String className = ""; 1096 try (JarFile jarFile = new JarFile(file)) { 1097 Enumeration<JarEntry> entries = jarFile.entries(); 1098 while (entries.hasMoreElements()) { 1099 entry = entries.nextElement(); 1100 if (entry.isDirectory() || !entry.getName().endsWith(".class")) { 1101 continue; 1102 } 1103 className = entry.toString().replaceAll("\\.class$", "").replaceAll("/", "."); 1104 if (className.indexOf(packageName) >= 0) { 1105 classNames.add(new ClassNameWrapper(cl, className)); 1106 } 1107 } 1108 } catch (Exception e) { 1109 logger.error("Failed to open file: " + file.getAbsolutePath(), e); 1110 } 1111 return classNames; 1112 } 1113 getJarClassNames(AddOn ao, String packageName)1114 private List<ClassNameWrapper> getJarClassNames(AddOn ao, String packageName) { 1115 List<ClassNameWrapper> classNames = new ArrayList<>(); 1116 ZipEntry entry = null; 1117 String className = ""; 1118 try (JarFile jarFile = new JarFile(ao.getFile())) { 1119 Enumeration<JarEntry> entries = jarFile.entries(); 1120 while (entries.hasMoreElements()) { 1121 entry = entries.nextElement(); 1122 if (entry.isDirectory() || !entry.getName().endsWith(".class")) { 1123 continue; 1124 } 1125 className = entry.toString().replaceAll("\\.class$", "").replaceAll("/", "."); 1126 if (className.indexOf(packageName) >= 0) { 1127 classNames.add( 1128 new ClassNameWrapper(this.addOnLoaders.get(ao.getId()), className)); 1129 } 1130 } 1131 } catch (Exception e) { 1132 logger.error("Failed to open file: " + ao.getFile().getAbsolutePath(), e); 1133 } 1134 return classNames; 1135 } 1136 1137 private static final class JarFilenameFilter implements FilenameFilter { 1138 @Override accept(File dir, String fileName)1139 public boolean accept(File dir, String fileName) { 1140 if (fileName.endsWith(".jar")) { 1141 return true; 1142 } 1143 return false; 1144 } 1145 } 1146 1147 // ZAP: Added 1148 private static final class ClassRecurseDirFileFilter implements FileFilter { 1149 1150 private boolean recurse; 1151 ClassRecurseDirFileFilter(boolean recurse)1152 public ClassRecurseDirFileFilter(boolean recurse) { 1153 this.recurse = recurse; 1154 } 1155 1156 @Override accept(File file)1157 public boolean accept(File file) { 1158 if (recurse && file.isDirectory() && !file.getName().startsWith(".")) { 1159 return true; 1160 } else if (file.isFile() && file.getName().endsWith(".class")) { 1161 return true; 1162 } 1163 1164 return false; 1165 } 1166 } 1167 1168 private class ClassNameWrapper { 1169 private ClassLoader cl; 1170 private String className; 1171 ClassNameWrapper(ClassLoader cl, String className)1172 public ClassNameWrapper(ClassLoader cl, String className) { 1173 super(); 1174 this.cl = cl; 1175 this.className = className; 1176 } 1177 getCl()1178 public ClassLoader getCl() { 1179 return cl; 1180 } 1181 getClassName()1182 public String getClassName() { 1183 return className; 1184 } 1185 } 1186 loadList(Configuration config, String key)1187 private static List<String> loadList(Configuration config, String key) { 1188 List<String> data = new ArrayList<>(); 1189 String blockStr = config.getString(key, null); 1190 if (blockStr != null && blockStr.length() > 0) { 1191 for (String str : blockStr.split(",")) { 1192 data.add(str); 1193 } 1194 } 1195 return data; 1196 } 1197 saveList(FileConfiguration config, String key, List<String> list)1198 private static void saveList(FileConfiguration config, String key, List<String> list) { 1199 StringBuilder sb = new StringBuilder(); 1200 1201 for (String id : list) { 1202 if (sb.length() > 0) { 1203 sb.append(','); 1204 } 1205 sb.append(id); 1206 } 1207 1208 config.setProperty(key, sb.toString()); 1209 try { 1210 config.save(); 1211 } catch (ConfigurationException e) { 1212 logger.error("Failed to save list [" + key + "]: " + sb.toString(), e); 1213 } 1214 } 1215 loadAddOnsRunState( HierarchicalConfiguration config, AddOnCollection addOnCollection)1216 private static Map<AddOn, AddOnRunState> loadAddOnsRunState( 1217 HierarchicalConfiguration config, AddOnCollection addOnCollection) { 1218 List<HierarchicalConfiguration> savedAddOns = config.configurationsAt(ADDONS_RUNNABLE_KEY); 1219 1220 Map<AddOn, AddOnRunState> runnableAddOns = new HashMap<>(); 1221 for (HierarchicalConfiguration savedAddOn : savedAddOns) { 1222 AddOn addOn = addOnCollection.getAddOn(savedAddOn.getString(ADDON_RUNNABLE_ID_KEY, "")); 1223 if (addOn == null) { 1224 // No longer exists, skip it. 1225 continue; 1226 } 1227 String version = savedAddOn.getString(ADDON_RUNNABLE_FULL_VERSION_KEY, ""); 1228 if (version.isEmpty()) { 1229 // Try read the old version, which was an integer. 1230 version = savedAddOn.getString(ADDON_RUNNABLE_VERSION_KEY, ""); 1231 } 1232 if (version.isEmpty()) { 1233 // No version, skip it. 1234 continue; 1235 } 1236 1237 int result = 1238 addOn.getVersion().compareTo(createLegacyVersion(version, addOn.getName())); 1239 if (result != 0) { 1240 if (result > 1) { 1241 runnableAddOns.put(addOn, new AddOnRunState()); 1242 } 1243 // Different version, nothing more to do. 1244 continue; 1245 } 1246 1247 List<String> runnableExtensions = new ArrayList<>(); 1248 List<String> currentExtensions = addOn.getExtensionsWithDeps(); 1249 for (String savedExtension : 1250 savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) { 1251 if (currentExtensions.contains(savedExtension)) { 1252 runnableExtensions.add(savedExtension); 1253 } 1254 } 1255 runnableAddOns.put(addOn, new AddOnRunState(runnableExtensions)); 1256 } 1257 1258 return runnableAddOns; 1259 } 1260 createLegacyVersion(String version, String addOnName)1261 private static Version createLegacyVersion(String version, String addOnName) { 1262 try { 1263 return new Version(version); 1264 } catch (IllegalArgumentException e) { 1265 if (logger.isDebugEnabled()) { 1266 logger.debug( 1267 "Failed to create (legacy?) version with [" 1268 + version 1269 + "] for runnable add-on [" 1270 + addOnName 1271 + "]", 1272 e); 1273 } 1274 } 1275 1276 try { 1277 return new Version(version + ".0.0"); 1278 } catch (IllegalArgumentException e) { 1279 if (logger.isDebugEnabled()) { 1280 logger.debug( 1281 "Failed to create legacy version with [" 1282 + version 1283 + ".0.0] for runnable add-on [" 1284 + addOnName 1285 + "]", 1286 e); 1287 } 1288 } 1289 1290 return null; 1291 } 1292 saveAddOnsRunState(Map<AddOn, List<String>> runnableAddOns)1293 private void saveAddOnsRunState(Map<AddOn, List<String>> runnableAddOns) { 1294 addOnsStateConfig.clearTree(ADDONS_RUNNABLE_BASE_KEY); 1295 1296 int i = 0; 1297 for (Map.Entry<AddOn, List<String>> runnableAddOnEntry : runnableAddOns.entrySet()) { 1298 String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ")."; 1299 AddOn addOn = runnableAddOnEntry.getKey(); 1300 1301 addOnsStateConfig.setProperty(elementBaseKey + ADDON_RUNNABLE_ID_KEY, addOn.getId()); 1302 addOnsStateConfig.setProperty( 1303 elementBaseKey + ADDON_RUNNABLE_FULL_VERSION_KEY, addOn.getVersion()); 1304 // For older ZAP versions, which can't read the semantic version, just an integer. 1305 addOnsStateConfig.setProperty( 1306 elementBaseKey + ADDON_RUNNABLE_VERSION_KEY, 1307 addOn.getVersion().getMajorVersion()); 1308 1309 String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY; 1310 for (String extension : runnableAddOnEntry.getValue()) { 1311 addOnsStateConfig.addProperty(extensionBaseKey, extension); 1312 } 1313 1314 i++; 1315 } 1316 1317 try { 1318 addOnsStateConfig.save(); 1319 } catch (ConfigurationException e) { 1320 logger.error("Failed to save state of runnable add-ons:", e); 1321 } 1322 } 1323 migrateOldAddOnsState(ZapXmlConfiguration newConfig)1324 private static boolean migrateOldAddOnsState(ZapXmlConfiguration newConfig) { 1325 boolean dataMigrated = false; 1326 HierarchicalConfiguration oldConfig = 1327 (HierarchicalConfiguration) Model.getSingleton().getOptionsParam().getConfig(); 1328 1329 if (oldConfig.containsKey(ADDONS_BLOCK_LIST)) { 1330 List<String> blockList = loadList(oldConfig, ADDONS_BLOCK_LIST); 1331 oldConfig.clearProperty(ADDONS_BLOCK_LIST); 1332 saveList(newConfig, ADDONS_BLOCK_LIST, blockList); 1333 dataMigrated = true; 1334 } 1335 1336 List<HierarchicalConfiguration> oldAddOnsState = 1337 oldConfig.configurationsAt(ADDONS_RUNNABLE_KEY); 1338 if (!oldAddOnsState.isEmpty()) { 1339 int i = 0; 1340 for (HierarchicalConfiguration savedAddOn : oldAddOnsState) { 1341 String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ")."; 1342 newConfig.setProperty( 1343 elementBaseKey + ADDON_RUNNABLE_ID_KEY, 1344 savedAddOn.getString(ADDON_RUNNABLE_ID_KEY, "")); 1345 String version = savedAddOn.getString(ADDON_RUNNABLE_FULL_VERSION_KEY, ""); 1346 if (version.isEmpty()) { 1347 newConfig.setProperty( 1348 elementBaseKey + ADDON_RUNNABLE_VERSION_KEY, 1349 savedAddOn.getString(ADDON_RUNNABLE_VERSION_KEY, "")); 1350 } else { 1351 newConfig.setProperty( 1352 elementBaseKey + ADDON_RUNNABLE_FULL_VERSION_KEY, version); 1353 } 1354 1355 String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY; 1356 for (String extension : 1357 savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) { 1358 newConfig.addProperty(extensionBaseKey, extension); 1359 } 1360 } 1361 oldConfig.clearTree(ADDONS_RUNNABLE_KEY); 1362 dataMigrated = true; 1363 } 1364 return dataMigrated; 1365 } 1366 1367 /** 1368 * An {@code UninstallationProgressCallback} that does nothing. A "{@code null}" object, for use 1369 * when no callback is given during the uninstallation process. 1370 */ 1371 private static class NullUninstallationProgressCallBack 1372 implements AddOnUninstallationProgressCallback { 1373 1374 @Override uninstallingAddOn(AddOn addOn, boolean updating)1375 public void uninstallingAddOn(AddOn addOn, boolean updating) {} 1376 1377 @Override activeScanRulesWillBeRemoved(int numberOfRules)1378 public void activeScanRulesWillBeRemoved(int numberOfRules) {} 1379 1380 @Override activeScanRuleRemoved(String name)1381 public void activeScanRuleRemoved(String name) {} 1382 1383 @Override passiveScanRulesWillBeRemoved(int numberOfRules)1384 public void passiveScanRulesWillBeRemoved(int numberOfRules) {} 1385 1386 @Override passiveScanRuleRemoved(String name)1387 public void passiveScanRuleRemoved(String name) {} 1388 1389 @Override filesWillBeRemoved(int numberOfFiles)1390 public void filesWillBeRemoved(int numberOfFiles) {} 1391 1392 @Override fileRemoved()1393 public void fileRemoved() {} 1394 1395 @Override extensionsWillBeRemoved(int numberOfExtensions)1396 public void extensionsWillBeRemoved(int numberOfExtensions) {} 1397 1398 @Override extensionRemoved(String name)1399 public void extensionRemoved(String name) {} 1400 1401 @Override addOnUninstalled(boolean uninstalled)1402 public void addOnUninstalled(boolean uninstalled) {} 1403 } 1404 1405 private static class AddOnRunState { 1406 1407 private final boolean newerVersion; 1408 private final List<String> extensions; 1409 AddOnRunState()1410 public AddOnRunState() { 1411 this.newerVersion = true; 1412 this.extensions = Collections.emptyList(); 1413 } 1414 AddOnRunState(List<String> extensions)1415 public AddOnRunState(List<String> extensions) { 1416 this.newerVersion = false; 1417 this.extensions = extensions; 1418 } 1419 hasNewerVersion()1420 public boolean hasNewerVersion() { 1421 return newerVersion; 1422 } 1423 getExtensions()1424 public List<String> getExtensions() { 1425 return extensions; 1426 } 1427 } 1428 } 1429