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