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 host) {
135         baseHost = host;
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 context) {
161         results.add(context);
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         if (name.matches("\\w*")) {
206           logger.info("Stopping '{}' threw this exception:", name, e);
207         } else {
208           logger.info("Stopping and threw this exception:", e);
209         }
210       }
211 
212       File appDir;
213       File docBase = Path.of(ctx.getDocBase()).toFile();
214 
215       if (!docBase.isAbsolute()) {
216         appDir = Path.of(getAppBase().getPath(), ctx.getDocBase()).toFile();
217       } else {
218         appDir = docBase;
219       }
220 
221       logger.debug("Deleting '{}'", appDir.getAbsolutePath());
222       Utils.delete(appDir);
223 
224       String warFilename = formatContextFilename(name);
225       File warFile = Path.of(getAppBase().getPath(), warFilename + ".war").toFile();
226       logger.debug("Deleting '{}'", warFile.getAbsolutePath());
227       Utils.delete(warFile);
228 
229       File configFile = getConfigFile(ctx);
230       if (configFile != null) {
231         logger.debug("Deleting '{}'", configFile.getAbsolutePath());
232         Utils.delete(configFile);
233       }
234 
235       removeInternal(name);
236     }
237   }
238 
239   /**
240    * Removes the internal.
241    *
242    * @param name the name
243    *
244    * @throws Exception the exception
245    */
246   private void removeInternal(String name) throws Exception {
247     checkChanges(name);
248   }
249 
250   @Override
251   public void installWar(String name) throws Exception {
252     checkChanges(name);
253   }
254 
255   /**
256    * Install context internal.
257    *
258    * @param name the name
259    *
260    * @throws Exception the exception
261    */
262   private void installContextInternal(String name) throws Exception {
263     checkChanges(name);
264   }
265 
266   @Override
267   public Context findContext(String name) {
268     String safeName = formatContextName(name);
269     if (safeName == null) {
270       return null;
271     }
272     Context result = findContextInternal(safeName);
273     if (result == null && safeName.isEmpty()) {
274       result = findContextInternal("/");
275     }
276     return result;
277   }
278 
279   @Override
280   public String formatContextName(String name) {
281     if (name == null) {
282       return null;
283     }
284     String result = name.trim();
285     if (!result.startsWith("/")) {
286       result = "/" + result;
287     }
288     if ("/".equals(result) || "/ROOT".equals(result)) {
289       result = "";
290     }
291     // For ROOT Parallel Deployment, remove "/ROOT"
292     if (result.startsWith("/ROOT##")) {
293       result = result.substring(5);
294     }
295     // For ROOT Parallel Usage, remove "/"
296     if (result.startsWith("/##")) {
297       result = result.substring(1);
298     }
299     return result;
300   }
301 
302   @Override
303   public String formatContextFilename(String contextName) {
304     if (contextName == null) {
305       return null;
306     }
307     String result = contextName.trim();
308     if (result.startsWith("/")) {
309       result = result.substring(1);
310     }
311     if (result.isEmpty()) {
312       return "ROOT";
313     }
314     // For a context with one or more sub-paths, replace all "/" with "#"
315     result = result.replace('/', '#');
316     // For ROOT Parallel Usage, prepend "ROOT"
317     if (result.startsWith("##")) {
318       result = "ROOT" + result;
319     }
320     return result;
321   }
322 
323   @Override
324   public void discardWorkDir(Context context) {
325     if (context instanceof StandardContext standardContext) {
326       String path = standardContext.getWorkPath();
327       logger.info("Discarding '{}'", path);
328       Utils.delete(Path.of(path, "org").toFile());
329     } else {
330       logger.error("context '{}' is not an instance of '{}', expected StandardContext",
331           context.getName(), context.getClass().getName());
332     }
333   }
334 
335   @Override
336   public String getServletFileNameForJsp(Context context, String jspName) {
337     String servletName = null;
338 
339     ServletConfig servletConfig = (ServletConfig) context.findChild("jsp");
340     if (servletConfig != null) {
341       ServletContext sctx = context.getServletContext();
342       Options opt = new EmbeddedServletOptions(servletConfig, sctx);
343       JspRuntimeContext jrctx = new JspRuntimeContext(sctx, opt);
344       JspCompilationContext jcctx = createJspCompilationContext(jspName, opt, sctx, jrctx, null);
345       servletName = jcctx.getServletJavaFileName();
346     } else {
347       logger.error(NO_JSP_SERVLET, context.getName());
348     }
349     return servletName;
350   }
351 
352   @Override
353   public void recompileJsps(Context context, Summary summary, List<String> names) {
354     ServletConfig servletConfig = (ServletConfig) context.findChild("jsp");
355     if (servletConfig != null) {
356       if (summary != null) {
357         synchronized (servletConfig) {
358           ServletContext sctx = context.getServletContext();
359           Options opt = new EmbeddedServletOptions(servletConfig, sctx);
360 
361           JspRuntimeContext jrctx = new JspRuntimeContext(sctx, opt);
362           /*
363            * we need to pass context classloader here, so the jsps can reference /WEB-INF/classes
364            * and /WEB-INF/lib. JspCompilationContext would only take URLClassLoader, so we fake it
365            */
366           try (URLClassLoader classLoader =
367               new URLClassLoader(new URL[0], context.getLoader().getClassLoader())) {
368             for (String name : names) {
369               long time = System.currentTimeMillis();
370               JspCompilationContext jcctx =
371                   createJspCompilationContext(name, opt, sctx, jrctx, classLoader);
372               ClassLoader prevCl = ClassUtils.overrideThreadContextClassLoader(classLoader);
373               try {
374                 Item item = summary.getItems().get(name);
375                 if (item != null) {
376                   try {
377                     Compiler compiler = jcctx.createCompiler();
378                     compiler.compile();
379                     item.setState(Item.STATE_READY);
380                     item.setException(null);
381                     logger.info("Compiled '{}': OK", name);
382                   } catch (Exception e) {
383                     item.setState(Item.STATE_FAILED);
384                     item.setException(e);
385                     logger.error("Compiled '{}': FAILED", name, e);
386                   }
387                   item.setCompileTime(System.currentTimeMillis() - time);
388                 } else {
389                   logger.error("{} is not on the summary list, ignored", name);
390                 }
391               } finally {
392                 ClassUtils.overrideThreadContextClassLoader(prevCl);
393               }
394             }
395           } catch (IOException e) {
396             this.logger.error("", e);
397           } finally {
398             jrctx.destroy();
399           }
400         }
401       } else {
402         logger.error("summary is null for '{}', request ignored", context.getName());
403       }
404     } else {
405       logger.error(NO_JSP_SERVLET, context.getName());
406     }
407   }
408 
409   @Override
410   public void listContextJsps(Context context, Summary summary, boolean compile) {
411     ServletConfig servletConfig = (ServletConfig) context.findChild("jsp");
412     if (servletConfig != null) {
413       synchronized (servletConfig) {
414         ServletContext sctx = context.getServletContext();
415         Options opt = new EmbeddedServletOptions(servletConfig, sctx);
416 
417         JspRuntimeContext jrctx = new JspRuntimeContext(sctx, opt);
418         try {
419           if (summary.getItems() == null) {
420             summary.setItems(new HashMap<>());
421           }
422 
423           /*
424            * mark all items as missing
425            */
426           for (Item item : summary.getItems().values()) {
427             item.setMissing(true);
428           }
429 
430           /*
431            * we need to pass context classloader here, so the jsps can reference /WEB-INF/classes
432            * and /WEB-INF/lib. JspCompilationContext would only take URLClassLoader, so we fake it
433            */
434           try (URLClassLoader urlcl =
435               new URLClassLoader(new URL[0], context.getLoader().getClassLoader())) {
436 
437             compileItem("/", opt, context, jrctx, summary, urlcl, 0, compile);
438           } catch (IOException e) {
439             this.logger.error("", e);
440           }
441         } finally {
442           jrctx.destroy();
443         }
444       }
445 
446       //
447       // delete "missing" items by keeping "not missing" ones
448       //
449       Map<String, Item> hashMap = new HashMap<>();
450       for (Entry<String, Item> entry : summary.getItems().entrySet()) {
451         if (!entry.getValue().isMissing()) {
452           hashMap.put(entry.getKey(), entry.getValue());
453         }
454       }
455 
456       summary.setItems(hashMap);
457     } else {
458       logger.error(NO_JSP_SERVLET, context.getName());
459     }
460   }
461 
462   @Override
463   public boolean getAvailable(Context context) {
464     return context.getState().isAvailable();
465   }
466 
467   @Override
468   public File getConfigFile(Context context) {
469     URL configUrl = context.getConfigFile();
470     if (configUrl != null) {
471       try {
472         URI configUri = configUrl.toURI();
473         if ("file".equals(configUri.getScheme())) {
474           return Path.of(configUri).toFile();
475         }
476       } catch (URISyntaxException ex) {
477         logger.error("Could not convert URL to URI: '{}'", configUrl, ex);
478       }
479     }
480     return null;
481   }
482 
483   @Override
484   public void bindToContext(Context context) throws NamingException {
485     changeContextBinding(context, true);
486   }
487 
488   @Override
489   public void unbindFromContext(Context context) throws NamingException {
490     changeContextBinding(context, false);
491   }
492 
493   /**
494    * Register access to global resources.
495    *
496    * @param resourceLink the resource link
497    */
498   protected void registerGlobalResourceAccess(ContextResourceLink resourceLink) {
499     ResourceLinkFactory.registerGlobalResourceAccess(ResourceResolverBean.getGlobalNamingContext(),
500         resourceLink.getName(), resourceLink.getGlobal());
501   }
502 
503   /**
504    * Change context binding.
505    *
506    * @param context the context
507    * @param bind the bind
508    *
509    * @throws NamingException the naming exception
510    */
511   private void changeContextBinding(Context context, boolean bind) throws NamingException {
512     Object token = getNamingToken(context);
513     ClassLoader loader = Thread.currentThread().getContextClassLoader();
514     if (bind) {
515       ContextBindings.bindClassLoader(context, token, loader);
516     } else {
517       ContextBindings.unbindClassLoader(context, token, loader);
518     }
519   }
520 
521   /**
522    * Lists and optionally compiles a directory recursively.
523    *
524    * @param jspName name of JSP file or directory to be listed and compiled.
525    * @param opt the JSP compiler options
526    * @param ctx the context
527    * @param jrctx the runtime context used to create the compilation context
528    * @param summary the summary in which the output is stored
529    * @param classLoader the classloader used by the compiler
530    * @param level the depth in the tree at which the item was encountered
531    * @param compile whether or not to compile the item or just to check whether it's out of date
532    */
533   protected void compileItem(String jspName, Options opt, Context ctx, JspRuntimeContext jrctx,
534       Summary summary, URLClassLoader classLoader, int level, boolean compile) {
535     ServletContext sctx = ctx.getServletContext();
536     Set<String> paths = sctx.getResourcePaths(jspName);
537 
538     if (paths != null) {
539       for (String name : paths) {
540         boolean isJsp =
541             name.endsWith(".jsp") || name.endsWith(".jspx") || opt.getJspConfig().isJspPage(name);
542 
543         if (isJsp) {
544           JspCompilationContext jcctx =
545               createJspCompilationContext(name, opt, sctx, jrctx, classLoader);
546           ClassLoader prevCl = ClassUtils.overrideThreadContextClassLoader(classLoader);
547           try {
548             Item item = summary.getItems().get(name);
549 
550             if (item == null) {
551               item = new Item();
552               item.setName(name);
553             }
554 
555             item.setLevel(level);
556             item.setCompileTime(-1);
557 
558             Long[] objects = this.getResourceAttributes(name, ctx);
559             item.setSize(objects[0]);
560             item.setLastModified(objects[1]);
561 
562             long time = System.currentTimeMillis();
563             try {
564               Compiler compiler = jcctx.createCompiler();
565               if (compile) {
566                 compiler.compile();
567                 item.setState(Item.STATE_READY);
568                 item.setException(null);
569               } else if (!compiler.isOutDated()) {
570                 item.setState(Item.STATE_READY);
571                 item.setException(null);
572               } else if (item.getState() != Item.STATE_FAILED) {
573                 item.setState(Item.STATE_OOD);
574                 item.setException(null);
575               }
576               logger.info("Compiled '{}': OK", name);
577             } catch (Exception e) {
578               item.setState(Item.STATE_FAILED);
579               item.setException(e);
580               logger.info("Compiled '{}': FAILED", name, e);
581             }
582             if (compile) {
583               item.setCompileTime(System.currentTimeMillis() - time);
584             }
585             item.setMissing(false);
586             summary.putItem(name, item);
587           } finally {
588             ClassUtils.overrideThreadContextClassLoader(prevCl);
589           }
590         } else {
591           compileItem(name, opt, ctx, jrctx, summary, classLoader, level + 1, compile);
592         }
593       }
594     } else {
595       logger.debug("getResourcePaths() is null for '{}'. Empty dir? Or Tomcat bug?", jspName);
596     }
597   }
598 
599   /**
600    * Find context internal.
601    *
602    * @param name the context name
603    *
604    * @return the context
605    */
606   protected Context findContextInternal(String name) {
607     return (Context) host.findChild(name);
608   }
609 
610   /**
611    * Check changes.
612    *
613    * @param name the name
614    *
615    * @throws Exception the exception
616    */
617   protected void checkChanges(String name) throws Exception {
618     Boolean result = (Boolean) mbeanServer.invoke(objectNameDeployer, "tryAddServiced",
619         new String[] {name}, new String[] {String.class.getName()});
620     if (result.booleanValue()) {
621       try {
622         mbeanServer.invoke(objectNameDeployer, "check", new String[] {name},
623             new String[] {String.class.getName()});
624       } finally {
625         mbeanServer.invoke(objectNameDeployer, "removeServiced", new String[] {name},
626             new String[] {String.class.getName()});
627       }
628     }
629   }
630 
631   /**
632    * Returns the security token required to bind to a naming context.
633    *
634    * @param context the catalina context
635    *
636    * @return the security token for use with <code>ContextBindings</code>
637    */
638   protected abstract Object getNamingToken(Context context);
639 
640   /**
641    * Creates the jsp compilation context.
642    *
643    * @param name the name
644    * @param opt the opt
645    * @param sctx the sctx
646    * @param jrctx the jrctx
647    * @param classLoader the class loader
648    *
649    * @return the jsp compilation context
650    */
651   protected abstract JspCompilationContext createJspCompilationContext(String name, Options opt,
652       ServletContext sctx, JspRuntimeContext jrctx, ClassLoader classLoader);
653 
654   /**
655    * Creates the valve.
656    *
657    * @return the valve
658    */
659   protected abstract Valve createValve();
660 
661   /**
662    * Adds the filter mapping.
663    *
664    * @param filterName the filter name
665    * @param dispatcherMap the dispatcher map
666    * @param filterClass the filter class
667    * @param types the types as urls or servlet name
668    * @param results the results
669    * @param filterMapType the filter map type
670    */
671   protected void addFilterMapping(String filterName, String dispatcherMap, String filterClass,
672       String[] types, Collection<FilterMapping> results, FilterMapType filterMapType) {
673     for (String type : types) {
674       FilterMapping fm = new FilterMapping();
675       if (filterMapType == FilterMapType.URL) {
676         fm.setUrl(type);
677       } else {
678         fm.setServletName(type);
679       }
680       fm.setFilterName(filterName);
681       fm.setDispatcherMap(dispatcherMap);
682       fm.setFilterClass(filterClass);
683       results.add(fm);
684     }
685   }
686 
687 }