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