001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.server.webapp;
031:
032: import com.caucho.Version;
033: import com.caucho.config.*;
034: import com.caucho.i18n.CharacterEncoding;
035: import com.caucho.java.LineMap;
036: import com.caucho.java.LineMapException;
037: import com.caucho.java.ScriptStackTrace;
038: import com.caucho.server.connection.AbstractHttpRequest;
039: import com.caucho.server.connection.AbstractHttpResponse;
040: import com.caucho.server.connection.CauchoRequest;
041: import com.caucho.server.connection.CauchoResponse;
042: import com.caucho.server.dispatch.BadRequestException;
043: import com.caucho.server.util.CauchoSystem;
044: import com.caucho.util.*;
045: import com.caucho.vfs.ClientDisconnectException;
046: import com.caucho.vfs.Encoding;
047:
048: import javax.servlet.RequestDispatcher;
049: import javax.servlet.ServletException;
050: import javax.servlet.ServletRequest;
051: import javax.servlet.ServletResponse;
052: import javax.servlet.UnavailableException;
053: import javax.servlet.http.HttpServletRequest;
054: import javax.servlet.http.HttpServletResponse;
055: import java.io.CharArrayWriter;
056: import java.io.IOException;
057: import java.io.PrintWriter;
058: import java.util.HashMap;
059: import java.util.Locale;
060: import java.util.logging.Level;
061: import java.util.logging.Logger;
062:
063: /**
064: * Represents the final servlet in a filter chain.
065: */
066: public class ErrorPageManager {
067: private final static L10N L = new L10N(ErrorPageManager.class);
068: private final static Logger log = Log.open(ErrorPageManager.class);
069:
070: public static final char[] MSIE_PADDING;
071:
072: public static String REQUEST_URI = "javax.servlet.include.request_uri";
073: public static String CONTEXT_PATH = "javax.servlet.include.context_path";
074: public static String SERVLET_PATH = "javax.servlet.include.servlet_path";
075: public static String PATH_INFO = "javax.servlet.include.path_info";
076: public static String QUERY_STRING = "javax.servlet.include.query_string";
077:
078: public static String STATUS_CODE = "javax.servlet.error.status_code";
079: public static String EXCEPTION_TYPE = "javax.servlet.error.exception_type";
080: public static String MESSAGE = "javax.servlet.error.message";
081: public static String EXCEPTION = "javax.servlet.error.exception";
082: public static String ERROR_URI = "javax.servlet.error.request_uri";
083: public static String SERVLET_NAME = "javax.servlet.error.servlet_name";
084:
085: public static String JSP_EXCEPTION = "javax.servlet.jsp.jspException";
086:
087: public static String SHUTDOWN = "com.caucho.shutdown";
088:
089: private WebApp _app;
090: private WebAppContainer _appContainer;
091: private HashMap<Object, String> _errorPageMap = new HashMap<Object, String>();
092: private String _defaultLocation;
093:
094: private ErrorPageManager _parent;
095:
096: /**
097: * Create error page manager.
098: */
099: public ErrorPageManager() {
100: }
101:
102: /**
103: * Sets the manager parent.
104: */
105: public void setParent(ErrorPageManager parent) {
106: _parent = parent;
107: }
108:
109: /**
110: * Gets the manager parent.
111: */
112: public ErrorPageManager getParent() {
113: return _parent;
114: }
115:
116: /**
117: * Adds an error page.
118: */
119: public void addErrorPage(ErrorPage errorPage) {
120: if (errorPage.getExceptionType() != null) {
121: _errorPageMap.put(errorPage.getExceptionType(), errorPage
122: .getLocation());
123: } else if (errorPage.getErrorCode() < 0) {
124: _defaultLocation = errorPage.getLocation();
125: } else
126: _errorPageMap.put(new Integer(errorPage.getErrorCode()),
127: errorPage.getLocation());
128: }
129:
130: /**
131: * Sets the webApp.
132: */
133: public void setWebApp(WebApp app) {
134: _app = app;
135: }
136:
137: /**
138: * Sets the webApp container.
139: */
140: public void setWebAppContainer(WebAppContainer appContainer) {
141: _appContainer = appContainer;
142: }
143:
144: /**
145: * Displays a parse error.
146: */
147: public void sendServletError(Throwable e, ServletRequest req,
148: ServletResponse res) throws IOException {
149: HttpServletResponse response = (HttpServletResponse) res;
150: HttpServletRequest request = (HttpServletRequest) req;
151: Throwable rootExn = e;
152: Throwable errorPageExn = null;
153: LineMap lineMap = null;
154:
155: if (response instanceof AbstractHttpResponse) {
156: ((AbstractHttpResponse) response).killCache();
157: ((AbstractHttpResponse) response).setNoCache(true);
158: }
159:
160: try {
161: response.reset();
162: } catch (IllegalStateException e1) {
163: }
164:
165: if (rootExn instanceof ClientDisconnectException)
166: throw (ClientDisconnectException) rootExn;
167:
168: String location = null;
169:
170: String title = "500 Servlet Exception";
171: boolean badRequest = false;
172: boolean doStackTrace = true;
173: boolean isCompileException = false;
174: boolean isLineCompileException = false;
175: boolean isServletException = false;
176: Throwable compileException = null;
177: String lineMessage = null;
178:
179: boolean lookupErrorPage = true;
180: while (true) {
181: if (rootExn instanceof LineMapException)
182: lineMap = ((LineMapException) rootExn).getLineMap();
183:
184: if (lookupErrorPage) {
185: errorPageExn = rootExn;
186: }
187:
188: if (rootExn instanceof DisplayableException) {
189: doStackTrace = false;
190: isCompileException = true;
191: if (compileException == null)
192: compileException = rootExn;
193: } else if (rootExn instanceof CompileException) {
194: doStackTrace = false;
195: isCompileException = true;
196:
197: // use outer exception because it might have added more location info
198: /*
199: if (rootExn instanceof LineCompileException) {
200: compileException = rootExn;
201:
202: isLineCompileException = true;
203: }
204: else if (compileException == null) // ! isLineCompileException)
205: compileException = rootExn;
206: */
207: if (compileException == null) // ! isLineCompileException)
208: compileException = rootExn;
209: } else if (rootExn instanceof LineException) {
210: if (lineMessage == null)
211: lineMessage = rootExn.getMessage();
212: }
213:
214: if (rootExn instanceof BadRequestException)
215: badRequest = true;
216:
217: if (location != null || !lookupErrorPage) {
218: } else if (rootExn instanceof LineMapException
219: && rootExn instanceof ServletException
220: && !(rootExn instanceof LineCompileException)
221: && rootExn.getCause() != null) {
222: // hack to deal with JSP wrapping
223: } else if (!isServletException) {
224: // SRV.9.9.2 Servlet 2.4
225: //location = getErrorPage(rootExn, ServletException.class);
226: location = getErrorPage(rootExn);
227: isServletException = true;
228: } else {
229: location = getErrorPage(rootExn);
230: lookupErrorPage = false;
231: }
232:
233: if (location != null)
234: lookupErrorPage = false;
235:
236: Throwable cause = null;
237: if (rootExn instanceof ServletException
238: && !(rootExn instanceof LineCompileException))
239: cause = ((ServletException) rootExn).getRootCause();
240: else {
241: lookupErrorPage = false;
242: cause = rootExn.getCause();
243: }
244:
245: if (cause != null)
246: rootExn = cause;
247: else {
248: break;
249: }
250: }
251:
252: if (location == null && lookupErrorPage) {
253: location = getErrorPage(rootExn);
254: }
255:
256: if (location == null)
257: location = getErrorPage(500);
258:
259: if (location == null && _defaultLocation == null
260: && _parent != null) {
261: _parent.sendServletError(e, req, res);
262: return;
263: }
264:
265: if (isCompileException)
266: log.warning(compileException.getMessage());
267: else if (!doStackTrace)
268: log.warning(rootExn.toString());
269: else
270: log.log(Level.WARNING, e.toString(), e);
271:
272: if (badRequest) {
273: title = rootExn.getMessage();
274: doStackTrace = false;
275: badRequest = true;
276:
277: if (request instanceof CauchoRequest)
278: ((CauchoRequest) request).killKeepalive();
279:
280: response.resetBuffer();
281:
282: response.setStatus(response.SC_BAD_REQUEST, rootExn
283: .getMessage());
284:
285: } else if (rootExn instanceof UnavailableException) {
286: UnavailableException unAvail = (UnavailableException) rootExn;
287:
288: if (unAvail.isPermanent()) {
289: response.setStatus(HttpServletResponse.SC_NOT_FOUND);
290: title = "404 Not Found";
291:
292: if (location == null)
293: location = getErrorPage(response.SC_NOT_FOUND);
294: } else {
295: response
296: .setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
297: title = "503 Unavailable";
298:
299: if (unAvail.getUnavailableSeconds() > 0)
300: response.setIntHeader("Retry-After", unAvail
301: .getUnavailableSeconds());
302:
303: if (location == null)
304: location = getErrorPage(response.SC_SERVICE_UNAVAILABLE);
305: }
306: }
307: /*
308: else if (_app != null && app.getServer().isClosed()) {
309: response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
310: title = "503 Unavailable";
311: }
312: */
313: else
314: response
315: .setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
316:
317: if (location == null)
318: location = _defaultLocation;
319:
320: if (location != null) {
321: if (errorPageExn == null)
322: errorPageExn = rootExn;
323:
324: request.setAttribute(JSP_EXCEPTION, errorPageExn);
325: request.setAttribute(EXCEPTION, errorPageExn);
326: request.setAttribute(EXCEPTION_TYPE, errorPageExn
327: .getClass());
328: if (request instanceof HttpServletRequest)
329: request.setAttribute(ERROR_URI,
330: ((HttpServletRequest) request).getRequestURI());
331: if (request instanceof AbstractHttpRequest)
332: request.setAttribute(AbstractHttpRequest.SERVLET_NAME,
333: ((AbstractHttpRequest) request)
334: .getServletName());
335:
336: request.setAttribute(STATUS_CODE, new Integer(500));
337: request.setAttribute(MESSAGE, errorPageExn.getMessage());
338: /*
339: if (_app != null && _app.getServer().isClosed())
340: setAttribute(SHUTDOWN, "shutdown");
341: */
342:
343: try {
344: RequestDispatcher disp = null;
345: // can't use filters because of error pages due to filters
346: // or security.
347:
348: if (_app != null)
349: disp = _app.getRequestDispatcher(location);
350: else if (_appContainer != null)
351: disp = _appContainer.getRequestDispatcher(location);
352:
353: if (disp != null) {
354: ((RequestDispatcherImpl) disp).error(request,
355: response);
356: return;
357: }
358: } catch (Throwable e1) {
359: log.log(Level.INFO, e1.toString(), e1);
360: rootExn = e1;
361: }
362: }
363:
364: response.setContentType("text/html");
365:
366: String encoding = CharacterEncoding.getLocalEncoding();
367:
368: if (encoding != null)
369: response.setCharacterEncoding(encoding);
370: else {
371: Locale locale = Locale.getDefault();
372: if (!"ISO-8859-1".equals(Encoding.getMimeName(locale)))
373: response.setLocale(Locale.getDefault());
374: }
375:
376: PrintWriter out = response.getWriter();
377:
378: out.println("<html>");
379: if (!response.isCommitted())
380: out.println("<head><title>" + escapeHtml(title)
381: + "</title></head>");
382: out.println("<body>");
383: out.println("<h1>" + escapeHtml(title) + "</h1>");
384:
385: out.println("<code><pre>");
386:
387: /*
388: if (app != null && app.getServer().isClosed()) {
389: pw.println("Server is temporarily unavailable");
390: doStackTrace = false;
391: }
392: else
393: */
394:
395: if (log.isLoggable(Level.FINE) || !Alarm.isTest())
396: doStackTrace = true;
397:
398: if (doStackTrace) {
399: out
400: .println("<script language='javascript' type='text/javascript'>");
401: out
402: .println("function show() { document.getElementById('trace').style.display = ''; }");
403: out.println("</script>");
404: out
405: .print("<a style=\"text-decoration\" href=\"javascript:show();\">[show]</a> ");
406: }
407:
408: if (compileException instanceof DisplayableException) {
409: DisplayableException dispExn = (DisplayableException) compileException;
410:
411: dispExn.print(out);
412: } else if (compileException != null)
413: out.println(escapeHtml(compileException.getMessage()));
414: else
415: out.println(escapeHtml(rootExn.toString()));
416:
417: if (doStackTrace) {
418: out.println("<span id=\"trace\" style=\"display:none\">");
419: printStackTrace(out, lineMessage, e, rootExn, lineMap);
420: out.println("</span>");
421: }
422:
423: /*
424: *if (doStackTrace || log.isLoggable(Level.FINE)) {
425: printStackTrace(out, lineMessage, e, rootExn, lineMap);
426: }
427: */
428:
429: out.println("</pre></code>");
430:
431: String version = null;
432: if (_app == null) {
433: } else if (_app.getServer() != null
434: && _app.getServer().getServerHeader() != null) {
435: version = _app.getServer().getServerHeader();
436: } else if (CauchoSystem.isTesting()) {
437: } else
438: version = com.caucho.Version.FULL_VERSION;
439:
440: if (version != null) {
441: out.println("<p /><hr />");
442: out.println("<small>");
443:
444: out.println(version);
445:
446: out.println("</small>");
447: }
448:
449: out.println("</body></html>");
450:
451: String userAgent = request.getHeader("User-Agent");
452:
453: if (userAgent != null && userAgent.indexOf("MSIE") >= 0) {
454: out.print(MSIE_PADDING);
455: }
456:
457: out.close();
458: }
459:
460: /**
461: * Sends an HTTP error to the browser.
462: *
463: * @param code the HTTP error code
464: * @param value a string message
465: */
466: public void sendError(CauchoRequest request,
467: CauchoResponse response, int code, String message)
468: throws IOException {
469: response.resetBuffer();
470:
471: /* XXX: if we've already got an error, won't this just mask it?
472: if (responseStream.isCommitted())
473: throw new IllegalStateException("response can't sendError() after commit");
474: */
475:
476: response.setStatus(code, message);
477:
478: try {
479: if (handleErrorStatus(request, response, code, message)
480: || code == HttpServletResponse.SC_NOT_MODIFIED)
481: return;
482:
483: response.setContentType("text/html");
484: PrintWriter out = response.getWriter();
485:
486: out.println("<html>");
487: if (!response.isCommitted()) {
488: out.print("<head><title>");
489: out.print(code);
490: out.print(" ");
491: out.print(message);
492: out.println("</title></head>");
493: }
494:
495: out.println("<body>");
496: out.print("<h1>");
497: out.print(code);
498: out.print(" ");
499: out.print(message);
500: out.println("</h1>");
501:
502: if (code == HttpServletResponse.SC_NOT_FOUND) {
503: out.println(L.l("{0} was not found on this server.",
504: escapeHtml(request.getPageURI())));
505: }
506:
507: String version = null;
508: if (_app == null) {
509: } else if (_app.getServer() != null
510: && _app.getServer().getServerHeader() != null) {
511: version = _app.getServer().getServerHeader();
512: } else if (CauchoSystem.isTesting()) {
513: } else
514: version = com.caucho.Version.FULL_VERSION;
515:
516: if (version != null) {
517: out.println("<p /><hr />");
518: out.println("<small>");
519:
520: out.println(version);
521:
522: out.println("</small>");
523: }
524: out.println("</body></html>");
525:
526: String userAgent = request.getHeader("User-Agent");
527:
528: if (userAgent != null && userAgent.indexOf("MSIE") >= 0) {
529: out.write(MSIE_PADDING, 0, MSIE_PADDING.length);
530: }
531: } catch (Exception e) {
532: log.log(Level.WARNING, e.toString(), e);
533: }
534: }
535:
536: /**
537: * Handles an error status code.
538: *
539: * @return true if we've forwarded to an error page.
540: */
541: private boolean handleErrorStatus(CauchoRequest request,
542: CauchoResponse response, int code, String message)
543: throws ServletException, IOException {
544: if (code == HttpServletResponse.SC_OK
545: || code == HttpServletResponse.SC_MOVED_TEMPORARILY
546: || code == HttpServletResponse.SC_NOT_MODIFIED)
547: return false;
548:
549: if (request.getRequestDepth(0) > 16)
550: return false;
551:
552: else if (request.getAttribute(AbstractHttpRequest.ERROR_URI) != null)
553: return false;
554:
555: response.killCache();
556:
557: String location = getErrorPage(code);
558:
559: if (location == null)
560: location = _defaultLocation;
561:
562: if (location == null && _parent != null)
563: return _parent.handleErrorStatus(request, response, code,
564: message);
565:
566: if (_app == null && _appContainer == null)
567: return false;
568:
569: if (location != null
570: && !location.equals(request.getRequestURI())) {
571: request.setAttribute(AbstractHttpRequest.STATUS_CODE,
572: new Integer(code));
573: request.setAttribute(AbstractHttpRequest.MESSAGE, message);
574: request.setAttribute(AbstractHttpRequest.ERROR_URI, request
575: .getRequestURI());
576: if (request instanceof AbstractHttpRequest)
577: request.setAttribute(AbstractHttpRequest.SERVLET_NAME,
578: ((AbstractHttpRequest) request)
579: .getServletName());
580:
581: try {
582: RequestDispatcher disp = null;
583: // can't use filters because of error pages due to filters
584: // or security.
585: if (_app != null)
586: disp = _app.getRequestDispatcher(location);
587: else if (_appContainer != null)
588: disp = _appContainer.getRequestDispatcher(location);
589:
590: //disp.forward(request, this, "GET", false);
591:
592: if (disp != null)
593: ((RequestDispatcherImpl) disp).error(request,
594: response);
595: else
596: return false;
597: } catch (Throwable e) {
598: sendServletError(e, request, response);
599: }
600:
601: return true;
602: }
603:
604: return false;
605: }
606:
607: /**
608: * Returns the URL of an error page for the given exception.
609: */
610: String getErrorPage(Throwable e) {
611: return getErrorPage(e, Throwable.class);
612: }
613:
614: /**
615: * Returns the URL of an error page for the given exception.
616: */
617: String getErrorPage(Throwable e, Class limit) {
618: Class cl = e.getClass();
619: for (; cl != null; cl = cl.getSuperclass()) {
620: String location = (String) _errorPageMap.get(cl.getName());
621: if (location != null)
622: return location;
623:
624: if (cl == limit)
625: break;
626: }
627:
628: for (cl = e.getClass(); cl != null; cl = cl.getSuperclass()) {
629: String name = cl.getName();
630: int p = name.lastIndexOf('.');
631:
632: if (p > 0) {
633: name = name.substring(p + 1);
634:
635: String location = (String) _errorPageMap.get(name);
636: if (location != null)
637: return location;
638: }
639:
640: if (cl == limit)
641: break;
642: }
643:
644: return null;
645: }
646:
647: /**
648: * Returns the URL of an error page for the given exception.
649: */
650: String getErrorPage(int code) {
651: Integer key = new Integer(code);
652:
653: String location = (String) _errorPageMap.get(key);
654: if (location != null)
655: return location;
656:
657: return (String) _errorPageMap.get(new Integer(0));
658: }
659:
660: /**
661: * Escapes HTML symbols in a stack trace.
662: */
663: private void printStackTrace(PrintWriter out, String lineMessage,
664: Throwable e, Throwable rootExn, LineMap lineMap) {
665: CharArrayWriter writer = new CharArrayWriter();
666: PrintWriter pw = new PrintWriter(writer);
667:
668: if (lineMessage != null)
669: pw.println(lineMessage);
670:
671: if (lineMap != null)
672: lineMap.printStackTrace(e, pw);
673: else
674: ScriptStackTrace.printStackTrace(e, pw);
675:
676: pw.close();
677:
678: char[] array = writer.toCharArray();
679: out.print(escapeHtml(new String(array)));
680: }
681:
682: /**
683: * Escapes special symbols in a string. For example '<' becomes '<'
684: */
685: private String escapeHtml(String s) {
686: if (s == null)
687: return null;
688:
689: CharBuffer cb = new CharBuffer();
690: int lineCharacter = 0;
691: boolean startsWithSpace = false;
692:
693: for (int i = 0; i < s.length(); i++) {
694: char ch = s.charAt(i);
695:
696: lineCharacter++;
697:
698: if (ch == '<')
699: cb.append("<");
700: else if (ch == '&')
701: cb.append("&");
702: /*
703: else if (ch == '%')
704: cb.append("%25");
705: */
706: else if (ch == '\n' || ch == '\r') {
707: lineCharacter = 0;
708: cb.append(ch);
709: startsWithSpace = false;
710: } else if (lineCharacter > 70 && ch == ' '
711: && !startsWithSpace) {
712: lineCharacter = 0;
713: cb.append('\n');
714: for (; i + 1 < s.length() && s.charAt(i + 1) == ' '; i++) {
715: }
716: } else if (lineCharacter == 1 && (ch == ' ' || ch == '\t')) {
717: cb.append((char) ch);
718: startsWithSpace = true;
719: } else
720: cb.append(ch);
721: }
722:
723: return cb.toString();
724: }
725:
726: static {
727: MSIE_PADDING = ("\n\n\n\n"
728: + "<!--\n"
729: + " - Unfortunately, Microsoft has added a clever new\n"
730: + " - \"feature\" to Internet Explorer. If the text in\n"
731: + " - an error's message is \"too small\", specifically\n"
732: + " - less than 512 bytes, Internet Explorer returns\n"
733: + " - its own error message. Yes, you can turn that\n"
734: + " - off, but *surprise* it's pretty tricky to find\n"
735: + " - buried as a switch called \"smart error\n"
736: + " - messages\" That means, of course, that many of\n"
737: + " - Resin's error messages are censored by default.\n"
738: + " - And, of course, you'll be shocked to learn that\n"
739: + " - IIS always returns error messages that are long\n"
740: + " - enough to make Internet Explorer happy. The\n"
741: + " - workaround is pretty simple: pad the error\n"
742: + " - message with a big comment to push it over the\n"
743: + " - five hundred and twelve byte minimum. Of course,\n"
744: + " - that's exactly what you're reading right now.\n"
745: + " -->\n").toCharArray();
746: }
747: }
|