507 lines
19 KiB
Java
507 lines
19 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
* (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package org.apache.tomcat.util.scan;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.net.URLClassLoader;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Deque;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedList;
|
|
import java.util.Set;
|
|
import java.util.jar.Attributes;
|
|
import java.util.jar.Manifest;
|
|
|
|
import javax.servlet.ServletContext;
|
|
|
|
import org.apache.juli.logging.Log;
|
|
import org.apache.juli.logging.LogFactory;
|
|
import org.apache.tomcat.Jar;
|
|
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.ExceptionUtils;
|
|
import org.apache.tomcat.util.buf.UriUtil;
|
|
import org.apache.tomcat.util.compat.JreCompat;
|
|
import org.apache.tomcat.util.res.StringManager;
|
|
|
|
/**
|
|
* The default {@link JarScanner} implementation scans the WEB-INF/lib directory
|
|
* followed by the provided classloader and then works up the classloader
|
|
* hierarchy. This implementation is sufficient to meet the requirements of the
|
|
* Servlet 3.0 specification as well as to provide a number of Tomcat specific
|
|
* extensions. The extensions are:
|
|
* <ul>
|
|
* <li>Scanning the classloader hierarchy (enabled by default)</li>
|
|
* <li>Testing all files to see if they are JARs (disabled by default)</li>
|
|
* <li>Testing all directories to see if they are exploded JARs
|
|
* (disabled by default)</li>
|
|
* </ul>
|
|
* All of the extensions may be controlled via configuration.
|
|
*/
|
|
public class StandardJarScanner implements JarScanner {
|
|
|
|
private final Log log = LogFactory.getLog(StandardJarScanner.class); // must not be static
|
|
|
|
/**
|
|
* The string resources for this package.
|
|
*/
|
|
private static final StringManager sm = StringManager.getManager(Constants.Package);
|
|
|
|
private static final Set<ClassLoader> CLASSLOADER_HIERARCHY;
|
|
|
|
static {
|
|
Set<ClassLoader> cls = new HashSet<>();
|
|
|
|
ClassLoader cl = StandardJarScanner.class.getClassLoader();
|
|
while (cl != null) {
|
|
cls.add(cl);
|
|
cl = cl.getParent();
|
|
}
|
|
|
|
CLASSLOADER_HIERARCHY = Collections.unmodifiableSet(cls);
|
|
}
|
|
|
|
/**
|
|
* Controls the classpath scanning extension.
|
|
*/
|
|
private boolean scanClassPath = true;
|
|
public boolean isScanClassPath() {
|
|
return scanClassPath;
|
|
}
|
|
public void setScanClassPath(boolean scanClassPath) {
|
|
this.scanClassPath = scanClassPath;
|
|
}
|
|
|
|
/**
|
|
* Controls the JAR file Manifest scanning extension.
|
|
*/
|
|
private boolean scanManifest = true;
|
|
public boolean isScanManifest() {
|
|
return scanManifest;
|
|
}
|
|
public void setScanManifest(boolean scanManifest) {
|
|
this.scanManifest = scanManifest;
|
|
}
|
|
|
|
/**
|
|
* 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 = true;
|
|
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"));
|
|
}
|
|
|
|
if (jarScanFilter instanceof StandardJarScanFilter) {
|
|
if (((StandardJarScanFilter) jarScanFilter).isSkipAll())
|
|
return;
|
|
}
|
|
|
|
Set<URL> processedURLs = new HashSet<>();
|
|
|
|
// Scan WEB-INF/lib
|
|
Set<String> dirList = context.getResourcePaths(Constants.WEB_INF_LIB);
|
|
if (dirList != null) {
|
|
for (String path : dirList) {
|
|
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, null);
|
|
} 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
|
|
try {
|
|
URL webInfURL = context.getResource(Constants.WEB_INF_CLASSES);
|
|
if (webInfURL != null) {
|
|
// WEB-INF/classes will also be included in the URLs returned
|
|
// by the web application class loader so ensure the class path
|
|
// scanning below does not re-scan this location.
|
|
processedURLs.add(webInfURL);
|
|
|
|
if (isScanAllDirectories()) {
|
|
URL url = context.getResource(Constants.WEB_INF_CLASSES + "/META-INF");
|
|
if (url != null) {
|
|
try {
|
|
callback.scanWebInfClasses();
|
|
} catch (IOException e) {
|
|
log.warn(sm.getString("jarScan.webinfclassesFail"), e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (MalformedURLException e) {
|
|
// Ignore. Won't happen. URLs are of the correct form.
|
|
}
|
|
|
|
// Scan the classpath
|
|
if (isScanClassPath()) {
|
|
doScanClassPath(scanType, context, callback, processedURLs);
|
|
}
|
|
}
|
|
|
|
|
|
protected void doScanClassPath(JarScanType scanType, ServletContext context,
|
|
JarScannerCallback callback, Set<URL> processedURLs) {
|
|
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;
|
|
|
|
// Use a Deque so URLs can be removed as they are processed
|
|
// and new URLs can be added as they are discovered during
|
|
// processing.
|
|
Deque<URL> classPathUrlsToProcess = new LinkedList<>();
|
|
|
|
while (classLoader != null && classLoader != stopLoader) {
|
|
if (classLoader instanceof URLClassLoader) {
|
|
if (isWebapp) {
|
|
isWebapp = isWebappClassLoader(classLoader);
|
|
}
|
|
|
|
classPathUrlsToProcess.addAll(
|
|
Arrays.asList(((URLClassLoader) classLoader).getURLs()));
|
|
|
|
processURLs(scanType, callback, processedURLs, isWebapp, classPathUrlsToProcess);
|
|
}
|
|
classLoader = classLoader.getParent();
|
|
}
|
|
|
|
if (JreCompat.isJre9Available()) {
|
|
// The application and platform class loaders are not
|
|
// instances of URLClassLoader. Use the class path in this
|
|
// case.
|
|
addClassPath(classPathUrlsToProcess);
|
|
// Also add any modules
|
|
JreCompat.getInstance().addBootModulePath(classPathUrlsToProcess);
|
|
processURLs(scanType, callback, processedURLs, false, classPathUrlsToProcess);
|
|
}
|
|
}
|
|
|
|
|
|
protected void processURLs(JarScanType scanType, JarScannerCallback callback,
|
|
Set<URL> processedURLs, boolean isWebapp, Deque<URL> classPathUrlsToProcess) {
|
|
|
|
if (jarScanFilter instanceof StandardJarScanFilter) {
|
|
if (((StandardJarScanFilter) jarScanFilter).isSkipAll())
|
|
return;
|
|
}
|
|
|
|
while (!classPathUrlsToProcess.isEmpty()) {
|
|
URL url = classPathUrlsToProcess.pop();
|
|
|
|
if (processedURLs.contains(url)) {
|
|
// Skip this URL it has already been processed
|
|
continue;
|
|
}
|
|
|
|
ClassPathEntry cpe = new ClassPathEntry(url);
|
|
|
|
// 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", url));
|
|
}
|
|
try {
|
|
processedURLs.add(url);
|
|
process(scanType, callback, url, null, isWebapp, classPathUrlsToProcess);
|
|
} catch (IOException ioe) {
|
|
log.warn(sm.getString("jarScan.classloaderFail", url), ioe);
|
|
}
|
|
} else {
|
|
// JAR / directory has been skipped
|
|
if (log.isTraceEnabled()) {
|
|
log.trace(sm.getString("jarScan.classloaderJarNoScan", url));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
protected void addClassPath(Deque<URL> classPathUrlsToProcess) {
|
|
String classPath = System.getProperty("java.class.path");
|
|
|
|
if (classPath == null || classPath.length() == 0) {
|
|
return;
|
|
}
|
|
|
|
String[] classPathEntries = classPath.split(File.pathSeparator);
|
|
for (String classPathEntry : classPathEntries) {
|
|
File f = new File(classPathEntry);
|
|
try {
|
|
classPathUrlsToProcess.add(f.toURI().toURL());
|
|
} catch (MalformedURLException e) {
|
|
log.warn(sm.getString("jarScan.classPath.badEntry", classPathEntry), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* 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 static boolean isWebappClassLoader(ClassLoader classLoader) {
|
|
return !CLASSLOADER_HIERARCHY.contains(classLoader);
|
|
}
|
|
|
|
|
|
/*
|
|
* Scan a URL for JARs with the optional extensions to look at all files
|
|
* and all directories.
|
|
*/
|
|
protected void process(JarScanType scanType, JarScannerCallback callback,
|
|
URL url, String webappPath, boolean isWebapp, Deque<URL> classPathUrlsToProcess)
|
|
throws IOException {
|
|
|
|
if (log.isTraceEnabled()) {
|
|
log.trace(sm.getString("jarScan.jarUrlStart", url));
|
|
}
|
|
|
|
if ("jar".equals(url.getProtocol()) || url.getPath().endsWith(Constants.JAR_EXT)) {
|
|
try (Jar jar = JarFactory.newInstance(url)) {
|
|
if (isScanManifest()) {
|
|
processManifest(jar, isWebapp, classPathUrlsToProcess);
|
|
}
|
|
callback.scan(jar, webappPath, isWebapp);
|
|
}
|
|
} else if ("file".equals(url.getProtocol())) {
|
|
File f;
|
|
try {
|
|
f = new File(url.toURI());
|
|
if (f.isFile() && isScanAllFiles()) {
|
|
// Treat this file as a JAR
|
|
URL jarURL = UriUtil.buildJarUrl(f);
|
|
try (Jar jar = JarFactory.newInstance(jarURL)) {
|
|
if (isScanManifest()) {
|
|
processManifest(jar, isWebapp, classPathUrlsToProcess);
|
|
}
|
|
callback.scan(jar, 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 (Throwable t) {
|
|
ExceptionUtils.handleThrowable(t);
|
|
// Wrap the exception and re-throw
|
|
IOException ioe = new IOException();
|
|
ioe.initCause(t);
|
|
throw ioe;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void processManifest(Jar jar, boolean isWebapp,
|
|
Deque<URL> classPathUrlsToProcess) throws IOException {
|
|
|
|
// Not processed for web application JARs nor if the caller did not
|
|
// provide a Deque of URLs to append to.
|
|
if (isWebapp || classPathUrlsToProcess == null) {
|
|
return;
|
|
}
|
|
|
|
Manifest manifest = jar.getManifest();
|
|
if (manifest != null) {
|
|
Attributes attributes = manifest.getMainAttributes();
|
|
String classPathAttribute = attributes.getValue("Class-Path");
|
|
if (classPathAttribute == null) {
|
|
return;
|
|
}
|
|
String[] classPathEntries = classPathAttribute.split(" ");
|
|
for (String classPathEntry : classPathEntries) {
|
|
classPathEntry = classPathEntry.trim();
|
|
if (classPathEntry.length() == 0) {
|
|
continue;
|
|
}
|
|
URL jarURL = jar.getJarFileURL();
|
|
URL classPathEntryURL;
|
|
try {
|
|
URI jarURI = jarURL.toURI();
|
|
/*
|
|
* Note: Resolving the relative URLs from the manifest has the
|
|
* potential to introduce security concerns. However, since
|
|
* only JARs provided by the container and NOT those provided
|
|
* by web applications are processed, there should be no
|
|
* issues.
|
|
* If this feature is ever extended to include JARs provided
|
|
* by web applications, checks should be added to ensure that
|
|
* any relative URL does not step outside the web application.
|
|
*/
|
|
URI classPathEntryURI = jarURI.resolve(classPathEntry);
|
|
classPathEntryURL = classPathEntryURI.toURL();
|
|
} catch (Exception e) {
|
|
if (log.isDebugEnabled()) {
|
|
log.debug(sm.getString("jarScan.invalidUri", jarURL), e);
|
|
}
|
|
continue;
|
|
}
|
|
classPathUrlsToProcess.add(classPathEntryURL);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private static class ClassPathEntry {
|
|
|
|
private final boolean jar;
|
|
private final String name;
|
|
|
|
public ClassPathEntry(URL url) {
|
|
String path = url.getPath();
|
|
int end = path.lastIndexOf(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;
|
|
}
|
|
}
|
|
}
|