1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.jasper.compiler;
18
19 import java.io.File;
20 import java.io.FileNotFoundException;
21 import java.io.FilePermission;
22 import java.io.IOException;
23 import java.net.URISyntaxException;
24 import java.net.URL;
25 import java.net.URLClassLoader;
26 import java.security.CodeSource;
27 import java.security.PermissionCollection;
28 import java.security.Policy;
29 import java.security.cert.Certificate;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.atomic.AtomicInteger;
35
36 import javax.servlet.ServletContext;
37 import javax.servlet.ServletException;
38
39 import org.apache.jasper.Constants;
40 import org.apache.jasper.JspCompilationContext;
41 import org.apache.jasper.Options;
42 import org.apache.jasper.runtime.ExceptionUtils;
43 import org.apache.jasper.servlet.JspServletWrapper;
44 import org.apache.jasper.util.FastRemovalDequeue;
45 import org.apache.juli.logging.Log;
46 import org.apache.juli.logging.LogFactory;
47
48
49 /**
50 * Class for tracking JSP compile time file dependencies when the
51 * >%@include file="..."%< directive is used.
52 *
53 * A background thread periodically checks the files a JSP page
54 * is dependent upon. If a dependent file changes the JSP page
55 * which included it is recompiled.
56 *
57 * Only used if a web application context is a directory.
58 *
59 * @author Glenn L. Nielsen
60 */
61 public final class JspRuntimeContext {
62
63 /**
64 * Logger
65 */
66 private final Log log = LogFactory.getLog(JspRuntimeContext.class); // must not be static
67
68 /**
69 * Counts how many times the webapp's JSPs have been reloaded.
70 */
71 private final AtomicInteger jspReloadCount = new AtomicInteger(0);
72
73 /**
74 * Counts how many times JSPs have been unloaded in this webapp.
75 */
76 private final AtomicInteger jspUnloadCount = new AtomicInteger(0);
77
78 // ----------------------------------------------------------- Constructors
79
80 /**
81 * Create a JspRuntimeContext for a web application context.
82 *
83 * Loads in any previously generated dependencies from file.
84 *
85 * @param context ServletContext for web application
86 * @param options The main Jasper options
87 */
88 public JspRuntimeContext(ServletContext context, Options options) {
89
90 this.context = context;
91 this.options = options;
92
93 // Get the parent class loader
94 ClassLoader loader = Thread.currentThread().getContextClassLoader();
95 if (loader == null) {
96 loader = this.getClass().getClassLoader();
97 }
98
99 if (log.isDebugEnabled()) {
100 if (loader != null) {
101 log.debug(Localizer.getMessage("jsp.message.parent_class_loader_is",
102 loader.toString()));
103 } else {
104 log.debug(Localizer.getMessage("jsp.message.parent_class_loader_is",
105 "<none>"));
106 }
107 }
108
109 parentClassLoader = loader;
110 classpath = initClassPath();
111
112 if (context instanceof org.apache.jasper.servlet.JspCServletContext) {
113 codeSource = null;
114 permissionCollection = null;
115 return;
116 }
117
118 if (Constants.IS_SECURITY_ENABLED) {
119 SecurityHolder holder = initSecurity();
120 codeSource = holder.cs;
121 permissionCollection = holder.pc;
122 } else {
123 codeSource = null;
124 permissionCollection = null;
125 }
126
127 // If this web application context is running from a
128 // directory, start the background compilation thread
129 String appBase = context.getRealPath("/");
130 if (!options.getDevelopment()
131 && appBase != null
132 && options.getCheckInterval() > 0) {
133 lastCompileCheck = System.currentTimeMillis();
134 }
135
136 if (options.getMaxLoadedJsps() > 0) {
137 jspQueue = new FastRemovalDequeue<>(options.getMaxLoadedJsps());
138 if (log.isDebugEnabled()) {
139 log.debug(Localizer.getMessage("jsp.message.jsp_queue_created",
140 "" + options.getMaxLoadedJsps(), context.getContextPath()));
141 }
142 }
143
144 /* Init parameter is in seconds, locally we use milliseconds */
145 jspIdleTimeout = options.getJspIdleTimeout() * 1000;
146 }
147
148 // ----------------------------------------------------- Instance Variables
149
150 /**
151 * This web applications ServletContext
152 */
153 private final ServletContext context;
154 private final Options options;
155 private final ClassLoader parentClassLoader;
156 private final PermissionCollection permissionCollection;
157 private final CodeSource codeSource;
158 private final String classpath;
159 private volatile long lastCompileCheck = -1L;
160 private volatile long lastJspQueueUpdate = System.currentTimeMillis();
161 /* JSP idle timeout in milliseconds */
162 private long jspIdleTimeout;
163
164 /**
165 * Maps JSP pages to their JspServletWrapper's
166 */
167 private final Map<String, JspServletWrapper> jsps = new ConcurrentHashMap<>();
168
169 /**
170 * Keeps JSP pages ordered by last access.
171 */
172 private FastRemovalDequeue<JspServletWrapper> jspQueue = null;
173
174 /**
175 * Map of class name to associated source map. This is maintained here as
176 * multiple JSPs can depend on the same file (included JSP, tag file, etc.)
177 * so a web application scoped Map is required.
178 */
179 private final Map<String,SmapStratum> smaps = new ConcurrentHashMap<>();
180
181 /**
182 * Flag that indicates if a background compilation check is in progress.
183 */
184 private volatile boolean compileCheckInProgress = false;
185
186
187 // ------------------------------------------------------ Public Methods
188
189 /**
190 * Add a new JspServletWrapper.
191 *
192 * @param jspUri JSP URI
193 * @param jsw Servlet wrapper for JSP
194 */
195 public void addWrapper(String jspUri, JspServletWrapper jsw) {
196 jsps.put(jspUri, jsw);
197 }
198
199 /**
200 * Get an already existing JspServletWrapper.
201 *
202 * @param jspUri JSP URI
203 * @return JspServletWrapper for JSP
204 */
205 public JspServletWrapper getWrapper(String jspUri) {
206 return jsps.get(jspUri);
207 }
208
209 /**
210 * Remove a JspServletWrapper.
211 *
212 * @param jspUri JSP URI of JspServletWrapper to remove
213 */
214 public void removeWrapper(String jspUri) {
215 jsps.remove(jspUri);
216 }
217
218 /**
219 * Push a newly compiled JspServletWrapper into the queue at first
220 * execution of jsp. Destroy any JSP that has been replaced in the queue.
221 *
222 * @param jsw Servlet wrapper for jsp.
223 * @return an unloadHandle that can be pushed to front of queue at later execution times.
224 * */
225 public FastRemovalDequeue<JspServletWrapper>.Entry push(JspServletWrapper jsw) {
226 if (log.isTraceEnabled()) {
227 log.trace(Localizer.getMessage("jsp.message.jsp_added",
228 jsw.getJspUri(), context.getContextPath()));
229 }
230 FastRemovalDequeue<JspServletWrapper>.Entry entry = jspQueue.push(jsw);
231 JspServletWrapper replaced = entry.getReplaced();
232 if (replaced != null) {
233 if (log.isDebugEnabled()) {
234 log.debug(Localizer.getMessage("jsp.message.jsp_removed_excess",
235 replaced.getJspUri(), context.getContextPath()));
236 }
237 unloadJspServletWrapper(replaced);
238 entry.clearReplaced();
239 }
240 return entry;
241 }
242
243 /**
244 * Push unloadHandle for JspServletWrapper to front of the queue.
245 *
246 * @param unloadHandle the unloadHandle for the jsp.
247 * */
248 public void makeYoungest(FastRemovalDequeue<JspServletWrapper>.Entry unloadHandle) {
249 if (log.isTraceEnabled()) {
250 JspServletWrapper jsw = unloadHandle.getContent();
251 log.trace(Localizer.getMessage("jsp.message.jsp_queue_update",
252 jsw.getJspUri(), context.getContextPath()));
253 }
254 jspQueue.moveFirst(unloadHandle);
255 }
256
257 /**
258 * Returns the number of JSPs for which JspServletWrappers exist, i.e.,
259 * the number of JSPs that have been loaded into the webapp.
260 *
261 * @return The number of JSPs that have been loaded into the webapp
262 */
263 public int getJspCount() {
264 return jsps.size();
265 }
266
267 /**
268 * Get the SecurityManager Policy CodeSource for this web
269 * application context.
270 *
271 * @return CodeSource for JSP
272 */
273 public CodeSource getCodeSource() {
274 return codeSource;
275 }
276
277 /**
278 * Get the parent ClassLoader.
279 *
280 * @return ClassLoader parent
281 */
282 public ClassLoader getParentClassLoader() {
283 return parentClassLoader;
284 }
285
286 /**
287 * Get the SecurityManager PermissionCollection for this
288 * web application context.
289 *
290 * @return PermissionCollection permissions
291 */
292 public PermissionCollection getPermissionCollection() {
293 return permissionCollection;
294 }
295
296 /**
297 * Process a "destroy" event for this web application context.
298 */
299 public void destroy() {
300 for (JspServletWrapper jspServletWrapper : jsps.values()) {
301 jspServletWrapper.destroy();
302 }
303 }
304
305 /**
306 * Increments the JSP reload counter.
307 */
308 public void incrementJspReloadCount() {
309 jspReloadCount.incrementAndGet();
310 }
311
312 /**
313 * Resets the JSP reload counter.
314 *
315 * @param count Value to which to reset the JSP reload counter
316 */
317 public void setJspReloadCount(int count) {
318 jspReloadCount.set(count);
319 }
320
321 /**
322 * Gets the current value of the JSP reload counter.
323 *
324 * @return The current value of the JSP reload counter
325 */
326 public int getJspReloadCount() {
327 return jspReloadCount.intValue();
328 }
329
330 /**
331 * Gets the number of JSPs that are in the JSP limiter queue
332 *
333 * @return The number of JSPs (in the webapp with which this JspServlet is
334 * associated) that are in the JSP limiter queue
335 */
336 public int getJspQueueLength() {
337 if (jspQueue != null) {
338 return jspQueue.getSize();
339 }
340 return -1;
341 }
342
343 /**
344 * Gets the number of JSPs that have been unloaded.
345 *
346 * @return The number of JSPs (in the webapp with which this JspServlet is
347 * associated) that have been unloaded
348 */
349 public int getJspUnloadCount() {
350 return jspUnloadCount.intValue();
351 }
352
353
354 /**
355 * Method used by background thread to check the JSP dependencies
356 * registered with this class for JSP's.
357 */
358 public void checkCompile() {
359
360 if (lastCompileCheck < 0) {
361 // Checking was disabled
362 return;
363 }
364 long now = System.currentTimeMillis();
365 if (now > (lastCompileCheck + (options.getCheckInterval() * 1000L))) {
366 lastCompileCheck = now;
367 } else {
368 return;
369 }
370
371 List<JspServletWrapper> wrappersToReload = new ArrayList<>();
372 // Tell JspServletWrapper to ignore the reload attribute while this
373 // check is in progress. See BZ 62603.
374 compileCheckInProgress = true;
375
376 Object [] wrappers = jsps.values().toArray();
377 for (int i = 0; i < wrappers.length; i++ ) {
378 JspServletWrapper jsw = (JspServletWrapper)wrappers[i];
379 JspCompilationContext ctxt = jsw.getJspEngineContext();
380 // Sync on JspServletWrapper when calling ctxt.compile()
381 synchronized(jsw) {
382 try {
383 ctxt.compile();
384 if (jsw.getReload()) {
385 wrappersToReload.add(jsw);
386 }
387 } catch (FileNotFoundException ex) {
388 ctxt.incrementRemoved();
389 } catch (Throwable t) {
390 ExceptionUtils.handleThrowable(t);
391 jsw.getServletContext().log(Localizer.getMessage("jsp.error.backgroundCompilationFailed"), t);
392 }
393 }
394 }
395
396 // See BZ 62603.
397 // OK to process reload flag now.
398 compileCheckInProgress = false;
399 // Ensure all servlets and tags that need to be reloaded, are reloaded.
400 for (JspServletWrapper jsw : wrappersToReload) {
401 // Triggers reload
402 try {
403 if (jsw.isTagFile()) {
404 // Although this is a public method, all other paths to this
405 // method use this sync and it is required to prevent race
406 // conditions during the reload.
407 synchronized (this) {
408 jsw.loadTagFile();
409 }
410 } else {
411 jsw.getServlet();
412 }
413 } catch (ServletException e) {
414 jsw.getServletContext().log(Localizer.getMessage("jsp.error.reload"), e);
415 }
416 }
417 }
418
419 public boolean isCompileCheckInProgress() {
420 return compileCheckInProgress;
421 }
422
423 /**
424 * @return the classpath that is passed off to the Java compiler.
425 */
426 public String getClassPath() {
427 return classpath;
428 }
429
430 /**
431 * @return Last time the update background task has run
432 */
433 public long getLastJspQueueUpdate() {
434 return lastJspQueueUpdate;
435 }
436
437
438 public Map<String,SmapStratum> getSmaps() {
439 return smaps;
440 }
441
442
443 // -------------------------------------------------------- Private Methods
444
445 /**
446 * Method used to initialize classpath for compiles.
447 * @return the compilation classpath
448 */
449 private String initClassPath() {
450
451 StringBuilder cpath = new StringBuilder();
452
453 if (parentClassLoader instanceof URLClassLoader) {
454 URL [] urls = ((URLClassLoader)parentClassLoader).getURLs();
455
456 for (int i = 0; i < urls.length; i++) {
457 // Tomcat can use URLs other than file URLs. However, a protocol
458 // other than file: will generate a bad file system path, so
459 // only add file: protocol URLs to the classpath.
460
461 if (urls[i].getProtocol().equals("file") ) {
462 try {
463 // Need to decode the URL, primarily to convert %20
464 // sequences back to spaces
465 String decoded = urls[i].toURI().getPath();
466 cpath.append(decoded + File.pathSeparator);
467 } catch (URISyntaxException e) {
468 log.warn(Localizer.getMessage("jsp.warning.classpathUrl"), e);
469 }
470 }
471 }
472 }
473
474 cpath.append(options.getScratchDir() + File.pathSeparator);
475
476 String cp = (String) context.getAttribute(Constants.SERVLET_CLASSPATH);
477 if (cp == null || cp.equals("")) {
478 cp = options.getClassPath();
479 }
480
481 String path = cpath.toString() + cp;
482
483 if(log.isDebugEnabled()) {
484 log.debug("Compilation classpath initialized: " + path);
485 }
486 return path;
487 }
488
489 /**
490 * Helper class to allow initSecurity() to return two items
491 */
492 private static class SecurityHolder{
493 private final CodeSource cs;
494 private final PermissionCollection pc;
495 private SecurityHolder(CodeSource cs, PermissionCollection pc){
496 this.cs = cs;
497 this.pc = pc;
498 }
499 }
500 /**
501 * Method used to initialize SecurityManager data.
502 */
503 private SecurityHolder initSecurity() {
504
505 // Setup the PermissionCollection for this web app context
506 // based on the permissions configured for the root of the
507 // web app context directory, then add a file read permission
508 // for that directory.
509 Policy policy = Policy.getPolicy();
510 CodeSource source = null;
511 PermissionCollection permissions = null;
512 if( policy != null ) {
513 try {
514 // Get the permissions for the web app context
515 String docBase = context.getRealPath("/");
516 if( docBase == null ) {
517 docBase = options.getScratchDir().toString();
518 }
519 String codeBase = docBase;
520 if (!codeBase.endsWith(File.separator)){
521 codeBase = codeBase + File.separator;
522 }
523 File contextDir = new File(codeBase);
524 URL url = contextDir.getCanonicalFile().toURI().toURL();
525 source = new CodeSource(url,(Certificate[])null);
526 permissions = policy.getPermissions(source);
527
528 // Create a file read permission for web app context directory
529 if (!docBase.endsWith(File.separator)){
530 permissions.add
531 (new FilePermission(docBase,"read"));
532 docBase = docBase + File.separator;
533 } else {
534 permissions.add
535 (new FilePermission
536 (docBase.substring(0,docBase.length() - 1),"read"));
537 }
538 docBase = docBase + "-";
539 permissions.add(new FilePermission(docBase,"read"));
540
541 // Spec says apps should have read/write for their temp
542 // directory. This is fine, as no security sensitive files, at
543 // least any that the app doesn't have full control of anyway,
544 // will be written here.
545 String workDir = options.getScratchDir().toString();
546 if (!workDir.endsWith(File.separator)){
547 permissions.add
548 (new FilePermission(workDir,"read,write"));
549 workDir = workDir + File.separator;
550 }
551 workDir = workDir + "-";
552 permissions.add(new FilePermission(
553 workDir,"read,write,delete"));
554
555 // Allow the JSP to access org.apache.jasper.runtime.HttpJspBase
556 permissions.add( new RuntimePermission(
557 "accessClassInPackage.org.apache.jasper.runtime") );
558 } catch (RuntimeException | IOException e) {
559 context.log(Localizer.getMessage("jsp.error.security"), e);
560 }
561 }
562 return new SecurityHolder(source, permissions);
563 }
564
565 private void unloadJspServletWrapper(JspServletWrapper jsw) {
566 removeWrapper(jsw.getJspUri());
567 synchronized(jsw) {
568 jsw.destroy();
569 }
570 jspUnloadCount.incrementAndGet();
571 }
572
573
574 /**
575 * Method used by background thread to check if any JSP's should be unloaded.
576 */
577 public void checkUnload() {
578
579 if (log.isTraceEnabled()) {
580 int queueLength = -1;
581 if (jspQueue != null) {
582 queueLength = jspQueue.getSize();
583 }
584 log.trace(Localizer.getMessage("jsp.message.jsp_unload_check",
585 context.getContextPath(), "" + jsps.size(), "" + queueLength));
586 }
587 long now = System.currentTimeMillis();
588 if (jspIdleTimeout > 0) {
589 long unloadBefore = now - jspIdleTimeout;
590 Object [] wrappers = jsps.values().toArray();
591 for (int i = 0; i < wrappers.length; i++ ) {
592 JspServletWrapper jsw = (JspServletWrapper)wrappers[i];
593 synchronized(jsw) {
594 if (jsw.getLastUsageTime() < unloadBefore) {
595 if (log.isDebugEnabled()) {
596 log.debug(Localizer.getMessage("jsp.message.jsp_removed_idle",
597 jsw.getJspUri(), context.getContextPath(),
598 "" + (now-jsw.getLastUsageTime())));
599 }
600 if (jspQueue != null) {
601 jspQueue.remove(jsw.getUnloadHandle());
602 }
603 unloadJspServletWrapper(jsw);
604 }
605 }
606 }
607 }
608 lastJspQueueUpdate = now;
609 }
610 }
611