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.valves;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.io.Writer;
25 import java.util.Scanner;
26 import java.util.concurrent.atomic.AtomicBoolean;
27
28 import javax.servlet.RequestDispatcher;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.HttpServletResponse;
31
32 import org.apache.catalina.connector.Request;
33 import org.apache.catalina.connector.Response;
34 import org.apache.catalina.util.ErrorPageSupport;
35 import org.apache.catalina.util.IOTools;
36 import org.apache.catalina.util.ServerInfo;
37 import org.apache.catalina.util.TomcatCSS;
38 import org.apache.coyote.ActionCode;
39 import org.apache.tomcat.util.ExceptionUtils;
40 import org.apache.tomcat.util.descriptor.web.ErrorPage;
41 import org.apache.tomcat.util.res.StringManager;
42 import org.apache.tomcat.util.security.Escape;
43
44 /**
45  * <p>Implementation of a Valve that outputs HTML error pages.</p>
46  *
47  * <p>This Valve should be attached at the Host level, although it will work
48  * if attached to a Context.</p>
49  *
50  * <p>HTML code from the Cocoon 2 project.</p>
51  *
52  * @author Remy Maucherat
53  * @author Craig R. McClanahan
54  * @author <a href="mailto:nicolaken@supereva.it">Nicola Ken Barozzi</a> Aisa
55  * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
56  * @author Yoav Shapira
57  */

58 public class ErrorReportValve extends ValveBase {
59
60     private boolean showReport = true;
61
62     private boolean showServerInfo = true;
63
64     private final ErrorPageSupport errorPageSupport = new ErrorPageSupport();
65
66
67     //------------------------------------------------------ Constructor
68
69     public ErrorReportValve() {
70         super(true);
71     }
72
73
74     // --------------------------------------------------------- Public Methods
75
76     /**
77      * Invoke the next Valve in the sequence. When the invoke returns, check
78      * the response state. If the status code is greater than or equal to 400
79      * or an uncaught exception was thrown then the error handling will be
80      * triggered.
81      *
82      * @param request The servlet request to be processed
83      * @param response The servlet response to be created
84      *
85      * @exception IOException if an input/output error occurs
86      * @exception ServletException if a servlet error occurs
87      */

88     @Override
89     public void invoke(Request request, Response response) throws IOException, ServletException {
90
91         // Perform the request
92         getNext().invoke(request, response);
93
94         if (response.isCommitted()) {
95             if (response.setErrorReported()) {
96                 // Error wasn't previously reported but we can't write an error
97                 // page because the response has already been committed. Attempt
98                 // to flush any data that is still to be written to the client.
99                 try {
100                     response.flushBuffer();
101                 } catch (Throwable t) {
102                     ExceptionUtils.handleThrowable(t);
103                 }
104                 // Close immediately to signal to the client that something went
105                 // wrong
106                 response.getCoyoteResponse().action(ActionCode.CLOSE_NOW,
107                         request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
108             }
109             return;
110         }
111
112         Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
113
114         // If an async request is in progress and is not going to end once this
115         // container thread finishes, do not process any error page here.
116         if (request.isAsync() && !request.isAsyncCompleting()) {
117             return;
118         }
119
120         if (throwable != null && !response.isError()) {
121             // Make sure that the necessary methods have been called on the
122             // response. (It is possible a component may just have set the
123             // Throwable. Tomcat won't do that but other components might.)
124             // These are safe to call at this point as we know that the response
125             // has not been committed.
126             response.reset();
127             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
128         }
129
130         // One way or another, response.sendError() will have been called before
131         // execution reaches this point and suspended the response. Need to
132         // reverse that so this valve can write to the response.
133         response.setSuspended(false);
134
135         try {
136             report(request, response, throwable);
137         } catch (Throwable tt) {
138             ExceptionUtils.handleThrowable(tt);
139         }
140     }
141
142
143     // ------------------------------------------------------ Protected Methods
144
145
146     /**
147      * Prints out an error report.
148      *
149      * @param request The request being processed
150      * @param response The response being generated
151      * @param throwable The exception that occurred (which possibly wraps
152      *  a root cause exception
153      */

154     protected void report(Request request, Response response, Throwable throwable) {
155
156         int statusCode = response.getStatus();
157
158         // Do nothing on a 1xx, 2xx and 3xx status
159         // Do nothing if anything has been written already
160         // Do nothing if the response hasn't been explicitly marked as in error
161         //    and that error has not been reported.
162         if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
163             return;
164         }
165
166         // If an error has occurred that prevents further I/O, don't waste time
167         // producing an error report that will never be read
168         AtomicBoolean result = new AtomicBoolean(false);
169         response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
170         if (!result.get()) {
171             return;
172         }
173
174         ErrorPage errorPage = null;
175         if (throwable != null) {
176             errorPage = errorPageSupport.find(throwable);
177         }
178         if (errorPage == null) {
179             errorPage = errorPageSupport.find(statusCode);
180         }
181         if (errorPage == null) {
182             // Default error page
183             errorPage = errorPageSupport.find(0);
184         }
185
186
187         if (errorPage != null) {
188             if (sendErrorPage(errorPage.getLocation(), response)) {
189                 // If the page was sent successfully, don't write the standard
190                 // error page.
191                 return;
192             }
193         }
194
195         String message = Escape.htmlElementContent(response.getMessage());
196         if (message == null) {
197             if (throwable != null) {
198                 String exceptionMessage = throwable.getMessage();
199                 if (exceptionMessage != null && exceptionMessage.length() > 0) {
200                     message = Escape.htmlElementContent((new Scanner(exceptionMessage)).nextLine());
201                 }
202             }
203             if (message == null) {
204                 message = "";
205             }
206         }
207
208         // Do nothing if there is no reason phrase for the specified status code and
209         // no error message provided
210         String reason = null;
211         String description = null;
212         StringManager smClient = StringManager.getManager(
213                 Constants.Package, request.getLocales());
214         response.setLocale(smClient.getLocale());
215         try {
216             reason = smClient.getString("http." + statusCode + ".reason");
217             description = smClient.getString("http." + statusCode + ".desc");
218         } catch (Throwable t) {
219             ExceptionUtils.handleThrowable(t);
220         }
221         if (reason == null || description == null) {
222             if (message.isEmpty()) {
223                 return;
224             } else {
225                 reason = smClient.getString("errorReportValve.unknownReason");
226                 description = smClient.getString("errorReportValve.noDescription");
227             }
228         }
229
230         StringBuilder sb = new StringBuilder();
231
232         sb.append("<!doctype html><html lang=\"");
233         sb.append(smClient.getLocale().getLanguage()).append("\">");
234         sb.append("<head>");
235         sb.append("<title>");
236         sb.append(smClient.getString("errorReportValve.statusHeader",
237                 String.valueOf(statusCode), reason));
238         sb.append("</title>");
239         sb.append("<style type=\"text/css\">");
240         sb.append(TomcatCSS.TOMCAT_CSS);
241         sb.append("</style>");
242         sb.append("</head><body>");
243         sb.append("<h1>");
244         sb.append(smClient.getString("errorReportValve.statusHeader",
245                 String.valueOf(statusCode), reason)).append("</h1>");
246         if (isShowReport()) {
247             sb.append("<hr class=\"line\" />");
248             sb.append("<p><b>");
249             sb.append(smClient.getString("errorReportValve.type"));
250             sb.append("</b> ");
251             if (throwable != null) {
252                 sb.append(smClient.getString("errorReportValve.exceptionReport"));
253             } else {
254                 sb.append(smClient.getString("errorReportValve.statusReport"));
255             }
256             sb.append("</p>");
257             if (!message.isEmpty()) {
258                 sb.append("<p><b>");
259                 sb.append(smClient.getString("errorReportValve.message"));
260                 sb.append("</b> ");
261                 sb.append(message).append("</p>");
262             }
263             sb.append("<p><b>");
264             sb.append(smClient.getString("errorReportValve.description"));
265             sb.append("</b> ");
266             sb.append(description);
267             sb.append("</p>");
268             if (throwable != null) {
269                 String stackTrace = getPartialServletStackTrace(throwable);
270                 sb.append("<p><b>");
271                 sb.append(smClient.getString("errorReportValve.exception"));
272                 sb.append("</b></p><pre>");
273                 sb.append(Escape.htmlElementContent(stackTrace));
274                 sb.append("</pre>");
275
276                 int loops = 0;
277                 Throwable rootCause = throwable.getCause();
278                 while (rootCause != null && (loops < 10)) {
279                     stackTrace = getPartialServletStackTrace(rootCause);
280                     sb.append("<p><b>");
281                     sb.append(smClient.getString("errorReportValve.rootCause"));
282                     sb.append("</b></p><pre>");
283                     sb.append(Escape.htmlElementContent(stackTrace));
284                     sb.append("</pre>");
285                     // In case root cause is somehow heavily nested
286                     rootCause = rootCause.getCause();
287                     loops++;
288                 }
289
290                 sb.append("<p><b>");
291                 sb.append(smClient.getString("errorReportValve.note"));
292                 sb.append("</b> ");
293                 sb.append(smClient.getString("errorReportValve.rootCauseInLogs"));
294                 sb.append("</p>");
295
296             }
297             sb.append("<hr class=\"line\" />");
298         }
299         if (isShowServerInfo()) {
300             sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");
301         }
302         sb.append("</body></html>");
303
304         try {
305             try {
306                 response.setContentType("text/html");
307                 response.setCharacterEncoding("utf-8");
308             } catch (Throwable t) {
309                 ExceptionUtils.handleThrowable(t);
310                 if (container.getLogger().isDebugEnabled()) {
311                     container.getLogger().debug("status.setContentType", t);
312                 }
313             }
314             Writer writer = response.getReporter();
315             if (writer != null) {
316                 // If writer is null, it's an indication that the response has
317                 // been hard committed already, which should never happen
318                 writer.write(sb.toString());
319                 response.finishResponse();
320             }
321         } catch (IOException e) {
322             // Ignore
323         } catch (IllegalStateException e) {
324             // Ignore
325         }
326
327     }
328
329
330     /**
331      * Print out a partial servlet stack trace (truncating at the last
332      * occurrence of javax.servlet.).
333      * @param t The stack trace to process
334      * @return the stack trace relative to the application layer
335      */

336     protected String getPartialServletStackTrace(Throwable t) {
337         StringBuilder trace = new StringBuilder();
338         trace.append(t.toString()).append(System.lineSeparator());
339         StackTraceElement[] elements = t.getStackTrace();
340         int pos = elements.length;
341         for (int i = elements.length - 1; i >= 0; i--) {
342             if ((elements[i].getClassName().startsWith
343                  ("org.apache.catalina.core.ApplicationFilterChain"))
344                 && (elements[i].getMethodName().equals("internalDoFilter"))) {
345                 pos = i;
346                 break;
347             }
348         }
349         for (int i = 0; i < pos; i++) {
350             if (!(elements[i].getClassName().startsWith
351                   ("org.apache.catalina.core."))) {
352                 trace.append('\t').append(elements[i].toString()).append(System.lineSeparator());
353             }
354         }
355         return trace.toString();
356     }
357
358
359     private boolean sendErrorPage(String location, Response response) {
360         File file = new File(location);
361         if (!file.isAbsolute()) {
362             file = new File(getContainer().getCatalinaBase(), location);
363         }
364         if (!file.isFile() || !file.canRead()) {
365             getContainer().getLogger().warn(
366                     sm.getString("errorReportValve.errorPageNotFound", location));
367             return false;
368         }
369
370         // Hard coded for now. Consider making this optional. At Valve level or
371         // page level?
372         response.setContentType("text/html");
373         response.setCharacterEncoding("UTF-8");
374
375         try (OutputStream os = response.getOutputStream();
376                 InputStream is = new FileInputStream(file);){
377             IOTools.flow(is, os);
378         } catch (IOException e) {
379             getContainer().getLogger().warn(
380                     sm.getString("errorReportValve.errorPageIOException", location), e);
381             return false;
382         }
383
384         return true;
385     }
386
387
388     /**
389      * Enables/Disables full error reports
390      *
391      * @param showReport <code>true</code> to show full error data
392      */

393     public void setShowReport(boolean showReport) {
394         this.showReport = showReport;
395     }
396
397     public boolean isShowReport() {
398         return showReport;
399     }
400
401     /**
402      * Enables/Disables server info on error pages
403      *
404      * @param showServerInfo <code>true</code> to show server info
405      */

406     public void setShowServerInfo(boolean showServerInfo) {
407         this.showServerInfo = showServerInfo;
408     }
409
410     public boolean isShowServerInfo() {
411         return showServerInfo;
412     }
413
414
415     public boolean setProperty(String name, String value) {
416         if (name.startsWith("errorCode.")) {
417             int code = Integer.parseInt(name.substring(10));
418             ErrorPage ep = new ErrorPage();
419             ep.setErrorCode(code);
420             ep.setLocation(value);
421             errorPageSupport.add(ep);
422             return true;
423         } else if (name.startsWith("exceptionType.")) {
424             String className = name.substring(14);
425             ErrorPage ep = new ErrorPage();
426             ep.setExceptionType(className);
427             ep.setLocation(value);
428             errorPageSupport.add(ep);
429             return true;
430         }
431         return false;
432     }
433
434     public String getProperty(String name) {
435         String result;
436         if (name.startsWith("errorCode.")) {
437             int code = Integer.parseInt(name.substring(10));
438             ErrorPage ep = errorPageSupport.find(code);
439             if (ep == null) {
440                 result = null;
441             } else {
442                 result = ep.getLocation();
443             }
444         } else if (name.startsWith("exceptionType.")) {
445             String className = name.substring(14);
446             ErrorPage ep = errorPageSupport.find(className);
447             if (ep == null) {
448                 result = null;
449             } else {
450                 result = ep.getLocation();
451             }
452         } else {
453             result = null;
454         }
455         return result;
456     }
457 }
458