1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */

17 package org.apache.tomcat.util.scan;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.net.MalformedURLException;
22 import java.net.URI;
23 import java.net.URL;
24 import java.net.URLClassLoader;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.Deque;
28 import java.util.HashSet;
29 import java.util.LinkedList;
30 import java.util.Set;
31 import java.util.jar.Attributes;
32 import java.util.jar.Manifest;
33
34 import javax.servlet.ServletContext;
35
36 import org.apache.juli.logging.Log;
37 import org.apache.juli.logging.LogFactory;
38 import org.apache.tomcat.Jar;
39 import org.apache.tomcat.JarScanFilter;
40 import org.apache.tomcat.JarScanType;
41 import org.apache.tomcat.JarScanner;
42 import org.apache.tomcat.JarScannerCallback;
43 import org.apache.tomcat.util.ExceptionUtils;
44 import org.apache.tomcat.util.buf.UriUtil;
45 import org.apache.tomcat.util.compat.JreCompat;
46 import org.apache.tomcat.util.res.StringManager;
47
48 /**
49  * The default {@link JarScanner} implementation scans the WEB-INF/lib directory
50  * followed by the provided classloader and then works up the classloader
51  * hierarchy. This implementation is sufficient to meet the requirements of the
52  * Servlet 3.0 specification as well as to provide a number of Tomcat specific
53  * extensions. The extensions are:
54  * <ul>
55  *   <li>Scanning the classloader hierarchy (enabled by default)</li>
56  *   <li>Testing all files to see if they are JARs (disabled by default)</li>
57  *   <li>Testing all directories to see if they are exploded JARs
58  *       (disabled by default)</li>
59  * </ul>
60  * All of the extensions may be controlled via configuration.
61  */

62 public class StandardJarScanner implements JarScanner {
63
64     private final Log log = LogFactory.getLog(StandardJarScanner.class); // must not be static
65
66     /**
67      * The string resources for this package.
68      */

69     private static final StringManager sm = StringManager.getManager(Constants.Package);
70
71     private static final Set<ClassLoader> CLASSLOADER_HIERARCHY;
72
73     static {
74         Set<ClassLoader> cls = new HashSet<>();
75
76         ClassLoader cl = StandardJarScanner.class.getClassLoader();
77         while (cl != null) {
78             cls.add(cl);
79             cl = cl.getParent();
80         }
81
82         CLASSLOADER_HIERARCHY = Collections.unmodifiableSet(cls);
83     }
84
85     /**
86      * Controls the classpath scanning extension.
87      */

88     private boolean scanClassPath = true;
89     public boolean isScanClassPath() {
90         return scanClassPath;
91     }
92     public void setScanClassPath(boolean scanClassPath) {
93         this.scanClassPath = scanClassPath;
94     }
95
96     /**
97      * Controls the JAR file Manifest scanning extension.
98      */

99     private boolean scanManifest = true;
100     public boolean isScanManifest() {
101         return scanManifest;
102     }
103     public void setScanManifest(boolean scanManifest) {
104         this.scanManifest = scanManifest;
105     }
106
107     /**
108      * Controls the testing all files to see of they are JAR files extension.
109      */

110     private boolean scanAllFiles = false;
111     public boolean isScanAllFiles() {
112         return scanAllFiles;
113     }
114     public void setScanAllFiles(boolean scanAllFiles) {
115         this.scanAllFiles = scanAllFiles;
116     }
117
118     /**
119      * Controls the testing all directories to see of they are exploded JAR
120      * files extension.
121      */

122     private boolean scanAllDirectories = true;
123     public boolean isScanAllDirectories() {
124         return scanAllDirectories;
125     }
126     public void setScanAllDirectories(boolean scanAllDirectories) {
127         this.scanAllDirectories = scanAllDirectories;
128     }
129
130     /**
131      * Controls the testing of the bootstrap classpath which consists of the
132      * runtime classes provided by the JVM and any installed system extensions.
133      */

134     private boolean scanBootstrapClassPath = false;
135     public boolean isScanBootstrapClassPath() {
136         return scanBootstrapClassPath;
137     }
138     public void setScanBootstrapClassPath(boolean scanBootstrapClassPath) {
139         this.scanBootstrapClassPath = scanBootstrapClassPath;
140     }
141
142     /**
143      * Controls the filtering of the results from the scan for JARs
144      */

145     private JarScanFilter jarScanFilter = new StandardJarScanFilter();
146     @Override
147     public JarScanFilter getJarScanFilter() {
148         return jarScanFilter;
149     }
150     @Override
151     public void setJarScanFilter(JarScanFilter jarScanFilter) {
152         this.jarScanFilter = jarScanFilter;
153     }
154
155     /**
156      * Scan the provided ServletContext and class loader for JAR files. Each JAR
157      * file found will be passed to the callback handler to be processed.
158      *
159      * @param scanType      The type of JAR scan to perform. This is passed to
160      *                          the filter which uses it to determine how to
161      *                          filter the results
162      * @param context       The ServletContext - used to locate and access
163      *                      WEB-INF/lib
164      * @param callback      The handler to process any JARs found
165      */

166     @Override
167     public void scan(JarScanType scanType, ServletContext context,
168             JarScannerCallback callback) {
169
170         if (log.isTraceEnabled()) {
171             log.trace(sm.getString("jarScan.webinflibStart"));
172         }
173
174         if (jarScanFilter.isSkipAll()) {
175             return;
176         }
177
178         Set<URL> processedURLs = new HashSet<>();
179
180         // Scan WEB-INF/lib
181         Set<String> dirList = context.getResourcePaths(Constants.WEB_INF_LIB);
182         if (dirList != null) {
183             for (String path : dirList) {
184                 if (path.endsWith(Constants.JAR_EXT) &&
185                         getJarScanFilter().check(scanType,
186                                 path.substring(path.lastIndexOf('/')+1))) {
187                     // Need to scan this JAR
188                     if (log.isDebugEnabled()) {
189                         log.debug(sm.getString("jarScan.webinflibJarScan", path));
190                     }
191                     URL url = null;
192                     try {
193                         url = context.getResource(path);
194                         processedURLs.add(url);
195                         process(scanType, callback, url, path, truenull);
196                     } catch (IOException e) {
197                         log.warn(sm.getString("jarScan.webinflibFail", url), e);
198                     }
199                 } else {
200                     if (log.isTraceEnabled()) {
201                         log.trace(sm.getString("jarScan.webinflibJarNoScan", path));
202                     }
203                 }
204             }
205         }
206
207         // Scan WEB-INF/classes
208         try {
209             URL webInfURL = context.getResource(Constants.WEB_INF_CLASSES);
210             if (webInfURL != null) {
211                 // WEB-INF/classes will also be included in the URLs returned
212                 // by the web application class loader so ensure the class path
213                 // scanning below does not re-scan this location.
214                 processedURLs.add(webInfURL);
215
216                 if (isScanAllDirectories()) {
217                     URL url = context.getResource(Constants.WEB_INF_CLASSES + "/META-INF");
218                     if (url != null) {
219                         try {
220                             callback.scanWebInfClasses();
221                         } catch (IOException e) {
222                             log.warn(sm.getString("jarScan.webinfclassesFail"), e);
223                         }
224                     }
225                 }
226             }
227         } catch (MalformedURLException e) {
228             // Ignore. Won't happen. URLs are of the correct form.
229         }
230
231         // Scan the classpath
232         if (isScanClassPath()) {
233             doScanClassPath(scanType, context, callback, processedURLs);
234         }
235     }
236
237
238     protected void doScanClassPath(JarScanType scanType, ServletContext context,
239             JarScannerCallback callback, Set<URL> processedURLs) {
240         if (log.isTraceEnabled()) {
241             log.trace(sm.getString("jarScan.classloaderStart"));
242         }
243
244         ClassLoader stopLoader = null;
245         if (!isScanBootstrapClassPath()) {
246             // Stop when we reach the bootstrap class loader
247             stopLoader = ClassLoader.getSystemClassLoader().getParent();
248         }
249
250         ClassLoader classLoader = context.getClassLoader();
251
252         // JARs are treated as application provided until the common class
253         // loader is reached.
254         boolean isWebapp = true;
255
256         // Use a Deque so URLs can be removed as they are processed
257         // and new URLs can be added as they are discovered during
258         // processing.
259         Deque<URL> classPathUrlsToProcess = new LinkedList<>();
260
261         while (classLoader != null && classLoader != stopLoader) {
262             if (classLoader instanceof URLClassLoader) {
263                 if (isWebapp) {
264                     isWebapp = isWebappClassLoader(classLoader);
265                 }
266
267                 classPathUrlsToProcess.addAll(
268                         Arrays.asList(((URLClassLoader) classLoader).getURLs()));
269
270                 processURLs(scanType, callback, processedURLs, isWebapp, classPathUrlsToProcess);
271             }
272             classLoader = classLoader.getParent();
273         }
274
275         if (JreCompat.isJre9Available()) {
276             // The application and platform class loaders are not
277             // instances of URLClassLoader. Use the class path in this
278             // case.
279             addClassPath(classPathUrlsToProcess);
280             // Also add any modules
281             JreCompat.getInstance().addBootModulePath(classPathUrlsToProcess);
282             processURLs(scanType, callback, processedURLs, false, classPathUrlsToProcess);
283         }
284     }
285
286
287     protected void processURLs(JarScanType scanType, JarScannerCallback callback,
288             Set<URL> processedURLs, boolean isWebapp, Deque<URL> classPathUrlsToProcess) {
289
290         if (jarScanFilter.isSkipAll()) {
291             return;
292         }
293
294         while (!classPathUrlsToProcess.isEmpty()) {
295             URL url = classPathUrlsToProcess.pop();
296
297             if (processedURLs.contains(url)) {
298                 // Skip this URL it has already been processed
299                 continue;
300             }
301
302             ClassPathEntry cpe = new ClassPathEntry(url);
303
304             // JARs are scanned unless the filter says not to.
305             // Directories are scanned for pluggability scans or
306             // if scanAllDirectories is enabled unless the
307             // filter says not to.
308             if ((cpe.isJar() ||
309                     scanType == JarScanType.PLUGGABILITY ||
310                     isScanAllDirectories()) &&
311                             getJarScanFilter().check(scanType,
312                                     cpe.getName())) {
313                 if (log.isDebugEnabled()) {
314                     log.debug(sm.getString("jarScan.classloaderJarScan", url));
315                 }
316                 try {
317                     processedURLs.add(url);
318                     process(scanType, callback, url, null, isWebapp, classPathUrlsToProcess);
319                 } catch (IOException ioe) {
320                     log.warn(sm.getString("jarScan.classloaderFail", url), ioe);
321                 }
322             } else {
323                 // JAR / directory has been skipped
324                 if (log.isTraceEnabled()) {
325                     log.trace(sm.getString("jarScan.classloaderJarNoScan", url));
326                 }
327             }
328         }
329     }
330
331
332     protected void addClassPath(Deque<URL> classPathUrlsToProcess) {
333         String classPath = System.getProperty("java.class.path");
334
335         if (classPath == null || classPath.length() == 0) {
336             return;
337         }
338
339         String[] classPathEntries = classPath.split(File.pathSeparator);
340         for (String classPathEntry : classPathEntries) {
341             File f = new File(classPathEntry);
342             try {
343                 classPathUrlsToProcess.add(f.toURI().toURL());
344             } catch (MalformedURLException e) {
345                 log.warn(sm.getString("jarScan.classPath.badEntry", classPathEntry), e);
346             }
347         }
348     }
349
350
351     /*
352      * Since class loader hierarchies can get complicated, this method attempts
353      * to apply the following rule: A class loader is a web application class
354      * loader unless it loaded this class (StandardJarScanner) or is a parent
355      * of the class loader that loaded this class.
356      *
357      * This should mean:
358      *   the webapp class loader is an application class loader
359      *   the shared class loader is an application class loader
360      *   the server class loader is not an application class loader
361      *   the common class loader is not an application class loader
362      *   the system class loader is not an application class loader
363      *   the bootstrap class loader is not an application class loader
364      */

365     private static boolean isWebappClassLoader(ClassLoader classLoader) {
366         return !CLASSLOADER_HIERARCHY.contains(classLoader);
367     }
368
369
370     /*
371      * Scan a URL for JARs with the optional extensions to look at all files
372      * and all directories.
373      */

374     protected void process(JarScanType scanType, JarScannerCallback callback,
375             URL url, String webappPath, boolean isWebapp, Deque<URL> classPathUrlsToProcess)
376             throws IOException {
377
378         if (log.isTraceEnabled()) {
379             log.trace(sm.getString("jarScan.jarUrlStart", url));
380         }
381
382         if ("jar".equals(url.getProtocol()) || url.getPath().endsWith(Constants.JAR_EXT)) {
383             try (Jar jar = JarFactory.newInstance(url)) {
384                 if (isScanManifest()) {
385                     processManifest(jar, isWebapp, classPathUrlsToProcess);
386                 }
387                 callback.scan(jar, webappPath, isWebapp);
388             }
389         } else if ("file".equals(url.getProtocol())) {
390             File f;
391             try {
392                 f = new File(url.toURI());
393                 if (f.isFile() && isScanAllFiles()) {
394                     // Treat this file as a JAR
395                     URL jarURL = UriUtil.buildJarUrl(f);
396                     try (Jar jar = JarFactory.newInstance(jarURL)) {
397                         if (isScanManifest()) {
398                             processManifest(jar, isWebapp, classPathUrlsToProcess);
399                         }
400                         callback.scan(jar, webappPath, isWebapp);
401                     }
402                 } else if (f.isDirectory()) {
403                     if (scanType == JarScanType.PLUGGABILITY) {
404                         callback.scan(f, webappPath, isWebapp);
405                     } else {
406                         File metainf = new File(f.getAbsoluteFile() + File.separator + "META-INF");
407                         if (metainf.isDirectory()) {
408                             callback.scan(f, webappPath, isWebapp);
409                         }
410                     }
411                 }
412             } catch (Throwable t) {
413                 ExceptionUtils.handleThrowable(t);
414                 // Wrap the exception and re-throw
415                 IOException ioe = new IOException();
416                 ioe.initCause(t);
417                 throw ioe;
418             }
419         }
420     }
421
422
423     private void processManifest(Jar jar, boolean isWebapp,
424             Deque<URL> classPathUrlsToProcess) throws IOException {
425
426         // Not processed for web application JARs nor if the caller did not
427         // provide a Deque of URLs to append to.
428         if (isWebapp || classPathUrlsToProcess == null) {
429             return;
430         }
431
432         Manifest manifest = jar.getManifest();
433         if (manifest != null) {
434             Attributes attributes = manifest.getMainAttributes();
435             String classPathAttribute = attributes.getValue("Class-Path");
436             if (classPathAttribute == null) {
437                 return;
438             }
439             String[] classPathEntries = classPathAttribute.split(" ");
440             for (String classPathEntry : classPathEntries) {
441                 classPathEntry = classPathEntry.trim();
442                 if (classPathEntry.length() == 0) {
443                     continue;
444                 }
445                 URL jarURL = jar.getJarFileURL();
446                 URL classPathEntryURL;
447                 try {
448                     URI jarURI = jarURL.toURI();
449                     /*
450                      * Note: Resolving the relative URLs from the manifest has the
451                      *       potential to introduce security concerns. However, since
452                      *       only JARs provided by the container and NOT those provided
453                      *       by web applications are processed, there should be no
454                      *       issues.
455                      *       If this feature is ever extended to include JARs provided
456                      *       by web applications, checks should be added to ensure that
457                      *       any relative URL does not step outside the web application.
458                      */

459                     URI classPathEntryURI = jarURI.resolve(classPathEntry);
460                     classPathEntryURL = classPathEntryURI.toURL();
461                 } catch (Exception e) {
462                     if (log.isDebugEnabled()) {
463                         log.debug(sm.getString("jarScan.invalidUri", jarURL), e);
464                     }
465                     continue;
466                 }
467                 classPathUrlsToProcess.add(classPathEntryURL);
468             }
469         }
470     }
471
472
473     private static class ClassPathEntry {
474
475         private final boolean jar;
476         private final String name;
477
478         public ClassPathEntry(URL url) {
479             String path = url.getPath();
480             int end = path.lastIndexOf(Constants.JAR_EXT);
481             if (end != -1) {
482                 jar = true;
483                 int start = path.lastIndexOf('/', end);
484                 name = path.substring(start + 1, end + 4);
485             } else {
486                 jar = false;
487                 if (path.endsWith("/")) {
488                     path = path.substring(0, path.length() - 1);
489                 }
490                 int start = path.lastIndexOf('/');
491                 name = path.substring(start + 1);
492             }
493
494         }
495
496         public boolean isJar() {
497             return jar;
498         }
499
500         public String getName() {
501             return name;
502         }
503     }
504 }
505