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