// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.server.session; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSessionActivationListener; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import javax.servlet.http.HttpSessionContext; import javax.servlet.http.HttpSessionEvent; import org.eclipse.jetty.io.IdleTimeout; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Locker.Lock; /** * Session * * A heavy-weight Session object representing a HttpSession. Session objects * relating to a context are kept in a {@link SessionCache}. The purpose of * the SessionCache is to keep the working set of Session objects in memory * so that they may be accessed quickly, and facilitate the sharing of a * Session object amongst multiple simultaneous requests referring to the * same session id. * * The {@link SessionHandler} coordinates * the lifecycle of Session objects with the help of the SessionCache. * * @see SessionHandler * @see org.eclipse.jetty.server.SessionIdManager */ public class Session implements SessionHandler.SessionIf { private final static Logger LOG = Log.getLogger("org.eclipse.jetty.server.session"); /** * */ public final static String SESSION_CREATED_SECURE="org.eclipse.jetty.security.sessionCreatedSecure"; /** * State * * Validity states of a session */ public enum State {VALID, INVALID, INVALIDATING}; protected SessionData _sessionData; //the actual data associated with a session protected SessionHandler _handler; //the manager of the session protected String _extendedId; //the _id plus the worker name protected long _requests; protected boolean _idChanged; protected boolean _newSession; protected State _state = State.VALID; //state of the session:valid,invalid or being invalidated protected Locker _lock = new Locker(); //sync lock protected boolean _resident = false; protected SessionInactivityTimeout _sessionInactivityTimer = null; /* ------------------------------------------------------------- */ /** * SessionInactivityTimeout * * Each Session has a timer associated with it that fires whenever * it has been idle (ie not referenced by a request) for a * configurable amount of time, or the Session expires. * * @see SessionCache * */ public class SessionInactivityTimeout extends IdleTimeout { /** * */ public SessionInactivityTimeout() { super(getSessionHandler().getScheduler()); } /** * @see org.eclipse.jetty.io.IdleTimeout#onIdleExpired(java.util.concurrent.TimeoutException) */ @Override protected void onIdleExpired(TimeoutException timeout) { //called when the timer goes off if (LOG.isDebugEnabled()) LOG.debug("Timer expired for session {}", getId()); getSessionHandler().sessionInactivityTimerExpired(Session.this); } /** * @see org.eclipse.jetty.io.IdleTimeout#isOpen() */ @Override public boolean isOpen() { // Called to determine if the timer should be reset // True if: // 1. the session is still valid // BUT if passivated out to disk, do we really want this timer to keep going off? try (Lock lock = _lock.lockIfNotHeld()) { return isValid() && isResident(); } } /** * @see org.eclipse.jetty.io.IdleTimeout#setIdleTimeout(long) */ @Override public void setIdleTimeout(long idleTimeout) { if (LOG.isDebugEnabled()) LOG.debug("setIdleTimeout called: old="+getIdleTimeout()+" new="+idleTimeout); super.setIdleTimeout(idleTimeout); } } /* ------------------------------------------------------------- */ /** * Create a new session * @param handler the SessionHandler that manages this session * @param request the request the session should be based on * @param data the session data */ public Session (SessionHandler handler, HttpServletRequest request, SessionData data) { _handler = handler; _sessionData = data; _newSession = true; _sessionData.setDirty(true); _requests = 1; //access will not be called on this new session, but we are obviously in a request } /* ------------------------------------------------------------- */ /** * Re-inflate an existing session from some eg persistent store. * * @param handler the SessionHandler managing the session * @param data the session data */ public Session (SessionHandler handler, SessionData data) { _handler = handler; _sessionData = data; } /* ------------------------------------------------------------- */ /** * Returns the current number of requests that are active in the * Session. * * @return the number of active requests for this session */ public long getRequests() { try (Lock lock = _lock.lockIfNotHeld()) { return _requests; } } /* ------------------------------------------------------------- */ public void setExtendedId (String extendedId) { _extendedId = extendedId; } /* ------------------------------------------------------------- */ protected void cookieSet() { try (Lock lock = _lock.lockIfNotHeld()) { _sessionData.setCookieSet(_sessionData.getAccessed()); } } /* ------------------------------------------------------------ */ protected boolean access(long time) { try (Lock lock = _lock.lockIfNotHeld()) { if (!isValid()) return false; _newSession=false; long lastAccessed = _sessionData.getAccessed(); if (_sessionInactivityTimer != null) _sessionInactivityTimer.notIdle(); _sessionData.setAccessed(time); _sessionData.setLastAccessed(lastAccessed); _sessionData.calcAndSetExpiry(time); if (isExpiredAt(time)) { invalidate(); return false; } _requests++; return true; } } /* ------------------------------------------------------------ */ protected void complete() { try (Lock lock = _lock.lockIfNotHeld()) { _requests--; } } /* ------------------------------------------------------------- */ /** Check to see if session has expired as at the time given. * * @param time the time since the epoch in ms * @return true if expired */ protected boolean isExpiredAt(long time) { try (Lock lock = _lock.lockIfNotHeld()) { return _sessionData.isExpiredAt(time); } } /* ------------------------------------------------------------- */ /** Check if the Session has been idle longer than a number of seconds. * * @param sec the number of seconds * @return true if the session has been idle longer than the interval */ protected boolean isIdleLongerThan (int sec) { long now = System.currentTimeMillis(); try (Lock lock = _lock.lockIfNotHeld()) { return ((_sessionData.getAccessed() + (sec*1000)) <= now); } } /* ------------------------------------------------------------ */ /** * Call binding and attribute listeners based on the new and old * values of the attribute. * * @param name name of the attribute * @param newValue new value of the attribute * @param oldValue previous value of the attribute */ protected void callSessionAttributeListeners (String name, Object newValue, Object oldValue) { if (newValue==null || !newValue.equals(oldValue)) { if (oldValue!=null) unbindValue(name,oldValue); if (newValue!=null) bindValue(name,newValue); if (_handler == null) throw new IllegalStateException ("No session manager for session "+ _sessionData.getId()); _handler.doSessionAttributeListeners(this,name,oldValue,newValue); } } /* ------------------------------------------------------------- */ /** * Unbind value if value implements {@link HttpSessionBindingListener} (calls {@link HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent)}) * @param name the name with which the object is bound or unbound * @param value the bound value */ public void unbindValue(java.lang.String name, Object value) { if (value!=null&&value instanceof HttpSessionBindingListener) ((HttpSessionBindingListener)value).valueUnbound(new HttpSessionBindingEvent(this,name)); } /* ------------------------------------------------------------- */ /** * Bind value if value implements {@link HttpSessionBindingListener} (calls {@link HttpSessionBindingListener#valueBound(HttpSessionBindingEvent)}) * @param name the name with which the object is bound or unbound * @param value the bound value */ public void bindValue(java.lang.String name, Object value) { if (value!=null&&value instanceof HttpSessionBindingListener) ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this,name)); } /* ------------------------------------------------------------- */ /** * Call the activation listeners. This must be called holding the * lock. */ public void didActivate() { HttpSessionEvent event = new HttpSessionEvent(this); for (Iterator<String> iter = _sessionData.getKeys().iterator(); iter.hasNext();) { Object value = _sessionData.getAttribute(iter.next()); if (value instanceof HttpSessionActivationListener) { HttpSessionActivationListener listener = (HttpSessionActivationListener) value; listener.sessionDidActivate(event); } } } /* ------------------------------------------------------------- */ /** * Call the passivation listeners. This must be called holding the * lock */ public void willPassivate() { HttpSessionEvent event = new HttpSessionEvent(this); for (Iterator<String> iter = _sessionData.getKeys().iterator(); iter.hasNext();) { Object value = _sessionData.getAttribute(iter.next()); if (value instanceof HttpSessionActivationListener) { HttpSessionActivationListener listener = (HttpSessionActivationListener) value; listener.sessionWillPassivate(event); } } } /* ------------------------------------------------------------ */ public boolean isValid() { try (Lock lock = _lock.lockIfNotHeld()) { return _state==State.VALID; } } /* ------------------------------------------------------------- */ public long getCookieSetTime() { try (Lock lock = _lock.lockIfNotHeld()) { return _sessionData.getCookieSet(); } } /* ------------------------------------------------------------- */ @Override public long getCreationTime() throws IllegalStateException { try (Lock lock = _lock.lockIfNotHeld()) { checkValidForRead(); return _sessionData.getCreated(); } } /** * @see javax.servlet.http.HttpSession#getId() */ @Override public String getId() { try (Lock lock = _lock.lockIfNotHeld()) { return _sessionData.getId(); } } public String getExtendedId() { return _extendedId; } public String getContextPath() { return _sessionData.getContextPath(); } public String getVHost () { return _sessionData.getVhost(); } /** * @see javax.servlet.http.HttpSession#getLastAccessedTime() */ @Override public long getLastAccessedTime() { try (Lock lock = _lock.lockIfNotHeld()) { return _sessionData.getLastAccessed(); } } /** * @see javax.servlet.http.HttpSession#getServletContext() */ @Override public ServletContext getServletContext() { if (_handler == null) throw new IllegalStateException ("No session manager for session "+ _sessionData.getId()); return _handler._context; } /** * @see javax.servlet.http.HttpSession#setMaxInactiveInterval(int) */ @Override public void setMaxInactiveInterval(int secs) { try (Lock lock = _lock.lockIfNotHeld()) { _sessionData.setMaxInactiveMs((long)secs*1000L); _sessionData.calcAndSetExpiry(); _sessionData.setDirty(true); updateInactivityTimer(); if (LOG.isDebugEnabled()) { if (secs <= 0) LOG.debug("Session {} is now immortal (maxInactiveInterval={})", _sessionData.getId(), secs); else LOG.debug("Session {} maxInactiveInterval={}", _sessionData.getId(), secs); } } } /** * Set the inactivity timer to the smaller of the session maxInactivity * (ie session-timeout from web.xml), or the inactive eviction time. */ public void updateInactivityTimer () { try (Lock lock = _lock.lockIfNotHeld()) { if (LOG.isDebugEnabled())LOG.debug("updateInactivityTimer"); long maxInactive = _sessionData.getMaxInactiveMs(); int evictionPolicy = getSessionHandler().getSessionCache().getEvictionPolicy(); if (maxInactive <= 0) { //sessions are immortal, they never expire if (evictionPolicy < SessionCache.EVICT_ON_INACTIVITY) { //we do not want to evict inactive sessions setInactivityTimer(-1L); if (LOG.isDebugEnabled()) LOG.debug("Session is immortal && no inactivity eviction: timer cancelled"); } else { //sessions are immortal but we want to evict after inactivity setInactivityTimer(TimeUnit.SECONDS.toMillis(evictionPolicy)); if (LOG.isDebugEnabled()) LOG.debug("Session is immortal; evict after {} sec inactivity", evictionPolicy); } } else { //sessions are not immortal if (evictionPolicy < SessionCache.EVICT_ON_INACTIVITY) { //don't want to evict inactive sessions, set the timer for the session's maxInactive setting setInactivityTimer(_sessionData.getMaxInactiveMs()); if (LOG.isDebugEnabled()) LOG.debug("No inactive session eviction"); } else { //set the time to the lesser of the session's maxInactive and eviction timeout setInactivityTimer(Math.min(maxInactive, TimeUnit.SECONDS.toMillis(evictionPolicy))); if (LOG.isDebugEnabled()) LOG.debug("Inactivity timer set to lesser of maxInactive={} and inactivityEvict={}", maxInactive, evictionPolicy); } } } } /** * Set the inactivity timer * * @param ms value in millisec, -1 disables it */ private void setInactivityTimer (long ms) { if (_sessionInactivityTimer == null) _sessionInactivityTimer = new SessionInactivityTimeout(); _sessionInactivityTimer.setIdleTimeout(ms); } /** * */ public void stopInactivityTimer () { try (Lock lock = _lock.lockIfNotHeld()) { if (_sessionInactivityTimer != null) { _sessionInactivityTimer.setIdleTimeout(-1); _sessionInactivityTimer = null; if (LOG.isDebugEnabled()) LOG.debug("Session timer stopped"); } } } /** * @see javax.servlet.http.HttpSession#getMaxInactiveInterval() */ @Override public int getMaxInactiveInterval() { try (Lock lock = _lock.lockIfNotHeld()) { return (int)(_sessionData.getMaxInactiveMs()/1000); } } /** * @see javax.servlet.http.HttpSession#getSessionContext() */ @Override @Deprecated public HttpSessionContext getSessionContext() { checkValidForRead(); return SessionHandler.__nullSessionContext; } public SessionHandler getSessionHandler() { return _handler; } /* ------------------------------------------------------------- */ /** * Check that the session can be modified. * * @throws IllegalStateException if the session is invalid */ protected void checkValidForWrite() throws IllegalStateException { checkLocked(); if (_state == State.INVALID) throw new IllegalStateException("Not valid for write: id="+_sessionData.getId()+" created="+_sessionData.getCreated()+" accessed="+_sessionData.getAccessed()+" lastaccessed="+_sessionData.getLastAccessed()+" maxInactiveMs="+_sessionData.getMaxInactiveMs()+" expiry="+_sessionData.getExpiry()); if (_state == State.INVALIDATING) return; //in the process of being invalidated, listeners may try to remove attributes if (!isResident()) throw new IllegalStateException("Not valid for write: id="+_sessionData.getId()+" not resident"); } /* ------------------------------------------------------------- */ /** * Chech that the session data can be read. * * @throws IllegalStateException if the session is invalid */ protected void checkValidForRead () throws IllegalStateException { checkLocked(); if (_state == State.INVALID) throw new IllegalStateException("Invalid for read: id="+_sessionData.getId()+" created="+_sessionData.getCreated()+" accessed="+_sessionData.getAccessed()+" lastaccessed="+_sessionData.getLastAccessed()+" maxInactiveMs="+_sessionData.getMaxInactiveMs()+" expiry="+_sessionData.getExpiry()); if (_state == State.INVALIDATING) return; if (!isResident()) throw new IllegalStateException("Invalid for read: id="+_sessionData.getId()+" not resident"); } /* ------------------------------------------------------------- */ protected void checkLocked () throws IllegalStateException { if (!_lock.isLocked()) throw new IllegalStateException("Session not locked"); } /** * @see javax.servlet.http.HttpSession#getAttribute(java.lang.String) */ @Override public Object getAttribute(String name) { try (Lock lock = _lock.lockIfNotHeld()) { checkValidForRead(); return _sessionData.getAttribute(name); } } /** * @see javax.servlet.http.HttpSession#getValue(java.lang.String) */ @Override @Deprecated public Object getValue(String name) { try (Lock lock = _lock.lockIfNotHeld()) { return _sessionData.getAttribute(name); } } /** * @see javax.servlet.http.HttpSession#getAttributeNames() */ @Override public Enumeration<String> getAttributeNames() { try (Lock lock = _lock.lockIfNotHeld()) { checkValidForRead(); final Iterator<String> itor = _sessionData.getKeys().iterator(); return new Enumeration<String> () { @Override public boolean hasMoreElements() { return itor.hasNext(); } @Override public String nextElement() { return itor.next(); } }; } } /* ------------------------------------------------------------ */ public int getAttributes() { return _sessionData.getKeys().size(); } /* ------------------------------------------------------------ */ public Set<String> getNames() { return Collections.unmodifiableSet(_sessionData.getKeys()); } /* ------------------------------------------------------------- */ /** * @deprecated As of Version 2.2, this method is replaced by * {@link #getAttributeNames} */ @Deprecated @Override public String[] getValueNames() throws IllegalStateException { try (Lock lock = _lock.lockIfNotHeld()) { checkValidForRead(); Iterator<String> itor = _sessionData.getKeys().iterator(); if (!itor.hasNext()) return new String[0]; ArrayList<String> names = new ArrayList<String>(); while (itor.hasNext()) names.add(itor.next()); return names.toArray(new String[names.size()]); } } /* ------------------------------------------------------------- */ /** * @see javax.servlet.http.HttpSession#setAttribute(java.lang.String, java.lang.Object) */ @Override public void setAttribute(String name, Object value) { Object old=null; try (Lock lock = _lock.lockIfNotHeld()) { //if session is not valid, don't accept the set checkValidForWrite(); old=_sessionData.setAttribute(name,value); } if (value == null && old == null) return; //if same as remove attribute but attribute was already removed, no change callSessionAttributeListeners(name, value, old); } /* ------------------------------------------------------------- */ /** * @see javax.servlet.http.HttpSession#putValue(java.lang.String, java.lang.Object) */ @Override @Deprecated public void putValue(String name, Object value) { setAttribute(name,value); } /* ------------------------------------------------------------- */ /** * @see javax.servlet.http.HttpSession#removeAttribute(java.lang.String) */ @Override public void removeAttribute(String name) { setAttribute(name, null); } /* ------------------------------------------------------------- */ /** * @see javax.servlet.http.HttpSession#removeValue(java.lang.String) */ @Override @Deprecated public void removeValue(String name) { setAttribute(name, null); } /* ------------------------------------------------------------ */ /** Force a change to the id of a session. * * @param request the Request associated with the call to change id. */ public void renewId(HttpServletRequest request) { if (_handler == null) throw new IllegalStateException ("No session manager for session "+ _sessionData.getId()); String id = null; String extendedId = null; try (Lock lock = _lock.lockIfNotHeld()) { checkValidForWrite(); //don't renew id on a session that is not valid id = _sessionData.getId(); //grab the values as they are now extendedId = getExtendedId(); } String newId = _handler._sessionIdManager.renewSessionId(id, extendedId, request); try (Lock lock = _lock.lockIfNotHeld()) { checkValidForWrite(); _sessionData.setId(newId); setExtendedId(_handler._sessionIdManager.getExtendedId(newId, request)); } setIdChanged(true); } /* ------------------------------------------------------------- */ /** Called by users to invalidate a session, or called by the * access method as a request enters the session if the session * has expired, or called by manager as a result of scavenger * expiring session * * @see javax.servlet.http.HttpSession#invalidate() */ @Override public void invalidate() { if (_handler == null) throw new IllegalStateException ("No session manager for session "+ _sessionData.getId()); boolean result = beginInvalidate(); try { //if the session was not already invalid, or in process of being invalidated, do invalidate if (result) { //tell id mgr to remove session from all contexts _handler.getSessionIdManager().invalidateAll(_sessionData.getId()); } } catch (Exception e) { LOG.warn(e); } } /* ------------------------------------------------------------- */ /** Grab the lock on the session * @return the lock */ public Lock lock () { return _lock.lock(); } /* ------------------------------------------------------------- */ /** Grab the lock on the session if it isn't locked already * @return the lock */ public Lock lockIfNotHeld () { return _lock.lockIfNotHeld(); } /* ------------------------------------------------------------- */ /** * @return true if the session is not already invalid or being invalidated. */ protected boolean beginInvalidate() { boolean result = false; try (Lock lock = _lock.lockIfNotHeld()) { switch (_state) { case INVALID: { throw new IllegalStateException(); //spec does not allow invalidate of already invalid session } case VALID: { //only first change from valid to invalidating should be actionable result = true; _state = State.INVALIDATING; break; } default: { LOG.info("Session {} already being invalidated", _sessionData.getId()); } } } return result; } /* ------------------------------------------------------------- */ /** Call HttpSessionAttributeListeners as part of invalidating * a Session. * * @throws IllegalStateException */ @Deprecated protected void doInvalidate() throws IllegalStateException { finishInvalidate(); } /* ------------------------------------------------------------- */ /** Call HttpSessionAttributeListeners as part of invalidating * a Session. * * @throws IllegalStateException */ protected void finishInvalidate() throws IllegalStateException { try (Lock lock = _lock.lockIfNotHeld()) { try { if (LOG.isDebugEnabled()) LOG.debug("invalidate {}",_sessionData.getId()); if (_state == State.VALID || _state == State.INVALIDATING) { Set<String> keys = null; do { keys = _sessionData.getKeys(); for (String key:keys) { Object old=_sessionData.setAttribute(key,null); if (old == null) return; //if same as remove attribute but attribute was already removed, no change callSessionAttributeListeners(key, null, old); } } while (!keys.isEmpty()); } } finally { // mark as invalid _state = State.INVALID; } } } /* ------------------------------------------------------------- */ @Override public boolean isNew() throws IllegalStateException { try (Lock lock = _lock.lockIfNotHeld()) { checkValidForRead(); return _newSession; } } /* ------------------------------------------------------------- */ public void setIdChanged(boolean changed) { try (Lock lock = _lock.lockIfNotHeld()) { _idChanged=changed; } } /* ------------------------------------------------------------- */ public boolean isIdChanged () { try (Lock lock = _lock.lockIfNotHeld()) { return _idChanged; } } /* ------------------------------------------------------------- */ @Override public Session getSession() { // TODO why is this used return this; } /* ------------------------------------------------------------- */ protected SessionData getSessionData() { return _sessionData; } /* ------------------------------------------------------------- */ public void setResident (boolean resident) { _resident = resident; } /* ------------------------------------------------------------- */ public boolean isResident () { return _resident; } }