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}