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