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.core.api.WrappedException; 053import org.nuxeo.ecm.platform.ui.web.auth.CachableUserIdentificationInfo; 054import org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter; 055import org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService; 056import org.nuxeo.ecm.platform.web.common.exceptionhandling.descriptor.ErrorHandler; 057import org.nuxeo.runtime.api.Framework; 058 059/** 060 * @author arussel 061 */ 062public class DefaultNuxeoExceptionHandler implements NuxeoExceptionHandler { 063 064 private static final Log log = LogFactory.getLog(DefaultNuxeoExceptionHandler.class); 065 066 protected NuxeoExceptionHandlerParameters parameters; 067 068 @Override 069 public void setParameters(NuxeoExceptionHandlerParameters parameters) { 070 this.parameters = parameters; 071 } 072 073 /** 074 * Puts a marker in request to avoid looping over the exception handling mechanism 075 * 076 * @throws ServletException if request has already been marked as handled. The initial exception is then wrapped. 077 */ 078 079 protected void startHandlingException(HttpServletRequest request, HttpServletResponse response, Throwable t) 080 throws ServletException { 081 if (request.getAttribute(EXCEPTION_HANDLER_MARKER) == null) { 082 if (log.isDebugEnabled()) { 083 log.debug("Initial exception", t); 084 } 085 // mark request as already processed by this mechanism to avoid 086 // looping over it 087 request.setAttribute(EXCEPTION_HANDLER_MARKER, true); 088 // disable further redirect by nuxeo url system 089 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, true); 090 } else { 091 // avoid looping over exception mechanism 092 throw new ServletException(t); 093 } 094 } 095 096 @Override 097 public void handleException(HttpServletRequest request, HttpServletResponse response, Throwable t) 098 throws IOException, ServletException { 099 100 Throwable unwrappedException = ExceptionHelper.unwrapException(t); 101 102 // check for Anonymous case 103 if (ExceptionHelper.isSecurityError(unwrappedException)) { 104 Principal principal = getPrincipal(request); 105 if (principal instanceof NuxeoPrincipal) { 106 NuxeoPrincipal nuxeoPrincipal = (NuxeoPrincipal) principal; 107 if (nuxeoPrincipal.isAnonymous()) { 108 // redirect to login than to requested page 109 if (handleAnonymousException(request, response)) { 110 return; 111 } 112 } 113 } 114 } 115 116 startHandlingException(request, response, t); 117 try { 118 ErrorHandler handler = getHandler(t); 119 Integer code = handler.getCode(); 120 int status = code == null ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : code.intValue(); 121 parameters.getListener().startHandling(t, request, response); 122 123 StringWriter swriter = new StringWriter(); 124 PrintWriter pwriter = new PrintWriter(swriter); 125 t.printStackTrace(pwriter); 126 String stackTrace = swriter.getBuffer().toString(); 127 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 { 167 // do not throw an error, just log it: afterDispatch needs to 168 // be called, and sometimes the initial error is a 169 // ClientAbortException 170 log.error("Cannot forward to error page: " + "response is already committed"); 171 } 172 parameters.getListener().afterDispatch(unwrappedException, request, response); 173 } catch (ServletException e) { 174 throw e; 175 } catch (RuntimeException | IOException e) { 176 throw new ServletException(e); 177 } 178 } 179 180 @Override 181 public boolean handleAnonymousException(HttpServletRequest request, HttpServletResponse response) 182 throws IOException, ServletException { 183 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime() 184 .getComponent( 185 PluggableAuthenticationService.NAME); 186 if (authService == null) { 187 return false; 188 } 189 authService.invalidateSession(request); 190 String loginURL = getLoginURL(request); 191 if (loginURL == null) { 192 return false; 193 } 194 if (!response.isCommitted()) { 195 request.setAttribute(DISABLE_REDIRECT_REQUEST_KEY, true); 196 response.sendRedirect(loginURL); 197 parameters.getListener().responseComplete(); 198 } else { 199 log.error("Cannot redirect to login page: response is already committed"); 200 } 201 return true; 202 } 203 204 @Override 205 public String getLoginURL(HttpServletRequest request) { 206 PluggableAuthenticationService authService = (PluggableAuthenticationService) Framework.getRuntime() 207 .getComponent( 208 PluggableAuthenticationService.NAME); 209 Map<String, String> urlParameters = new HashMap<>(); 210 urlParameters.put(SECURITY_ERROR, "true"); 211 urlParameters.put(FORCE_ANONYMOUS_LOGIN, "true"); 212 if (request.getAttribute(REQUESTED_URL) != null) { 213 urlParameters.put(REQUESTED_URL, (String) request.getAttribute(REQUESTED_URL)); 214 } else { 215 urlParameters.put(REQUESTED_URL, NuxeoAuthenticationFilter.getRequestedUrl(request)); 216 } 217 String baseURL = authService.getBaseURL(request) + LOGOUT_PAGE; 218 return URIUtils.addParametersToURIQuery(baseURL, urlParameters); 219 } 220 221 protected ErrorHandler getHandler(Throwable t) { 222 Throwable throwable = ExceptionHelper.unwrapException(t); 223 String className = null; 224 if (throwable instanceof WrappedException) { 225 WrappedException wrappedException = (WrappedException) throwable; 226 className = wrappedException.getClassName(); 227 } else { 228 className = throwable.getClass().getName(); 229 } 230 for (ErrorHandler handler : parameters.getHandlers()) { 231 if (handler.getError() != null && className.matches(handler.getError())) { 232 return handler; 233 } 234 } 235 throw new NuxeoException("No error handler set."); 236 } 237 238 protected Object getUserMessage(String messageKey, Locale locale) { 239 return I18NUtils.getMessageString(parameters.getBundleName(), messageKey, null, locale); 240 } 241 242 protected Principal getPrincipal(HttpServletRequest request) { 243 Principal principal = request.getUserPrincipal(); 244 if (principal == null) { 245 LoginContext loginContext = (LoginContext) request.getAttribute(LOGINCONTEXT_KEY); 246 principal = Optional.ofNullable(loginContext) 247 .map(LoginContext::getSubject) 248 .map(Subject::getPrincipals) 249 .flatMap(principals -> principals.stream().findFirst()) 250 .orElse(null); 251 } 252 return principal; 253 } 254 255}