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