package net.unit8.waitt.server.tomcat8; import java.io.File; import java.io.IOException; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import javax.servlet.ServletContext; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.naming.StringManager; import org.apache.tomcat.JarScanFilter; import org.apache.tomcat.JarScanType; import org.apache.tomcat.JarScanner; import org.apache.tomcat.JarScannerCallback; import org.apache.tomcat.util.scan.Constants; import org.apache.tomcat.util.scan.StandardJarScanFilter; import org.apache.tomcat.util.scan.StandardJarScanner; import org.codehaus.plexus.classworlds.realm.ClassRealm; /** * * @author kawasima */ public class ClassRealmJarScanner implements JarScanner { private static final Log log = LogFactory.getLog(StandardJarScanner.class); /** * The string resources for this package. */ private static final StringManager sm = StringManager.getManager(Constants.Package); /** * Controls the classpath scanning extension. */ private boolean scanClassPath = true; public boolean isScanClassPath() { return scanClassPath; } public void setScanClassPath(boolean scanClassPath) { this.scanClassPath = scanClassPath; } /** * Controls the testing all files to see of they are JAR files extension. */ private boolean scanAllFiles = false; public boolean isScanAllFiles() { return scanAllFiles; } public void setScanAllFiles(boolean scanAllFiles) { this.scanAllFiles = scanAllFiles; } /** * Controls the testing all directories to see of they are exploded JAR * files extension. */ private boolean scanAllDirectories = false; public boolean isScanAllDirectories() { return scanAllDirectories; } public void setScanAllDirectories(boolean scanAllDirectories) { this.scanAllDirectories = scanAllDirectories; } /** * Controls the testing of the bootstrap classpath which consists of the * runtime classes provided by the JVM and any installed system extensions. */ private boolean scanBootstrapClassPath = false; public boolean isScanBootstrapClassPath() { return scanBootstrapClassPath; } public void setScanBootstrapClassPath(boolean scanBootstrapClassPath) { this.scanBootstrapClassPath = scanBootstrapClassPath; } /** * Controls the filtering of the results from the scan for JARs */ private JarScanFilter jarScanFilter = new StandardJarScanFilter(); @Override public JarScanFilter getJarScanFilter() { return jarScanFilter; } @Override public void setJarScanFilter(JarScanFilter jarScanFilter) { this.jarScanFilter = jarScanFilter; } /** * Scan the provided ServletContext and class loader for JAR files. Each JAR * file found will be passed to the callback handler to be processed. * * @param scanType The type of JAR scan to perform. This is passed to * the filter which uses it to determine how to * filter the results * @param context The ServletContext - used to locate and access * WEB-INF/lib * @param callback The handler to process any JARs found */ @Override public void scan(JarScanType scanType, ServletContext context, JarScannerCallback callback) { if (log.isTraceEnabled()) { log.trace(sm.getString("jarScan.webinflibStart")); } Set<URL> processedURLs = new HashSet<>(); // Scan WEB-INF/lib Set<String> dirList = context.getResourcePaths(Constants.WEB_INF_LIB); if (dirList != null) { Iterator<String> it = dirList.iterator(); while (it.hasNext()) { String path = it.next(); if (path.endsWith(Constants.JAR_EXT) && getJarScanFilter().check(scanType, path.substring(path.lastIndexOf('/')+1))) { // Need to scan this JAR if (log.isDebugEnabled()) { log.debug(sm.getString("jarScan.webinflibJarScan", path)); } URL url = null; try { url = context.getResource(path); processedURLs.add(url); process(scanType, callback, url, path, true); } catch (IOException e) { log.warn(sm.getString("jarScan.webinflibFail", url), e); } } else { if (log.isTraceEnabled()) { log.trace(sm.getString("jarScan.webinflibJarNoScan", path)); } } } } // Scan WEB-INF/classes if (isScanAllDirectories()) { try { URL url = context.getResource("/WEB-INF/classes/META-INF"); if (url != null) { // Class path scanning will look at WEB-INF/classes since // that is the URL that Tomcat's web application class // loader returns. Therefore, it is this URL that needs to // be added to the set of processed URLs. URL webInfURL = context.getResource("/WEB-INF/classes"); if (webInfURL != null) { processedURLs.add(webInfURL); } try { callback.scanWebInfClasses(); } catch (IOException e) { log.warn(sm.getString("jarScan.webinfclassesFail"), e); } } } catch (MalformedURLException e) { // Ignore } } // Scan the classpath if (isScanClassPath()) { if (log.isTraceEnabled()) { log.trace(sm.getString("jarScan.classloaderStart")); } ClassLoader stopLoader = null; if (!isScanBootstrapClassPath()) { // Stop when we reach the bootstrap class loader stopLoader = ClassLoader.getSystemClassLoader().getParent(); } ClassLoader classLoader = context.getClassLoader(); // JARs are treated as application provided until the common class // loader is reached. boolean isWebapp = true; while (classLoader != null && classLoader != stopLoader) { if (classLoader instanceof URLClassLoader) { if (isWebapp) { isWebapp = isWebappClassLoader(classLoader); } URL[] urls = ((URLClassLoader) classLoader).getURLs(); for (int i=0; i<urls.length; i++) { if (processedURLs.contains(urls[i])) { // Skip this URL it has already been processed continue; } ClassPathEntry cpe = new ClassPathEntry(urls[i]); // JARs are scanned unless the filter says not to. // Directories are scanned for pluggability scans or // if scanAllDirectories is enabled unless the // filter says not to. if ((cpe.isJar() || scanType == JarScanType.PLUGGABILITY || isScanAllDirectories()) && getJarScanFilter().check(scanType, cpe.getName())) { if (log.isDebugEnabled()) { log.debug(sm.getString( "jarScan.classloaderJarScan", urls[i])); } try { process(scanType, callback, urls[i], null, isWebapp); } catch (IOException ioe) { log.warn(sm.getString( "jarScan.classloaderFail", urls[i]), ioe); } } else { // JAR / directory has been skipped if (log.isTraceEnabled()) { log.trace(sm.getString( "jarScan.classloaderJarNoScan", urls[i])); } } } } if (classLoader instanceof ClassRealm) { classLoader = ((ClassRealm) classLoader).getParentClassLoader(); } else { classLoader = classLoader.getParent(); } } } } /* * Since class loader hierarchies can get complicated, this method attempts * to apply the following rule: A class loader is a web application class * loader unless it loaded this class (StandardJarScanner) or is a parent * of the class loader that loaded this class. * * This should mean: * the webapp class loader is an application class loader * the shared class loader is an application class loader * the server class loader is not an application class loader * the common class loader is not an application class loader * the system class loader is not an application class loader * the bootstrap class loader is not an application class loader */ private boolean isWebappClassLoader(ClassLoader classLoader) { ClassLoader nonWebappLoader = StandardJarScanner.class.getClassLoader(); while (nonWebappLoader != null) { if (nonWebappLoader == classLoader) { return false; } nonWebappLoader = nonWebappLoader.getParent(); } return true; } /* * Scan a URL for JARs with the optional extensions to look at all files * and all directories. */ private void process(JarScanType scanType, JarScannerCallback callback, URL url, String webappPath, boolean isWebapp) throws IOException { if (log.isTraceEnabled()) { log.trace(sm.getString("jarScan.jarUrlStart", url)); } URLConnection conn = url.openConnection(); if (conn instanceof JarURLConnection) { callback.scan((JarURLConnection) conn, webappPath, isWebapp); } else { String urlStr = url.toString(); if (urlStr.startsWith("file:") || urlStr.startsWith("http:") || urlStr.startsWith("https:")) { if (urlStr.endsWith(Constants.JAR_EXT)) { URL jarURL = new URL("jar:" + urlStr + "!/"); callback.scan((JarURLConnection) jarURL.openConnection(), webappPath, isWebapp); } else { File f; try { f = new File(url.toURI()); if (f.isFile() && isScanAllFiles()) { // Treat this file as a JAR URL jarURL = new URL("jar:" + urlStr + "!/"); callback.scan( (JarURLConnection) jarURL.openConnection(), webappPath, isWebapp); } else if (f.isDirectory()) { if (scanType == JarScanType.PLUGGABILITY) { callback.scan(f, webappPath, isWebapp); } else { File metainf = new File(f.getAbsoluteFile() + File.separator + "META-INF"); if (metainf.isDirectory()) { callback.scan(f, webappPath, isWebapp); } } } } catch (URISyntaxException e) { // Wrap the exception and re-throw IOException ioe = new IOException(); ioe.initCause(e); throw ioe; } } } } } private static class ClassPathEntry { private final boolean jar; private final String name; public ClassPathEntry(URL url) { String path = url.getPath(); int end = path.indexOf(Constants.JAR_EXT); if (end != -1) { jar = true; int start = path.lastIndexOf('/', end); name = path.substring(start + 1, end + 4); } else { jar = false; if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } int start = path.lastIndexOf('/'); name = path.substring(start + 1); } } public boolean isJar() { return jar; } public String getName() { return name; } } }