/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. You may obtain a * copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.apache.geode.modules.session.filter; import org.apache.geode.distributed.internal.DistributionConfig; import org.apache.geode.modules.session.internal.filter.GemfireHttpSession; import org.apache.geode.modules.session.internal.filter.GemfireSessionManager; import org.apache.geode.modules.session.internal.filter.SessionManager; import org.apache.geode.modules.session.internal.filter.util.ThreadLocalSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import javax.servlet.http.*; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.security.Principal; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; /** * Primary class which orchestrates everything. This is the class which gets configured in the * web.xml. */ public class SessionCachingFilter implements Filter { /** * Logger instance */ private static final Logger LOG = LoggerFactory.getLogger(SessionCachingFilter.class.getName()); /** * The filter configuration object we are associated with. If this value is null, this filter * instance is not currently configured. */ private FilterConfig filterConfig = null; /** * Some containers will want to instantiate multiple instances of this filter, but we only need * one SessionManager */ private static SessionManager manager = null; /** * Can be overridden during testing. */ private static AtomicInteger started = new AtomicInteger( Integer.getInteger(DistributionConfig.GEMFIRE_PREFIX + "override.session.manager.count", 1)); private static int percentInactiveTimeTriggerRebuild = Integer .getInteger(DistributionConfig.GEMFIRE_PREFIX + "session.inactive.trigger.rebuild", 80); /** * This latch ensures that at least one thread/instance has fired up the session manager before * any other threads complete the init method. */ private static CountDownLatch startingLatch = new CountDownLatch(1); /** * This request wrapper class extends the support class HttpServletRequestWrapper, which * implements all the methods in the HttpServletRequest interface, as delegations to the wrapped * request. You only need to override the methods that you need to change. You can get access to * the wrapped request using the method getRequest() */ public static class RequestWrapper extends HttpServletRequestWrapper { private static final String URL_SESSION_IDENTIFIER = ";jsessionid="; private ResponseWrapper response; private boolean sessionFromCookie = false; private boolean sessionFromURL = false; private String requestedSessionId = null; private GemfireHttpSession session = null; private SessionManager manager; private HttpServletRequest outerRequest = null; /** * Need to save this in case we need the original {@code RequestDispatcher} */ private HttpServletRequest originalRequest; public RequestWrapper(SessionManager manager, HttpServletRequest request, ResponseWrapper response) { super(request); this.response = response; this.manager = manager; this.originalRequest = request; final Cookie[] cookies = request.getCookies(); if (cookies != null) { for (final Cookie cookie : cookies) { if (cookie.getName().equalsIgnoreCase(manager.getSessionCookieName()) && cookie.getValue().endsWith("-GF")) { requestedSessionId = cookie.getValue(); sessionFromCookie = true; LOG.debug("Cookie contains sessionId: {}", requestedSessionId); } } } if (requestedSessionId == null) { requestedSessionId = extractSessionId(); LOG.debug("Extracted sessionId from URL {}", requestedSessionId); if (requestedSessionId != null) { sessionFromURL = true; } } } /** * {@inheritDoc} */ @Override public HttpSession getSession() { return getSession(true); } /** * Create our own sessions. TODO: Handle invalidated sessions * * @return a HttpSession */ @Override public HttpSession getSession(boolean create) { if (session != null && session.isValid()) { session.setIsNew(false); session.updateAccessTime(); /* * This is a massively gross hack. Currently, there is no way to actually update the last * accessed time for a session, so what we do here is once we're into X% of the session's * TTL we grab a new session from the container. * * (inactive * 1000) * (pct / 100) ==> (inactive * 10 * pct) */ if (session.getLastAccessedTime() - session.getCreationTime() > (session.getMaxInactiveInterval() * 10 * percentInactiveTimeTriggerRebuild)) { HttpSession nativeSession = super.getSession(); session.failoverSession(nativeSession); } return session; } if (requestedSessionId != null) { session = (GemfireHttpSession) manager.getSession(requestedSessionId); if (session != null) { session.setIsNew(false); // This means we've failed over to another node if (session.getNativeSession() == null) { try { ThreadLocalSession.set(session); HttpSession nativeSession = super.getSession(); session.failoverSession(nativeSession); session.putInRegion(); } finally { ThreadLocalSession.remove(); } } } } if (session == null || !session.isValid()) { if (create) { try { session = (GemfireHttpSession) manager.wrapSession(null); ThreadLocalSession.set(session); HttpSession nativeSession = super.getSession(); if (session.getNativeSession() == null) { session.setNativeSession(nativeSession); } else { assert (session.getNativeSession() == nativeSession); } session.setIsNew(true); manager.putSession(session); } finally { ThreadLocalSession.remove(); } } else { // create is false, and session is either null or not valid. // The spec says return a null: return null; } } if (session != null) { addSessionCookie(response); session.updateAccessTime(); } return session; } private void addSessionCookie(HttpServletResponse response) { // Don't bother if the response is already committed if (response.isCommitted()) { return; } // Get the existing cookies Cookie[] cookies = getCookies(); Cookie cookie = new Cookie(manager.getSessionCookieName(), session.getId()); cookie.setPath("".equals(getContextPath()) ? "/" : getContextPath()); response.addCookie(cookie); } private String getCookieString(Cookie c) { StringBuilder cookie = new StringBuilder(); cookie.append(c.getName()).append("=").append(c.getValue()); if (c.getPath() != null) { cookie.append("; ").append("Path=").append(c.getPath()); } if (c.getDomain() != null) { cookie.append("; ").append("Domain=").append(c.getDomain()); } if (c.getSecure()) { cookie.append("; ").append("Secure"); } cookie.append("; HttpOnly"); return cookie.toString(); } /** * {@inheritDoc} */ @Override public boolean isRequestedSessionIdFromCookie() { return sessionFromCookie; } /** * {@inheritDoc} */ @Override public boolean isRequestedSessionIdFromURL() { return sessionFromURL; } /** * {@inheritDoc} */ @Override public String getRequestedSessionId() { if (requestedSessionId != null) { return requestedSessionId; } else { return super.getRequestedSessionId(); } } /* * Hmmm... not sure if this is right or even good to do. So, in some cases - for ex. using a * Spring security filter, we have 3 possible wrappers to deal with - the original, this one and * one created by Spring. When a servlet or JSP is forwarded to the original request is passed * in, but then this (the wrapped) request is used by the JSP. In some cases, the outer wrapper * also contains information relevant to the request - in this case security info. So here we * allow access to that. There's probably a better way.... */ /** * {@inheritDoc} */ @Override public Principal getUserPrincipal() { if (outerRequest != null) { return outerRequest.getUserPrincipal(); } else { return super.getUserPrincipal(); } } /** * {@inheritDoc} */ @Override public String getRemoteUser() { if (outerRequest != null) { return outerRequest.getRemoteUser(); } else { return super.getRemoteUser(); } } /** * {@inheritDoc} */ @Override public boolean isUserInRole(String role) { if (outerRequest != null) { return outerRequest.isUserInRole(role); } else { return super.isUserInRole(role); } } ////////////////////////////////////////////////////////////// // Non-API methods void setOuterWrapper(HttpServletRequest outer) { this.outerRequest = outer; } ////////////////////////////////////////////////////////////// // Private methods private String extractSessionId() { final int prefix = getRequestURL().indexOf(URL_SESSION_IDENTIFIER); if (prefix != -1) { final int start = prefix + URL_SESSION_IDENTIFIER.length(); int suffix = getRequestURL().indexOf("?", start); if (suffix < 0) { suffix = getRequestURL().indexOf("#", start); } if (suffix <= prefix) { return getRequestURL().substring(start); } return getRequestURL().substring(start, suffix); } return null; } } /** * This response wrapper class extends the support class HttpServletResponseWrapper, which * implements all the methods in the HttpServletResponse interface, as delegations to the wrapped * response. You only need to override the methods that you need to change. You can get access to * the wrapped response using the method getResponse() */ class ResponseWrapper extends HttpServletResponseWrapper { HttpServletResponse originalResponse; public ResponseWrapper(HttpServletResponse response) throws IOException { super(response); originalResponse = response; } public HttpServletResponse getOriginalResponse() { return originalResponse; } @Override public void setHeader(String name, String value) { super.setHeader(name, value); } @Override public void setIntHeader(String name, int value) { super.setIntHeader(name, value); } } public SessionCachingFilter() {} /** * @param request The servlet request we are processing * @param response The servlet response we are creating * @param chain The filter chain we are processing * @throws IOException if an input/output error occurs * @throws ServletException if a servlet error occurs */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) request; HttpServletResponse httpResp = (HttpServletResponse) response; /** * Early out if this isn't the right kind of request. We might see a RequestWrapper instance * during a forward or include request. */ if (alreadyWrapped(httpReq)) { LOG.debug("Handling already-wrapped request"); chain.doFilter(request, response); return; } // Create wrappers for the request and response objects. // Using these, you can extend the capabilities of the // request and response, for example, allow setting parameters // on the request before sending the request to the rest of the filter chain, // or keep track of the cookies that are set on the response. // // Caveat: some servers do not handle wrappers very well for forward or // include requests. ResponseWrapper wrappedResponse = new ResponseWrapper(httpResp); final RequestWrapper wrappedRequest = new RequestWrapper(manager, httpReq, wrappedResponse); Throwable problem = null; try { chain.doFilter(wrappedRequest, wrappedResponse); } catch (Throwable t) { // If an exception is thrown somewhere down the filter chain, // we still want to execute our after processing, and then // rethrow the problem after that. problem = t; LOG.error("Exception processing filter chain", t); } GemfireHttpSession session = (GemfireHttpSession) wrappedRequest.getSession(false); // If there was a problem, we want to rethrow it if it is // a known type, otherwise log it. if (problem != null) { if (problem instanceof ServletException) { throw (ServletException) problem; } if (problem instanceof IOException) { throw (IOException) problem; } sendProcessingError(problem, response); } /** * Commit any updates. What actually happens at that point is dependent on the type of * attributes defined for use by the sessions. */ if (session != null) { session.commit(); } } /** * Test if a request has been wrapped with RequestWrapper somewhere in the chain of wrapped * requests. */ private boolean alreadyWrapped(final ServletRequest request) { if (request instanceof RequestWrapper) { return true; } if (!(request instanceof ServletRequestWrapper)) { return false; } final ServletRequest nestedRequest = ((ServletRequestWrapper) request).getRequest(); if (nestedRequest == request) { return false; } return alreadyWrapped(nestedRequest); } /** * Return the filter configuration object for this filter. */ public FilterConfig getFilterConfig() { return (this.filterConfig); } /** * Set the filter configuration object for this filter. * * @param filterConfig The filter configuration object */ public void setFilterConfig(FilterConfig filterConfig) { this.filterConfig = filterConfig; } /** * Destroy method for this filter */ @Override public void destroy() { if (manager != null) { manager.stop(); } } /** * This is where all the initialization happens. * * @param config * @throws ServletException */ @Override public void init(final FilterConfig config) { LOG.info("Starting Session Filter initialization"); this.filterConfig = config; if (started.getAndDecrement() > 0) { /** * Allow override for testing purposes */ String managerClassStr = config.getInitParameter("session-manager-class"); // Otherwise default if (managerClassStr == null) { managerClassStr = GemfireSessionManager.class.getName(); } try { manager = (SessionManager) Class.forName(managerClassStr).newInstance(); manager.start(config, this.getClass().getClassLoader()); } catch (Exception ex) { LOG.error("Exception creating Session Manager", ex); } startingLatch.countDown(); } else { try { startingLatch.await(); } catch (InterruptedException iex) { } LOG.debug("SessionManager and listener initialization skipped - " + "already done."); } LOG.info("Session Filter initialization complete"); LOG.debug("Filter class loader {}", this.getClass().getClassLoader()); } /** * Return a String representation of this object. */ @Override public String toString() { if (filterConfig == null) { return ("SessionCachingFilter()"); } StringBuilder sb = new StringBuilder("SessionCachingFilter("); sb.append(filterConfig); sb.append(")"); return (sb.toString()); } private void sendProcessingError(Throwable t, ServletResponse response) { String stackTrace = getStackTrace(t); if (stackTrace != null && !stackTrace.equals("")) { try { response.setContentType("text/html"); PrintStream ps = new PrintStream(response.getOutputStream()); PrintWriter pw = new PrintWriter(ps); pw.print("<html>\n<head>\n<title>Error</title>\n</head>\n<body>\n"); // NOI18N // PENDING! Localize this for next official release pw.print("<h1>The resource did not process correctly</h1>\n<pre>\n"); pw.print(stackTrace); pw.print("</pre></body>\n</html>"); // NOI18N pw.close(); ps.close(); response.getOutputStream().close(); } catch (Exception ex) { } } else { try { PrintStream ps = new PrintStream(response.getOutputStream()); t.printStackTrace(ps); ps.close(); response.getOutputStream().close(); } catch (Exception ex) { } } } public static String getStackTrace(Throwable t) { String stackTrace = null; try { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); t.printStackTrace(pw); pw.close(); sw.close(); stackTrace = sw.getBuffer().toString(); } catch (Exception ex) { } return stackTrace; } /** * Retrieve the SessionManager. This is only here so that tests can get access to the cache. */ public static SessionManager getSessionManager() { return manager; } /** * Return the GemFire session which wraps a native session * * @param nativeSession the native session for which the corresponding GemFire session should be * returned. * @return the GemFire session or null if no session maps to the native session */ public static HttpSession getWrappingSession(HttpSession nativeSession) { /* * This is a special case where the GemFire session has been set as a ThreadLocal during session * creation. */ GemfireHttpSession gemfireSession = (GemfireHttpSession) ThreadLocalSession.get(); if (gemfireSession != null) { gemfireSession.setNativeSession(nativeSession); return gemfireSession; } return getSessionManager().getWrappingSession(nativeSession.getId()); } }