Adding the ability to reload plugins directly from the interface instead of restarting Tomcat

This commit is contained in:
Nanne Baars
2015-09-22 20:38:19 +02:00
parent 901eff682f
commit 8d2771c108
18 changed files with 176 additions and 159 deletions

View File

@ -1032,4 +1032,5 @@ public abstract class AbstractLesson extends Screen implements Comparable<Object
return new File(w.getContext().getRealPath("/plugin_extracted/plugin/" + getLessonName() + "/"));
}
}

View File

@ -2,12 +2,12 @@ package org.owasp.webgoat.plugins;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import org.owasp.webgoat.classloader.PluginClassLoader;
import org.owasp.webgoat.lessons.AbstractLesson;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
@ -42,10 +42,11 @@ public class Plugin {
private void findLesson(String name) {
String realClassName = StringUtils.trimLeadingCharacter(name, '/').replaceAll("/", ".").replaceAll(".class", "");
PluginClassLoader cl = (PluginClassLoader) Thread.currentThread().getContextClassLoader();
//TODO should be passed in (refactor)
URLClassLoader cl = (URLClassLoader) Thread.currentThread().getContextClassLoader();
try {
Class clazz = cl.loadClass(realClassName, true);
Class clazz = cl.loadClass(realClassName);
if (AbstractLesson.class.isAssignableFrom(clazz)) {
this.lesson = clazz;
@ -55,7 +56,7 @@ public class Plugin {
}
}
public void loadFiles(Path file, boolean reload) {
public void loadFiles(Path file) {
if (fileEndsWith(file, ".html") && hasParentDirectoryWithName(file, NAME_LESSON_SOLUTION_DIRECTORY)) {
solutionLanguageFiles.put(file.getParent().getFileName().toString(), file.toFile());
}

View File

@ -3,10 +3,8 @@ package org.owasp.webgoat.plugins;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.nio.file.Paths;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@WebListener
/**
@ -25,7 +23,7 @@ public class PluginBackgroundLoader implements ServletContextListener {
String targetPath = event.getServletContext().getRealPath("plugin_extracted");
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(new PluginsLoader(Paths.get(pluginPath), Paths.get(targetPath)), 10, 5, TimeUnit.MINUTES);
//scheduler.scheduleAtFixedRate(new PluginsLoader(Paths.get(pluginPath), Paths.get(targetPath)), 10, 5, TimeUnit.MINUTES);
}
/** {@inheritDoc} */

View File

@ -55,7 +55,7 @@ public class PluginExtractor {
throws IOException {
final File targetFile = new File(targetDirectory, zipEntry.getName());
copyFile(zipFile, zipEntry, targetFile, false);
plugin.loadFiles(targetFile.toPath(), true);
plugin.loadFiles(targetFile.toPath());
}
private boolean processPropertyFile(ZipFile zipFile, ZipEntry zipEntry, File targetDirectory)

View File

@ -1,9 +1,9 @@
package org.owasp.webgoat.plugins;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.commons.io.FileUtils;
import org.owasp.webgoat.classloader.PluginClassLoader;
import org.owasp.webgoat.plugins.classloader.PluginClassLoaderFactory;
import org.owasp.webgoat.plugins.classloader.PluginClassLoaderRepository;
import org.owasp.webgoat.util.LabelProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -11,12 +11,14 @@ import org.springframework.util.ResourceUtils;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
@ -30,25 +32,22 @@ import java.util.concurrent.Executors;
*/
public class PluginsLoader implements Runnable {
/** Constant <code>WEBGOAT_PLUGIN_EXTENSION="jar"</code> */
protected static final String WEBGOAT_PLUGIN_EXTENSION = "jar";
private static final String WEBGOAT_PLUGIN_EXTENSION = "jar";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Path pluginSource;
private final PluginClassLoaderRepository repository;
private Path pluginTarget;
/**
* <p>Constructor for PluginsLoader.</p>
*
* @param pluginSource a {@link java.nio.file.Path} object.
* @param pluginTarget a {@link java.nio.file.Path} object.
*/
public PluginsLoader(Path pluginSource, Path pluginTarget) {
Preconditions.checkNotNull(pluginSource, "plugin source cannot be null");
Preconditions.checkNotNull(pluginTarget, "plugin target cannot be null");
this.pluginSource = pluginSource;
this.pluginTarget = pluginTarget;
public PluginsLoader(PluginClassLoaderRepository repository, Path pluginSource, Path pluginTarget) {
this.pluginSource = Objects.requireNonNull(pluginSource, "plugin source cannot be null");
this.pluginTarget = Objects.requireNonNull(pluginTarget, "plugin target cannot be null");
this.repository = Objects.requireNonNull(repository, "repository cannot be null");
}
/**
@ -58,21 +57,26 @@ public class PluginsLoader implements Runnable {
* @return a {@link java.util.List} object.
*/
public List<Plugin> loadPlugins(final boolean reload) {
final PluginClassLoader cl = (PluginClassLoader) Thread.currentThread().getContextClassLoader();
List<Plugin> plugins = Lists.newArrayList();
try {
PluginFileUtils.createDirsIfNotExists(pluginTarget);
cleanupExtractedPluginsDirectory();
List<URL> jars = listJars();
cl.addURL(jars);
plugins = processPlugins(jars, reload);
initClassLoader(jars);
plugins = processPlugins(jars);
} catch (Exception e) {
logger.error("Loading plugins failed", e);
}
return plugins;
}
private void initClassLoader(List<URL> jars) {
URLClassLoader classLoader = PluginClassLoaderFactory.createClassLoader(jars);
this.repository.replaceClassLoader(classLoader);
Thread.currentThread().setContextClassLoader(classLoader);
}
private void cleanupExtractedPluginsDirectory() {
Path i18nDirectory = pluginTarget.resolve("plugin/i18n/");
FileUtils.deleteQuietly(i18nDirectory.toFile());
@ -93,7 +97,7 @@ public class PluginsLoader implements Runnable {
return jars;
}
private List<Plugin> processPlugins(List<URL> jars, boolean reload) throws Exception {
private List<Plugin> processPlugins(List<URL> jars) throws Exception {
final List<Plugin> plugins = Lists.newArrayList();
final ExecutorService executorService = Executors.newFixedThreadPool(10);
final CompletionService<Plugin> completionService = new ExecutorCompletionService<>(executorService);

View File

@ -0,0 +1,31 @@
package org.owasp.webgoat.plugins.classloader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
/**
* Create a classloader for the plugins
*/
public class PluginClassLoaderFactory {
private static final Logger logger = LoggerFactory.getLogger(PluginClassLoaderFactory.class);
public static URLClassLoader createClassLoader(List<URL> urls) {
return new URLClassLoader(urls.toArray(new URL[urls.size()]), determineParentClassLoader());
}
private static ClassLoader determineParentClassLoader() {
ClassLoader parent = Thread.currentThread().getContextClassLoader();
try {
parent = Thread.currentThread().getContextClassLoader().getParent()
.loadClass("org.apache.jasper.runtime.JspContextWrapper").getClassLoader();
} catch (ClassNotFoundException e) {
logger.info("Tomcat JspContextWrapper not found, probably not running on Tomcat...");
}
return parent;
}
}

View File

@ -0,0 +1,35 @@
package org.owasp.webgoat.plugins.classloader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLClassLoader;
/**
* Holds the classloaders for the plugins. For now all the plugins are loaded by the same
* classloader. This class can be extended to contain a classloader per plugin.
*/
public class PluginClassLoaderRepository {
private static final Logger logger = LoggerFactory.getLogger(PluginClassLoaderRepository.class);
private URLClassLoader currentPluginLoader;
/**
* @return the plugin classloader
*/
public URLClassLoader get() {
return currentPluginLoader;
}
public void replaceClassLoader(URLClassLoader classLoader) {
if (this.currentPluginLoader != null) {
try {
this.currentPluginLoader.close();
} catch (IOException e) {
logger.warn("Unable to close the current classloader", e);
}
}
this.currentPluginLoader = classLoader;
}
}

View File

@ -0,0 +1,67 @@
/**
* *************************************************************************************************
*
*
* This file is part of WebGoat, an Open Web Application Security Project
* utility. For details, please see http://www.owasp.org/
*
* Copyright (c) 2002 - 20014 Bruce Mayhew
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 59 Temple
* Place - Suite 330, Boston, MA 02111-1307, USA.
*
* Getting Source ==============
*
* Source for this application is maintained at
* https://github.com/WebGoat/WebGoat, a repository for free software projects.
*
* For details, please see http://webgoat.github.io
*/
package org.owasp.webgoat.service;
import org.owasp.webgoat.session.WebSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
/**
* <p>PluginReloadService class.</p>
*
* @author nbaars
* @version $Id: $Id
*/
@Controller
public class PluginReloadService extends BaseService {
private static final Logger logger = LoggerFactory.getLogger(PluginReloadService.class);
/**
* Reload all the plugins
*
* @param session a {@link HttpSession} object.
*/
@RequestMapping(value = "/reloadplugins.mvc")
public @ResponseBody
ResponseEntity<Object> reloadPlugins(HttpSession session) {
WebSession webSession = (WebSession) session.getAttribute(WebSession.SESSION);
webSession.getCourse().loadLessonFromPlugin(session.getServletContext());
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@ -5,6 +5,7 @@ import org.owasp.webgoat.lessons.AbstractLesson;
import org.owasp.webgoat.lessons.Category;
import org.owasp.webgoat.plugins.Plugin;
import org.owasp.webgoat.plugins.PluginsLoader;
import org.owasp.webgoat.plugins.classloader.PluginClassLoaderRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -65,14 +66,13 @@ public class Course {
final Logger logger = LoggerFactory.getLogger(Course.class);
private final PluginClassLoaderRepository repository = new PluginClassLoaderRepository();
private final List<AbstractLesson> lessons = new LinkedList<AbstractLesson>();
private final static String PROPERTIES_FILENAME = HammerHead.propertiesPath;
private WebgoatProperties properties = null;
private final List<String> files = new LinkedList<String>();
private WebgoatContext webgoatContext;
/**
@ -327,7 +327,7 @@ public class Course {
return null;
}
private void loadLessonFromPlugin(ServletContext context) {
public void loadLessonFromPlugin(ServletContext context) {
logger.debug("Loading plugins into cache");
String pluginPath = context.getRealPath("plugin_lessons");
String targetPath = context.getRealPath("plugin_extracted");
@ -336,8 +336,8 @@ public class Course {
logger.error("Plugins directory {} not found", pluginPath);
return;
}
List<Plugin> plugins = new PluginsLoader(Paths.get(pluginPath), Paths.get(targetPath)).loadPlugins(true);
lessons.clear();
List<Plugin> plugins = new PluginsLoader(repository, Paths.get(pluginPath), Paths.get(targetPath)).loadPlugins(true);
for (Plugin plugin : plugins) {
try {
AbstractLesson lesson = plugin.getLesson().get();
@ -376,5 +376,4 @@ public class Course {
LegacyLoader loader = new LegacyLoader();
lessons.addAll(loader.loadLessons(webgoatContext, context, path, properties));
}
}

View File

@ -1,12 +1,12 @@
package org.owasp.webgoat.session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import org.owasp.webgoat.HammerHead;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* *************************************************************************************************

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/WebGoat">
<Loader delegate="true" loaderClass="org.owasp.webgoat.classloader.PluginClassLoader" searchExternalFirst="true"/>
</Context>

View File

@ -1,4 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/WebGoat">
<Loader delegate="true" loaderClass="org.owasp.webgoat.classloader.PluginClassLoader" searchExternalFirst="true"/>
</Context>
<Context antiJARLocking="true" path="/WebGoat"/>