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.catalina.core;
18
19 import java.io.IOException;
20 import java.util.concurrent.atomic.AtomicBoolean;
21
22 import javax.servlet.DispatcherType;
23 import javax.servlet.RequestDispatcher;
24 import javax.servlet.ServletContext;
25 import javax.servlet.ServletException;
26 import javax.servlet.http.HttpServletResponse;
27
28 import org.apache.catalina.Context;
29 import org.apache.catalina.Globals;
30 import org.apache.catalina.Wrapper;
31 import org.apache.catalina.connector.ClientAbortException;
32 import org.apache.catalina.connector.Request;
33 import org.apache.catalina.connector.Response;
34 import org.apache.catalina.valves.ValveBase;
35 import org.apache.coyote.ActionCode;
36 import org.apache.juli.logging.Log;
37 import org.apache.juli.logging.LogFactory;
38 import org.apache.tomcat.util.ExceptionUtils;
39 import org.apache.tomcat.util.descriptor.web.ErrorPage;
40 import org.apache.tomcat.util.res.StringManager;
41
42 /**
43  * Valve that implements the default basic behavior for the
44  * <code>StandardHost</code> container implementation.
45  * <p>
46  * <b>USAGE CONSTRAINT</b>:  This implementation is likely to be useful only
47  * when processing HTTP requests.
48  *
49  * @author Craig R. McClanahan
50  * @author Remy Maucherat
51  */

52 final class StandardHostValve extends ValveBase {
53
54     private static final Log log = LogFactory.getLog(StandardHostValve.class);
55
56     // Saves a call to getClassLoader() on very request. Under high load these
57     // calls took just long enough to appear as a hot spot (although a very
58     // minor one) in a profiler.
59     private static final ClassLoader MY_CLASSLOADER =
60             StandardHostValve.class.getClassLoader();
61
62     static final boolean STRICT_SERVLET_COMPLIANCE;
63
64     static final boolean ACCESS_SESSION;
65
66     static {
67         STRICT_SERVLET_COMPLIANCE = Globals.STRICT_SERVLET_COMPLIANCE;
68
69         String accessSession = System.getProperty(
70                 "org.apache.catalina.core.StandardHostValve.ACCESS_SESSION");
71         if (accessSession == null) {
72             ACCESS_SESSION = STRICT_SERVLET_COMPLIANCE;
73         } else {
74             ACCESS_SESSION = Boolean.parseBoolean(accessSession);
75         }
76     }
77
78     //------------------------------------------------------ Constructor
79     public StandardHostValve() {
80         super(true);
81     }
82
83
84     // ----------------------------------------------------- Instance Variables
85
86     /**
87      * The string manager for this package.
88      */

89     private static final StringManager sm =
90         StringManager.getManager(Constants.Package);
91
92
93     // --------------------------------------------------------- Public Methods
94
95     /**
96      * Select the appropriate child Context to process this request,
97      * based on the specified request URI.  If no matching Context can
98      * be found, return an appropriate HTTP error.
99      *
100      * @param request Request to be processed
101      * @param response Response to be produced
102      *
103      * @exception IOException if an input/output error occurred
104      * @exception ServletException if a servlet error occurred
105      */

106     @Override
107     public final void invoke(Request request, Response response)
108         throws IOException, ServletException {
109
110         // Select the Context to be used for this Request
111         Context context = request.getContext();
112         if (context == null) {
113             return;
114         }
115
116         if (request.isAsyncSupported()) {
117             request.setAsyncSupported(context.getPipeline().isAsyncSupported());
118         }
119
120         boolean asyncAtStart = request.isAsync();
121
122         try {
123             context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
124
125             if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
126                 // Don't fire listeners during async processing (the listener
127                 // fired for the request that called startAsync()).
128                 // If a request init listener throws an exception, the request
129                 // is aborted.
130                 return;
131             }
132
133             // Ask this Context to process this request. Requests that are
134             // already in error must have been routed here to check for
135             // application defined error pages so DO NOT forward them to the the
136             // application for processing.
137             try {
138                 if (!response.isErrorReportRequired()) {
139                     context.getPipeline().getFirst().invoke(request, response);
140                 }
141             } catch (Throwable t) {
142                 ExceptionUtils.handleThrowable(t);
143                 container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
144                 // If a new error occurred while trying to report a previous
145                 // error allow the original error to be reported.
146                 if (!response.isErrorReportRequired()) {
147                     request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
148                     throwable(request, response, t);
149                 }
150             }
151
152             // Now that the request/response pair is back under container
153             // control lift the suspension so that the error handling can
154             // complete and/or the container can flush any remaining data
155             response.setSuspended(false);
156
157             Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
158
159             // Protect against NPEs if the context was destroyed during a
160             // long running request.
161             if (!context.getState().isAvailable()) {
162                 return;
163             }
164
165             // Look for (and render if found) an application level error page
166             if (response.isErrorReportRequired()) {
167                 // If an error has occurred that prevents further I/O, don't waste time
168                 // producing an error report that will never be read
169                 AtomicBoolean result = new AtomicBoolean(false);
170                 response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
171                 if (result.get()) {
172                     if (t != null) {
173                         throwable(request, response, t);
174                     } else {
175                         status(request, response);
176                     }
177                 }
178             }
179
180             if (!request.isAsync() && !asyncAtStart) {
181                 context.fireRequestDestroyEvent(request.getRequest());
182             }
183         } finally {
184             // Access a session (if present) to update last accessed time, based
185             // on a strict interpretation of the specification
186             if (ACCESS_SESSION) {
187                 request.getSession(false);
188             }
189
190             context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
191         }
192     }
193
194
195     // -------------------------------------------------------- Private Methods
196
197     /**
198      * Handle the HTTP status code (and corresponding message) generated
199      * while processing the specified Request to produce the specified
200      * Response.  Any exceptions that occur during generation of the error
201      * report are logged and swallowed.
202      *
203      * @param request The request being processed
204      * @param response The response being generated
205      */

206     private void status(Request request, Response response) {
207
208         int statusCode = response.getStatus();
209
210         // Handle a custom error page for this status code
211         Context context = request.getContext();
212         if (context == null) {
213             return;
214         }
215
216         /* Only look for error pages when isError() is set.
217          * isError() is set when response.sendError() is invoked. This
218          * allows custom error pages without relying on default from
219          * web.xml.
220          */

221         if (!response.isError()) {
222             return;
223         }
224
225         ErrorPage errorPage = context.findErrorPage(statusCode);
226         if (errorPage == null) {
227             // Look for a default error page
228             errorPage = context.findErrorPage(0);
229         }
230         if (errorPage != null && response.isErrorReportRequired()) {
231             response.setAppCommitted(false);
232             request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
233                               Integer.valueOf(statusCode));
234
235             String message = response.getMessage();
236             if (message == null) {
237                 message = "";
238             }
239             request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
240             request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
241                     errorPage.getLocation());
242             request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
243                     DispatcherType.ERROR);
244
245
246             Wrapper wrapper = request.getWrapper();
247             if (wrapper != null) {
248                 request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
249                                   wrapper.getName());
250             }
251             request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
252                                  request.getRequestURI());
253             if (custom(request, response, errorPage)) {
254                 response.setErrorReported();
255                 try {
256                     response.finishResponse();
257                 } catch (ClientAbortException e) {
258                     // Ignore
259                 } catch (IOException e) {
260                     container.getLogger().warn("Exception Processing " + errorPage, e);
261                 }
262             }
263         }
264     }
265
266
267     /**
268      * Handle the specified Throwable encountered while processing
269      * the specified Request to produce the specified Response.  Any
270      * exceptions that occur during generation of the exception report are
271      * logged and swallowed.
272      *
273      * @param request The request being processed
274      * @param response The response being generated
275      * @param throwable The exception that occurred (which possibly wraps
276      *  a root cause exception
277      */

278     protected void throwable(Request request, Response response,
279                              Throwable throwable) {
280         Context context = request.getContext();
281         if (context == null) {
282             return;
283         }
284
285         Throwable realError = throwable;
286
287         if (realError instanceof ServletException) {
288             realError = ((ServletException) realError).getRootCause();
289             if (realError == null) {
290                 realError = throwable;
291             }
292         }
293
294         // If this is an aborted request from a client just log it and return
295         if (realError instanceof ClientAbortException ) {
296             if (log.isDebugEnabled()) {
297                 log.debug
298                     (sm.getString("standardHost.clientAbort",
299                         realError.getCause().getMessage()));
300             }
301             return;
302         }
303
304         ErrorPage errorPage = context.findErrorPage(throwable);
305         if ((errorPage == null) && (realError != throwable)) {
306             errorPage = context.findErrorPage(realError);
307         }
308
309         if (errorPage != null) {
310             if (response.setErrorReported()) {
311                 response.setAppCommitted(false);
312                 request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
313                         errorPage.getLocation());
314                 request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
315                         DispatcherType.ERROR);
316                 request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
317                         Integer.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
318                 request.setAttribute(RequestDispatcher.ERROR_MESSAGE,
319                                   throwable.getMessage());
320                 request.setAttribute(RequestDispatcher.ERROR_EXCEPTION,
321                                   realError);
322                 Wrapper wrapper = request.getWrapper();
323                 if (wrapper != null) {
324                     request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
325                                       wrapper.getName());
326                 }
327                 request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
328                                      request.getRequestURI());
329                 request.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE,
330                                   realError.getClass());
331                 if (custom(request, response, errorPage)) {
332                     try {
333                         response.finishResponse();
334                     } catch (IOException e) {
335                         container.getLogger().warn("Exception Processing " + errorPage, e);
336                     }
337                 }
338             }
339         } else {
340             // A custom error-page has not been defined for the exception
341             // that was thrown during request processing. Check if an
342             // error-page for error code 500 was specified and if so,
343             // send that page back as the response.
344             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
345             // The response is an error
346             response.setError();
347
348             status(request, response);
349         }
350     }
351
352
353     /**
354      * Handle an HTTP status code or Java exception by forwarding control
355      * to the location included in the specified errorPage object.  It is
356      * assumed that the caller has already recorded any request attributes
357      * that are to be forwarded to this page.  Return <code>true</code> if
358      * we successfully utilized the specified error page location, or
359      * <code>false</code> if the default error report should be rendered.
360      *
361      * @param request The request being processed
362      * @param response The response being generated
363      * @param errorPage The errorPage directive we are obeying
364      */

365     private boolean custom(Request request, Response response,
366                              ErrorPage errorPage) {
367
368         if (container.getLogger().isDebugEnabled()) {
369             container.getLogger().debug("Processing " + errorPage);
370         }
371
372         try {
373             // Forward control to the specified location
374             ServletContext servletContext =
375                 request.getContext().getServletContext();
376             RequestDispatcher rd =
377                 servletContext.getRequestDispatcher(errorPage.getLocation());
378
379             if (rd == null) {
380                 container.getLogger().error(
381                     sm.getString("standardHostValue.customStatusFailed", errorPage.getLocation()));
382                 return false;
383             }
384
385             if (response.isCommitted()) {
386                 // Response is committed - including the error page is the
387                 // best we can do
388                 rd.include(request.getRequest(), response.getResponse());
389             } else {
390                 // Reset the response (keeping the real error code and message)
391                 response.resetBuffer(true);
392                 response.setContentLength(-1);
393
394                 rd.forward(request.getRequest(), response.getResponse());
395
396                 // If we forward, the response is suspended again
397                 response.setSuspended(false);
398             }
399
400             // Indicate that we have successfully processed this custom page
401             return true;
402
403         } catch (Throwable t) {
404             ExceptionUtils.handleThrowable(t);
405             // Report our failure to process this custom page
406             container.getLogger().error("Exception Processing " + errorPage, t);
407             return false;
408         }
409     }
410 }
411