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