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