1
2
3
4
5
6
7
8
9
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
67
68
69 public abstract class AbstractTomcatContainer implements TomcatContainer {
70
71
72 protected final Logger logger = LoggerFactory.getLogger(getClass());
73
74
75 private static final String NO_JSP_SERVLET = "Context '{}' does not have 'JSP' servlet";
76
77
78 protected Host host;
79
80
81 protected Connector[] connectors;
82
83
84 protected ObjectName objectNameDeployer;
85
86
87 protected MBeanServer mbeanServer;
88
89
90 public enum FilterMapType {
91
92
93 URL,
94
95
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
237
238
239
240
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
253
254
255
256
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
288 if (result.startsWith("/ROOT##")) {
289 result = result.substring(5);
290 }
291
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
354
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
415
416 for (Item item : summary.getItems().values()) {
417 item.setMissing(true);
418 }
419
420
421
422
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
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
485
486
487
488 protected void registerGlobalResourceAccess(ContextResourceLink resourceLink) {
489 ResourceLinkFactory.registerGlobalResourceAccess(ResourceResolverBean.getGlobalNamingContext(),
490 resourceLink.getName(), resourceLink.getGlobal());
491 }
492
493
494
495
496
497
498
499
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
513
514
515
516
517
518
519
520
521
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
591
592
593
594
595
596 protected Context findContextInternal(String name) {
597 return (Context) host.findChild(name);
598 }
599
600
601
602
603
604
605
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
623
624
625
626
627
628 protected abstract Object getNamingToken(Context context);
629
630
631
632
633
634
635
636
637
638
639
640
641 protected abstract JspCompilationContext createJspCompilationContext(String name, Options opt,
642 ServletContext sctx, JspRuntimeContext jrctx, ClassLoader classLoader);
643
644
645
646
647
648
649 protected abstract Valve createValve();
650
651
652
653
654
655
656
657
658
659
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 }