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 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
241
242
243
244
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
257
258
259
260
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
292 if (result.startsWith("/ROOT##")) {
293 result = result.substring(5);
294 }
295
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
315 result = result.replace('/', '#');
316
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
364
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
425
426 for (Item item : summary.getItems().values()) {
427 item.setMissing(true);
428 }
429
430
431
432
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
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
495
496
497
498 protected void registerGlobalResourceAccess(ContextResourceLink resourceLink) {
499 ResourceLinkFactory.registerGlobalResourceAccess(ResourceResolverBean.getGlobalNamingContext(),
500 resourceLink.getName(), resourceLink.getGlobal());
501 }
502
503
504
505
506
507
508
509
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
523
524
525
526
527
528
529
530
531
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
601
602
603
604
605
606 protected Context findContextInternal(String name) {
607 return (Context) host.findChild(name);
608 }
609
610
611
612
613
614
615
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
633
634
635
636
637
638 protected abstract Object getNamingToken(Context context);
639
640
641
642
643
644
645
646
647
648
649
650
651 protected abstract JspCompilationContext createJspCompilationContext(String name, Options opt,
652 ServletContext sctx, JspRuntimeContext jrctx, ClassLoader classLoader);
653
654
655
656
657
658
659 protected abstract Valve createValve();
660
661
662
663
664
665
666
667
668
669
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 }