001/*
002 * (C) Copyright 2006-2018 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 *     Thierry Delprat
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.platform.web.common.requestcontroller.filter;
021
022import java.io.IOException;
023import java.security.Principal;
024import java.util.Locale;
025import java.util.Map.Entry;
026import java.util.TimeZone;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.locks.Lock;
029import java.util.concurrent.locks.ReentrantLock;
030
031import javax.servlet.Filter;
032import javax.servlet.FilterChain;
033import javax.servlet.FilterConfig;
034import javax.servlet.ServletException;
035import javax.servlet.ServletRequest;
036import javax.servlet.ServletResponse;
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpServletResponse;
039import javax.servlet.http.HttpSession;
040
041import org.apache.commons.lang3.time.FastDateFormat;
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.nuxeo.ecm.core.api.NuxeoException;
045import org.nuxeo.ecm.core.io.download.DownloadHelper;
046import org.nuxeo.ecm.platform.web.common.ServletHelper;
047import org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerManager;
048import org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestFilterConfig;
049import org.nuxeo.runtime.api.Framework;
050import org.nuxeo.runtime.transaction.TransactionHelper;
051import org.nuxeo.runtime.transaction.TransactionRuntimeException;
052
053/**
054 * Filter to handle transactions, response buffering, and request synchronization.
055 *
056 * @author tiry
057 */
058public class NuxeoRequestControllerFilter implements Filter {
059
060    private static final Log log = LogFactory.getLog(NuxeoRequestControllerFilter.class);
061
062    protected static final String SESSION_LOCK_KEY = "NuxeoSessionLockKey";
063
064    protected static final String SYNCED_REQUEST_FLAG = "NuxeoSessionAlreadySync";
065
066    protected static final int LOCK_TIMEOUT_S = 120;
067
068    // formatted http Expires: Thu, 01 Dec 1994 16:00:00 GMT
069    public static final FastDateFormat HTTP_EXPIRES_DATE_FORMAT = FastDateFormat.getInstance(
070            "EEE, dd MMM yyyy HH:mm:ss z", TimeZone.getTimeZone("GMT"), Locale.US);
071
072    @Override
073    public void init(FilterConfig filterConfig) {
074        // nothing to do
075    }
076
077    @Override
078    public void destroy() {
079        // nothing to do
080    }
081
082    public static String doFormatLogMessage(HttpServletRequest request, String info) {
083        String remoteHost = RemoteHostGuessExtractor.getRemoteHost(request);
084        Principal principal = request.getUserPrincipal();
085        String principalName = principal != null ? principal.getName() : "none";
086        String uri = request.getRequestURI();
087        HttpSession session = request.getSession(false);
088        String sessionId = session != null ? session.getId() : "none";
089        String threadName = Thread.currentThread().getName();
090        return "remote=" + remoteHost + ",principal=" + principalName + ",uri=" + uri + ",session=" + sessionId
091                + ",thread=" + threadName + ",info=" + info;
092    }
093
094    @Override
095    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
096            throws IOException, ServletException {
097        HttpServletRequest request = (HttpServletRequest) servletRequest;
098        HttpServletResponse response = (HttpServletResponse) servletResponse;
099        if (log.isDebugEnabled()) {
100            log.debug(doFormatLogMessage(request, "Entering NuxeoRequestController filter"));
101        }
102
103        RequestControllerManager rcm = Framework.getService(RequestControllerManager.class);
104        RequestFilterConfig config = rcm.getConfigForRequest(request);
105        boolean useSync = config.needSynchronization();
106        boolean useTx = config.needTransaction();
107        boolean useBuffer = config.needTransactionBuffered();
108        if (log.isDebugEnabled()) {
109            log.debug(doFormatLogMessage(request,
110                    "Handling request with tx=" + useTx + " and sync=" + useSync + " and buffer=" + useBuffer));
111        }
112        addHeaders(request, response, config);
113
114        boolean sessionSynched = false;
115        boolean txStarted = false;
116        boolean buffered = false;
117        try {
118            ServletHelper.setServletContext(request.getServletContext());
119            if (useSync) {
120                sessionSynched = simpleSyncOnSession(request);
121            }
122            if (useTx) {
123                if (!TransactionHelper.isTransactionActiveOrMarkedRollback()) {
124                    txStarted = ServletHelper.startTransaction(request);
125                    if (!txStarted) {
126                        throw new ServletException("Failed to start transaction");
127                    }
128                }
129                if (useBuffer) {
130                    response = new BufferingHttpServletResponse(response);
131                    buffered = true;
132                }
133            }
134            chain.doFilter(request, response);
135        } catch (IOException | ServletException | RuntimeException e) {
136            // Don't call response.sendError, because it commits the response
137            // which prevents NuxeoExceptionFilter from returning a custom error page.
138            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
139            if (TransactionHelper.isTransactionActive()) {
140                TransactionHelper.setTransactionRollbackOnly();
141            }
142            if (DownloadHelper.isClientAbortError(e)) {
143                DownloadHelper.logClientAbort(e);
144            } else if (e instanceof RuntimeException) { // NOSONAR
145                throw new ServletException(e);
146            } else {
147                throw e; // IOException | ServletException
148            }
149        } finally {
150            try {
151                if (txStarted) {
152                    try {
153                        TransactionHelper.commitOrRollbackTransaction();
154                    } catch (TransactionRuntimeException e) {
155                        // commit failed, report this to the client before stopping buffering
156                        // Don't call response.sendError, because it commits the response
157                        // which prevents NuxeoExceptionFilter from returning a custom error page.
158                        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
159                        log.error(e, e); // don't rethrow inside finally
160                    }
161                }
162            } finally {
163                if (buffered) {
164                    ((BufferingHttpServletResponse) response).stopBuffering();
165                }
166                if (sessionSynched) {
167                    simpleReleaseSyncOnSession(request);
168                }
169                ServletHelper.removeServletContext();
170            }
171        }
172
173        if (log.isDebugEnabled()) {
174            log.debug(doFormatLogMessage(request, "Exiting NuxeoRequestController filter"));
175        }
176    }
177
178    /**
179     * Synchronizes the HttpSession.
180     * <p>
181     * Uses a {@link Lock} object in the HttpSession and locks it. If HttpSession is not created, exits without locking
182     * anything.
183     */
184    public static boolean simpleSyncOnSession(HttpServletRequest request) {
185        HttpSession session = request.getSession(false);
186        if (session == null) {
187            if (log.isDebugEnabled()) {
188                log.debug(doFormatLogMessage(request, "HttpSession does not exist, this request won't be synched"));
189            }
190            return false;
191        }
192
193        if (log.isDebugEnabled()) {
194            log.debug(doFormatLogMessage(request, "Trying to sync on session "));
195        }
196
197        if (request.getAttribute(SYNCED_REQUEST_FLAG) != null) {
198            if (log.isWarnEnabled()) {
199                log.warn(doFormatLogMessage(request,
200                        "Request has already be synced, filter is reentrant, exiting without locking"));
201            }
202            return false;
203        }
204
205        Lock lock = (Lock) session.getAttribute(SESSION_LOCK_KEY);
206        if (lock == null) {
207            lock = new ReentrantLock();
208            session.setAttribute(SESSION_LOCK_KEY, lock);
209        }
210
211        boolean locked = false;
212        try {
213            locked = lock.tryLock(LOCK_TIMEOUT_S, TimeUnit.SECONDS);
214        } catch (InterruptedException e) {
215            Thread.currentThread().interrupt();
216            throw new NuxeoException(e);
217        }
218
219        if (locked) {
220            request.setAttribute(SYNCED_REQUEST_FLAG, true);
221            if (log.isDebugEnabled()) {
222                log.debug(doFormatLogMessage(request, "Request synced on session"));
223            }
224        } else {
225            if (log.isDebugEnabled()) {
226                log.debug(doFormatLogMessage(request, "Sync timeout"));
227            }
228        }
229
230        return locked;
231    }
232
233    /**
234     * Releases the {@link Lock} if present in the HttpSession.
235     */
236    public static boolean simpleReleaseSyncOnSession(HttpServletRequest request) {
237        HttpSession session = request.getSession(false);
238        if (session == null) {
239            if (log.isDebugEnabled()) {
240                log.debug(doFormatLogMessage(request,
241                        "No more HttpSession: can not unlock !, HttpSession must have been invalidated"));
242            }
243            return false;
244        }
245        log.debug("Trying to unlock on session " + session.getId() + " on Thread " + Thread.currentThread().getId());
246
247        Lock lock = (Lock) session.getAttribute(SESSION_LOCK_KEY);
248        if (lock == null) {
249            log.error("Unable to find session lock, HttpSession may have been invalidated");
250            return false;
251        } else {
252            lock.unlock();
253            if (request.getAttribute(SYNCED_REQUEST_FLAG) != null) {
254                request.removeAttribute(SYNCED_REQUEST_FLAG);
255            }
256            if (log.isDebugEnabled()) {
257                log.debug("session unlocked on Thread ");
258            }
259            return true;
260        }
261    }
262
263    protected void addHeaders(HttpServletRequest request, HttpServletResponse response,
264            RequestFilterConfig config) {
265        addConfiguredHeaders(response);
266        if (request.getMethod().equals("GET")) {
267            addCacheHeaders(response, config);
268        }
269    }
270
271    protected void addConfiguredHeaders(HttpServletResponse response) {
272        RequestControllerManager rcm = Framework.getService(RequestControllerManager.class);
273        for (Entry<String, String> en : rcm.getResponseHeaders().entrySet()) {
274            String headerName = en.getKey();
275            if (!response.containsHeader(headerName)) {
276                response.addHeader(headerName, en.getValue());
277            }
278        }
279    }
280
281    protected void addCacheHeaders(HttpServletResponse response, RequestFilterConfig config) {
282        if (config.isCached()) {
283            String privateOrPublic = config.isPrivate() ? "private" : "public";
284            response.setHeader("Cache-Control", privateOrPublic + ", max-age=" + config.getCacheTime());
285            long expires = System.currentTimeMillis() + Long.parseLong(config.getCacheTime()) * 1000;
286            response.setHeader("Expires", HTTP_EXPIRES_DATE_FORMAT.format(expires));
287        } else if (config.isPrivate()) {
288            response.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
289        }
290    }
291
292}