001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * arussel 018 */ 019package org.nuxeo.ecm.platform.web.common.exceptionhandling; 020 021import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.DISABLE_REDIRECT_REQUEST_KEY; 022import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.FORCE_ANONYMOUS_LOGIN; 023import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGINCONTEXT_KEY; 024import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.LOGOUT_PAGE; 025import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.REQUESTED_URL; 026import static org.nuxeo.ecm.platform.ui.web.auth.NXAuthConstants.SECURITY_ERROR; 027 028import java.io.IOException; 029import java.io.PrintWriter; 030import java.io.StringWriter; 031import java.security.Principal; 032import java.util.HashMap; 033import java.util.Locale; 034import java.util.Map; 035import java.util.Optional; 036import java.util.ResourceBundle; 037 038import javax.security.auth.Subject; 039import javax.security.auth.login.LoginContext; 040import javax.servlet.RequestDispatcher; 041import javax.servlet.ServletException; 042import javax.servlet.http.HttpServletRequest; 043import javax.servlet.http.HttpServletResponse; 044 045import org.apache.commons.logging.Log; 046import org.apache.commons.logging.LogFactory; 047import org.nuxeo.common.utils.URIUtils; 048import org.nuxeo.common.utils.i18n.I18NUtils; 049import org.nuxeo.ecm.core.api.NuxeoException; 050import org.nuxeo.ecm.core.api.NuxeoPrincipal; 051import org.nuxeo.ecm.core.io.download.DownloadHelper; 052import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter; 053import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService; 054import org.nuxeo.ecm.platform.web.common.exceptionhandling.descriptor.ErrorHandler; 055import org.nuxeo.runtime.api.Framework; 056 057/** 058 * @author arussel 059 */ 060public class DefaultNuxeoExceptionHandler implements NuxeoExceptionHandler { 061 062 private static final Log log = LogFactory.getLog(DefaultNuxeoExceptionHandler.class); 063 064 protected NuxeoExceptionHandlerParameters parameters; 065 066 @Override 067 public void setParameters(NuxeoExceptionHandlerParameters parameters) { 068 this.parameters = parameters; 069 } 070 071 /** 072 * Puts a marker in request to avoid looping over the exception handling mechanism 073 * 074 * @throws ServletException if request has already been marked as handled. The initial exception is then wrapped. 075 */ 076 077 protected void startHandlingException(HttpServletRequest request, HttpServletResponse response, Throwable t) 078 throws ServletException { 079 if (request.getAttribute(EXCEPTION_HANDLER_MARKER) == null) { 080 if (log.isDebugEnabled()) { 081 log.debug("Initial exception", t); 082 } 083 // mark request as already processed by this mechanism to avoid 084 // looping over it 085 request.setAttribute(EXCEPTION_HANDLER_MARKER, true); 086 // disable further redirect by nuxeo url system 087 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, true); 088 } else { 089 // avoid looping over exception mechanism 090 throw new ServletException(t); 091 } 092 } 093 094 @Override 095 public void handleException(HttpServletRequest request, HttpServletResponse response, Throwable t) 096 throws IOException, ServletException { 097 098 Throwable unwrappedException = ExceptionHelper.unwrapException(t); 099 100 // check for Anonymous case 101 if (ExceptionHelper.isSecurityError(unwrappedException)) { 102 Principal principal = getPrincipal(request); 103 if (principal instanceof NuxeoPrincipal) { 104 NuxeoPrincipal nuxeoPrincipal = (NuxeoPrincipal) principal; 105 if (nuxeoPrincipal.isAnonymous()) { 106 // redirect to login than to requested page 107 if (handleAnonymousException(request, response)) { 108 return; 109 } 110 } 111 } 112 } 113 114 startHandlingException(request, response, t); 115 try { 116 ErrorHandler handler = getHandler(t); 117 Integer code = handler.getCode(); 118 int status = code == null ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : code.intValue(); 119 parameters.getListener().startHandling(t, request, response); 120 121 StringWriter swriter = new StringWriter(); 122 PrintWriter pwriter = new PrintWriter(swriter); 123 t.printStackTrace(pwriter); 124 String stackTrace = swriter.getBuffer().toString(); 125 if (DownloadHelper.isClientAbortError(t)) { 126 DownloadHelper.logClientAbort(t); 127 } else if (status < HttpServletResponse.SC_INTERNAL_SERVER_ERROR) { // 500 128 log.debug(t.getMessage(), t); 129 } else { 130 log.error(stackTrace); 131 parameters.getLogger().error(stackTrace); 132 } 133 134 parameters.getListener().beforeSetErrorPageAttribute(unwrappedException, request, response); 135 request.setAttribute("exception_message", unwrappedException.getLocalizedMessage()); 136 request.setAttribute("user_message", getUserMessage(handler.getMessage(), request.getLocale())); 137 request.setAttribute("securityError", ExceptionHelper.isSecurityError(unwrappedException)); 138 request.setAttribute("messageBundle", ResourceBundle.getBundle(parameters.getBundleName(), 139 request.getLocale(), Thread.currentThread().getContextClassLoader())); 140 String dumpedRequest = parameters.getRequestDumper().getDump(request); 141 if (status >= HttpServletResponse.SC_INTERNAL_SERVER_ERROR) { // 500 142 parameters.getLogger().error(dumpedRequest); 143 } 144 request.setAttribute("isDevModeSet", Framework.isDevModeSet()); 145 if (Framework.isDevModeSet()) { 146 request.setAttribute("stackTrace", stackTrace); 147 request.setAttribute("request_dump", dumpedRequest); 148 } 149 150 parameters.getListener().beforeForwardToErrorPage(unwrappedException, request, response); 151 if (!response.isCommitted()) { 152 // The JSP error page needs the response Writer but somebody may already have retrieved 153 // the OutputStream and usage of these two can't be mixed. So we reset the response. 154 response.reset(); 155 response.setStatus(status); 156 String errorPage = handler.getPage(); 157 errorPage = (errorPage == null) ? parameters.getDefaultErrorPage() : errorPage; 158 RequestDispatcher requestDispatcher = request.getRequestDispatcher(errorPage); 159 if (requestDispatcher != null) { 160 requestDispatcher.forward(request, response); 161 } else { 162 log.error("Cannot forward to error page, no RequestDispatcher found for errorPage=" + errorPage 163 + " handler=" + handler); 164 } 165 parameters.getListener().responseComplete(); 166 } else if (!DownloadHelper.isClientAbortError(t)){ 167 // do not throw an error, just log it: afterDispatch needs to be called 168 log.error("Cannot forward to error page: response is already committed", t); 169 } 170 parameters.getListener().afterDispatch(unwrappedException, request, response); 171 } catch (ServletException e) { 172 throw e; 173 } catch (RuntimeException | IOException e) { 174 throw new ServletException(e); 175 } 176 } 177 178 @Override 179 public boolean handleAnonymousException(HttpServletRequest request, HttpServletResponse response) 180 throws IOException, ServletException { 181 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime() 182 .getComponent( 183 PluggableAuthenticationService.NAME); 184 if (authService == null) { 185 return false; 186 } 187 authService.invalidateSession(request); 188 String loginURL = getLoginURL(request); 189 if (loginURL == null) { 190 return false; 191 } 192 if (!response.isCommitted()) { 193 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, true); 194 response.sendRedirect(loginURL); 195 parameters.getListener().responseComplete(); 196 } else { 197 log.error("Cannot redirect to login page: response is already committed"); 198 } 199 return true; 200 } 201 202 @Override 203 public String getLoginURL(HttpServletRequest request) { 204 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime() 205 .getComponent( 206 PluggableAuthenticationService.NAME); 207 Map<String, String> urlParameters = new HashMap<>(); 208 urlParameters.put(SECURITY_ERROR, "true"); 209 urlParameters.put(FORCE_ANONYMOUS_LOGIN, "true"); 210 if (request.getAttribute(REQUESTED_URL) != null) { 211 urlParameters.put(REQUESTED_URL, (String) request.getAttribute(REQUESTED_URL)); 212 } else { 213 urlParameters.put(REQUESTED_URL, NuxeoAuthenticationFilter.getRequestedUrl(request)); 214 } 215 String baseURL = authService.getBaseURL(request) + LOGOUT_PAGE; 216 return URIUtils.addParametersToURIQuery(baseURL, urlParameters); 217 } 218 219 protected ErrorHandler getHandler(Throwable t) { 220 Throwable throwable = ExceptionHelper.unwrapException(t); 221 String className = throwable.getClass().getName(); 222 for (ErrorHandler handler : parameters.getHandlers()) { 223 if (handler.getError() != null && className.matches(handler.getError())) { 224 return handler; 225 } 226 } 227 throw new NuxeoException("No error handler set."); 228 } 229 230 protected Object getUserMessage(String messageKey, Locale locale) { 231 return I18NUtils.getMessageString(parameters.getBundleName(), messageKey, null, locale); 232 } 233 234 protected Principal getPrincipal(HttpServletRequest request) { 235 Principal principal = request.getUserPrincipal(); 236 if (principal == null) { 237 LoginContext loginContext = (LoginContext) request.getAttribute(LOGINCONTEXT_KEY); 238 principal = Optional.ofNullable(loginContext) 239 .map(LoginContext::getSubject) 240 .map(Subject::getPrincipals) 241 .flatMap(principals -> principals.stream().findFirst()) 242 .orElse(null); 243 } 244 return principal; 245 } 246 247}