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}