View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe;
12  
13  import jakarta.servlet.ServletConfig;
14  import jakarta.servlet.ServletContext;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.lang.management.ManagementFactory;
19  import java.net.URI;
20  import java.net.URISyntaxException;
21  import java.net.URL;
22  import java.net.URLClassLoader;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  import java.util.Set;
32  
33  import javax.management.MBeanServer;
34  import javax.management.MalformedObjectNameException;
35  import javax.management.ObjectName;
36  import javax.naming.NamingException;
37  
38  import org.apache.catalina.Container;
39  import org.apache.catalina.Context;
40  import org.apache.catalina.Engine;
41  import org.apache.catalina.Host;
42  import org.apache.catalina.Service;
43  import org.apache.catalina.Valve;
44  import org.apache.catalina.Wrapper;
45  import org.apache.catalina.connector.Connector;
46  import org.apache.catalina.core.StandardContext;
47  import org.apache.jasper.EmbeddedServletOptions;
48  import org.apache.jasper.JspCompilationContext;
49  import org.apache.jasper.Options;
50  import org.apache.jasper.compiler.Compiler;
51  import org.apache.jasper.compiler.JspRuntimeContext;
52  import org.apache.naming.ContextBindings;
53  import org.apache.naming.factory.ResourceLinkFactory;
54  import org.apache.tomcat.util.descriptor.web.ContextResourceLink;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  import org.springframework.util.ClassUtils;
58  
59  import psiprobe.beans.ResourceResolverBean;
60  import psiprobe.model.FilterMapping;
61  import psiprobe.model.jsp.Item;
62  import psiprobe.model.jsp.Summary;
63  
64  /**
65   * Abstraction layer to implement some functionality, which is common between different container
66   * adapters.
67   */
68  public abstract class AbstractTomcatContainer implements TomcatContainer {
69  
70    /** The logger. */
71    protected final Logger logger = LoggerFactory.getLogger(getClass());
72  
73    /** The Constant NO_JSP_SERVLET. */
74    private static final String NO_JSP_SERVLET = "Context '{}' does not have 'JSP' servlet";
75  
76    /** The host. */
77    protected Host host;
78  
79    /** The connectors. */
80    protected Connector[] connectors;
81  
82    /** The deployer o name. */
83    protected ObjectName objectNameDeployer;
84  
85    /** The mbean server. */
86    protected MBeanServer mbeanServer;
87  
88    /** The Enum FilterMapType. */
89    public enum FilterMapType {
90  
91      /** The url. */
92      URL,
93  
94      /** The servlet name. */
95      SERVLET_NAME
96    }
97  
98    @Override
99    public void setWrapper(Wrapper wrapper) {
100     Valve valve = createValve();
101     if (wrapper != null) {
102       host = (Host) wrapper.getParent().getParent();
103       Engine engine = (Engine) host.getParent();
104       Service service = engine.getService();
105       connectors = service.findConnectors();
106       try {
107         objectNameDeployer =
108             new ObjectName(host.getParent().getName() + ":type=Deployer,host=" + host.getName());
109       } catch (MalformedObjectNameException e) {
110         logger.trace("", e);
111       }
112       host.getPipeline().addValve(valve);
113       mbeanServer = ManagementFactory.getPlatformMBeanServer();
114     } else if (host != null) {
115       host.getPipeline().removeValve(valve);
116     }
117   }
118 
119   @Override
120   public File getAppBase() {
121     File base = new File(host.getAppBase());
122     if (!base.isAbsolute()) {
123       base = new File(System.getProperty("catalina.base"), host.getAppBase());
124     }
125     return base;
126   }
127 
128   @Override
129   public String getConfigBase() {
130     File configBase = new File(System.getProperty("catalina.base"), "conf");
131     Container baseHost = null;
132     Container thisContainer = host;
133     while (thisContainer != null) {
134       if (thisContainer instanceof Host) {
135         baseHost = thisContainer;
136       }
137       thisContainer = thisContainer.getParent();
138     }
139     if (baseHost != null) {
140       configBase = new File(configBase, baseHost.getName());
141     }
142     return configBase.getAbsolutePath();
143   }
144 
145   @Override
146   public String getHostName() {
147     return host.getName();
148   }
149 
150   @Override
151   public String getName() {
152     return host.getParent().getName();
153   }
154 
155   @Override
156   public List<Context> findContexts() {
157     List<Context> results = new ArrayList<>();
158     for (Container child : host.findChildren()) {
159       if (child instanceof Context) {
160         results.add((Context) child);
161       }
162     }
163     return results;
164   }
165 
166   @Override
167   public List<Connector> findConnectors() {
168     return Collections.unmodifiableList(Arrays.asList(connectors));
169   }
170 
171   @Override
172   public boolean installContext(String contextName) throws Exception {
173     contextName = formatContextName(contextName);
174     installContextInternal(contextName);
175     return findContext(contextName) != null;
176   }
177 
178   @Override
179   public void stop(String name) throws Exception {
180     Context ctx = findContext(name);
181     if (ctx != null) {
182       ctx.stop();
183     }
184   }
185 
186   @Override
187   public void start(String name) throws Exception {
188     Context ctx = findContext(name);
189     if (ctx != null) {
190       ctx.start();
191     }
192   }
193 
194   @Override
195   public void remove(String name) throws Exception {
196     name = formatContextName(name);
197     Context ctx = findContext(name);
198 
199     if (ctx != null) {
200 
201       try {
202         stop(name);
203       } catch (Exception e) {
204         logger.info("Stopping '{}' threw this exception:", name, e);
205       }
206 
207       File appDir;
208       File docBase = new File(ctx.getDocBase());
209 
210       if (!docBase.isAbsolute()) {
211         appDir = new File(getAppBase(), ctx.getDocBase());
212       } else {
213         appDir = docBase;
214       }
215 
216       logger.debug("Deleting '{}'", appDir.getAbsolutePath());
217       Utils.delete(appDir);
218 
219       String warFilename = formatContextFilename(name);
220       File warFile = new File(getAppBase(), warFilename + ".war");
221       logger.debug("Deleting '{}'", warFile.getAbsolutePath());
222       Utils.delete(warFile);
223 
224       File configFile = getConfigFile(ctx);
225       if (configFile != null) {
226         logger.debug("Deleting '{}'", configFile.getAbsolutePath());
227         Utils.delete(configFile);
228       }
229 
230       removeInternal(name);
231     }
232   }
233 
234   /**
235    * Removes the internal.
236    *
237    * @param name the name
238    *
239    * @throws Exception the exception
240    */
241   private void removeInternal(String name) throws Exception {
242     checkChanges(name);
243   }
244 
245   @Override
246   public void installWar(String name) throws Exception {
247     checkChanges(name);
248   }
249 
250   /**
251    * Install context internal.
252    *
253    * @param name the name
254    *
255    * @throws Exception the exception
256    */
257   private void installContextInternal(String name) throws Exception {
258     checkChanges(name);
259   }
260 
261   @Override
262   public Context findContext(String name) {
263     String safeName = formatContextName(name);
264     if (safeName == null) {
265       return null;
266     }
267     Context result = findContextInternal(safeName);
268     if (result == null && safeName.isEmpty()) {
269       result = findContextInternal("/");
270     }
271     return result;
272   }
273 
274   @Override
275   public String formatContextName(String name) {
276     if (name == null) {
277       return null;
278     }
279     String result = name.trim();
280     if (!result.startsWith("/")) {
281       result = "/" + result;
282     }
283     if ("/".equals(result) || "/ROOT".equals(result)) {
284       result = "";
285     }
286     // For ROOT Parallel Deployment, remove "/ROOT"
287     if (result.startsWith("/ROOT##")) {
288       result = result.substring(5);
289     }
290     // For ROOT Parallel Usage, remove "/"
291     if (result.startsWith("/##")) {
292       result = result.substring(1);
293     }
294     return result;
295   }
296 
297   @Override
298   public String formatContextFilename(String contextName) {
299     if (contextName == null) {
300       return null;
301     }
302     if (contextName.isEmpty()) {
303       return "ROOT";
304     }
305     if (contextName.startsWith("/")) {
306       return contextName.substring(1);
307     }
308     return contextName;
309   }
310 
311   @Override
312   public void discardWorkDir(Context context) {
313     if (context instanceof StandardContext) {
314       StandardContext standardContext = (StandardContext) context;
315       String path = standardContext.getWorkPath();
316       logger.info("Discarding '{}'", path);
317       Utils.delete(new File(path, "org"));
318     } else {
319       logger.error("context '{}' is not an instance of '{}', expected StandardContext",
320           context.getName(), context.getClass().getName());
321     }
322   }
323 
324   @Override
325   public String getServletFileNameForJsp(Context context, String jspName) {
326     String servletName = null;
327 
328     ServletConfig servletConfig = (ServletConfig) context.findChild("jsp");
329     if (servletConfig != null) {
330       ServletContext sctx = context.getServletContext();
331       Options opt = new EmbeddedServletOptions(servletConfig, sctx);
332       JspRuntimeContext jrctx = new JspRuntimeContext(sctx, opt);
333       JspCompilationContext jcctx = createJspCompilationContext(jspName, opt, sctx, jrctx, null);
334       servletName = jcctx.getServletJavaFileName();
335     } else {
336       logger.error(NO_JSP_SERVLET, context.getName());
337     }
338     return servletName;
339   }
340 
341   @Override
342   public void recompileJsps(Context context, Summary summary, List<String> names) {
343     ServletConfig servletConfig = (ServletConfig) context.findChild("jsp");
344     if (servletConfig != null) {
345       if (summary != null) {
346         synchronized (servletConfig) {
347           ServletContext sctx = context.getServletContext();
348           Options opt = new EmbeddedServletOptions(servletConfig, sctx);
349 
350           JspRuntimeContext jrctx = new JspRuntimeContext(sctx, opt);
351           /*
352            * we need to pass context classloader here, so the jsps can reference /WEB-INF/classes
353            * and /WEB-INF/lib. JspCompilationContext would only take URLClassLoader, so we fake it
354            */
355           try (URLClassLoader classLoader =
356               new URLClassLoader(new URL[0], context.getLoader().getClassLoader())) {
357             for (String name : names) {
358               long time = System.currentTimeMillis();
359               JspCompilationContext jcctx =
360                   createJspCompilationContext(name, opt, sctx, jrctx, classLoader);
361               ClassLoader prevCl = ClassUtils.overrideThreadContextClassLoader(classLoader);
362               try {
363                 Item item = summary.getItems().get(name);
364                 if (item != null) {
365                   try {
366                     Compiler compiler = jcctx.createCompiler();
367                     compiler.compile();
368                     item.setState(Item.STATE_READY);
369                     item.setException(null);
370                     logger.info("Compiled '{}': OK", name);
371                   } catch (Exception e) {
372                     item.setState(Item.STATE_FAILED);
373                     item.setException(e);
374                     logger.error("Compiled '{}': FAILED", name, e);
375                   }
376                   item.setCompileTime(System.currentTimeMillis() - time);
377                 } else {
378                   logger.error("{} is not on the summary list, ignored", name);
379                 }
380               } finally {
381                 ClassUtils.overrideThreadContextClassLoader(prevCl);
382               }
383             }
384           } catch (IOException e) {
385             this.logger.error("", e);
386           } finally {
387             jrctx.destroy();
388           }
389         }
390       } else {
391         logger.error("summary is null for '{}', request ignored", context.getName());
392       }
393     } else {
394       logger.error(NO_JSP_SERVLET, context.getName());
395     }
396   }
397 
398   @Override
399   public void listContextJsps(Context context, Summary summary, boolean compile) {
400     ServletConfig servletConfig = (ServletConfig) context.findChild("jsp");
401     if (servletConfig != null) {
402       synchronized (servletConfig) {
403         ServletContext sctx = context.getServletContext();
404         Options opt = new EmbeddedServletOptions(servletConfig, sctx);
405 
406         JspRuntimeContext jrctx = new JspRuntimeContext(sctx, opt);
407         try {
408           if (summary.getItems() == null) {
409             summary.setItems(new HashMap<>());
410           }
411 
412           /*
413            * mark all items as missing
414            */
415           for (Item item : summary.getItems().values()) {
416             item.setMissing(true);
417           }
418 
419           /*
420            * we need to pass context classloader here, so the jsps can reference /WEB-INF/classes
421            * and /WEB-INF/lib. JspCompilationContext would only take URLClassLoader, so we fake it
422            */
423           try (URLClassLoader urlcl =
424               new URLClassLoader(new URL[0], context.getLoader().getClassLoader())) {
425 
426             compileItem("/", opt, context, jrctx, summary, urlcl, 0, compile);
427           } catch (IOException e) {
428             this.logger.error("", e);
429           }
430         } finally {
431           jrctx.destroy();
432         }
433       }
434 
435       //
436       // delete "missing" items by keeping "not missing" ones
437       //
438       Map<String, Item> hashMap = new HashMap<>();
439       for (Entry<String, Item> entry : summary.getItems().entrySet()) {
440         if (!entry.getValue().isMissing()) {
441           hashMap.put(entry.getKey(), entry.getValue());
442         }
443       }
444 
445       summary.setItems(hashMap);
446     } else {
447       logger.error(NO_JSP_SERVLET, context.getName());
448     }
449   }
450 
451   @Override
452   public boolean getAvailable(Context context) {
453     return context.getState().isAvailable();
454   }
455 
456   @Override
457   public File getConfigFile(Context context) {
458     URL configUrl = context.getConfigFile();
459     if (configUrl != null) {
460       try {
461         URI configUri = configUrl.toURI();
462         if ("file".equals(configUri.getScheme())) {
463           return new File(configUri.getPath());
464         }
465       } catch (URISyntaxException ex) {
466         logger.error("Could not convert URL to URI: '{}'", configUrl, ex);
467       }
468     }
469     return null;
470   }
471 
472   @Override
473   public void bindToContext(Context context) throws NamingException {
474     changeContextBinding(context, true);
475   }
476 
477   @Override
478   public void unbindFromContext(Context context) throws NamingException {
479     changeContextBinding(context, false);
480   }
481 
482   /**
483    * Register access to global resources.
484    *
485    * @param resourceLink the resource link
486    */
487   protected void registerGlobalResourceAccess(ContextResourceLink resourceLink) {
488     ResourceLinkFactory.registerGlobalResourceAccess(ResourceResolverBean.getGlobalNamingContext(),
489         resourceLink.getName(), resourceLink.getGlobal());
490   }
491 
492   /**
493    * Change context binding.
494    *
495    * @param context the context
496    * @param bind the bind
497    *
498    * @throws NamingException the naming exception
499    */
500   private void changeContextBinding(Context context, boolean bind) throws NamingException {
501     Object token = getNamingToken(context);
502     ClassLoader loader = Thread.currentThread().getContextClassLoader();
503     if (bind) {
504       ContextBindings.bindClassLoader(context, token, loader);
505     } else {
506       ContextBindings.unbindClassLoader(context, token, loader);
507     }
508   }
509 
510   /**
511    * Lists and optionally compiles a directory recursively.
512    *
513    * @param jspName name of JSP file or directory to be listed and compiled.
514    * @param opt the JSP compiler options
515    * @param ctx the context
516    * @param jrctx the runtime context used to create the compilation context
517    * @param summary the summary in which the output is stored
518    * @param classLoader the classloader used by the compiler
519    * @param level the depth in the tree at which the item was encountered
520    * @param compile whether or not to compile the item or just to check whether it's out of date
521    */
522   protected void compileItem(String jspName, Options opt, Context ctx, JspRuntimeContext jrctx,
523       Summary summary, URLClassLoader classLoader, int level, boolean compile) {
524     ServletContext sctx = ctx.getServletContext();
525     Set<String> paths = sctx.getResourcePaths(jspName);
526 
527     if (paths != null) {
528       for (String name : paths) {
529         boolean isJsp =
530             name.endsWith(".jsp") || name.endsWith(".jspx") || opt.getJspConfig().isJspPage(name);
531 
532         if (isJsp) {
533           JspCompilationContext jcctx =
534               createJspCompilationContext(name, opt, sctx, jrctx, classLoader);
535           ClassLoader prevCl = ClassUtils.overrideThreadContextClassLoader(classLoader);
536           try {
537             Item item = summary.getItems().get(name);
538 
539             if (item == null) {
540               item = new Item();
541               item.setName(name);
542             }
543 
544             item.setLevel(level);
545             item.setCompileTime(-1);
546 
547             Long[] objects = this.getResourceAttributes(name, ctx);
548             item.setSize(objects[0]);
549             item.setLastModified(objects[1]);
550 
551             long time = System.currentTimeMillis();
552             try {
553               Compiler compiler = jcctx.createCompiler();
554               if (compile) {
555                 compiler.compile();
556                 item.setState(Item.STATE_READY);
557                 item.setException(null);
558               } else if (!compiler.isOutDated()) {
559                 item.setState(Item.STATE_READY);
560                 item.setException(null);
561               } else if (item.getState() != Item.STATE_FAILED) {
562                 item.setState(Item.STATE_OOD);
563                 item.setException(null);
564               }
565               logger.info("Compiled '{}': OK", name);
566             } catch (Exception e) {
567               item.setState(Item.STATE_FAILED);
568               item.setException(e);
569               logger.info("Compiled '{}': FAILED", name, e);
570             }
571             if (compile) {
572               item.setCompileTime(System.currentTimeMillis() - time);
573             }
574             item.setMissing(false);
575             summary.putItem(name, item);
576           } finally {
577             ClassUtils.overrideThreadContextClassLoader(prevCl);
578           }
579         } else {
580           compileItem(name, opt, ctx, jrctx, summary, classLoader, level + 1, compile);
581         }
582       }
583     } else {
584       logger.debug("getResourcePaths() is null for '{}'. Empty dir? Or Tomcat bug?", jspName);
585     }
586   }
587 
588   /**
589    * Find context internal.
590    *
591    * @param name the context name
592    *
593    * @return the context
594    */
595   protected Context findContextInternal(String name) {
596     return (Context) host.findChild(name);
597   }
598 
599   /**
600    * Check changes.
601    *
602    * @param name the name
603    *
604    * @throws Exception the exception
605    */
606   protected void checkChanges(String name) throws Exception {
607     Boolean result = (Boolean) mbeanServer.invoke(objectNameDeployer, "tryAddServiced",
608         new String[] {name}, new String[] {String.class.getName()});
609     if (result.booleanValue()) {
610       try {
611         mbeanServer.invoke(objectNameDeployer, "check", new String[] {name},
612             new String[] {String.class.getName()});
613       } finally {
614         mbeanServer.invoke(objectNameDeployer, "removeServiced", new String[] {name},
615             new String[] {String.class.getName()});
616       }
617     }
618   }
619 
620   /**
621    * Returns the security token required to bind to a naming context.
622    *
623    * @param context the catalina context
624    *
625    * @return the security token for use with <code>ContextBindings</code>
626    */
627   protected abstract Object getNamingToken(Context context);
628 
629   /**
630    * Creates the jsp compilation context.
631    *
632    * @param name the name
633    * @param opt the opt
634    * @param sctx the sctx
635    * @param jrctx the jrctx
636    * @param classLoader the class loader
637    *
638    * @return the jsp compilation context
639    */
640   protected abstract JspCompilationContext createJspCompilationContext(String name, Options opt,
641       ServletContext sctx, JspRuntimeContext jrctx, ClassLoader classLoader);
642 
643   /**
644    * Creates the valve.
645    *
646    * @return the valve
647    */
648   protected abstract Valve createValve();
649 
650   /**
651    * Adds the filter mapping.
652    *
653    * @param filterName the filter name
654    * @param dispatcherMap the dispatcher map
655    * @param filterClass the filter class
656    * @param types the types as urls or servlet name
657    * @param results the results
658    * @param filterMapType the filter map type
659    */
660   protected void addFilterMapping(String filterName, String dispatcherMap, String filterClass,
661       String[] types, Collection<FilterMapping> results, FilterMapType filterMapType) {
662     for (String type : types) {
663       FilterMapping fm = new FilterMapping();
664       if (filterMapType == FilterMapType.URL) {
665         fm.setUrl(type);
666       } else {
667         fm.setServletName(type);
668       }
669       fm.setFilterName(filterName);
670       fm.setDispatcherMap(dispatcherMap);
671       fm.setFilterClass(filterClass);
672       results.add(fm);
673     }
674   }
675 
676 }