/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/tool/impl/SessionComponent.java $ * $Id: SessionComponent.java 126061 2013-06-20 21:07:47Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.tool.impl; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.SecureRandom; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.lang.mutable.MutableLong; import org.springframework.util.StringUtils; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.id.api.IdManager; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.tool.api.NonPortableSession; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.SessionAttributeListener; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.SessionStore; import org.sakaiproject.tool.api.Tool; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.tool.api.ToolSession; /** * <p> * Standard implementation of the Sakai SessionManager. * </p> */ public abstract class SessionComponent implements SessionManager, SessionStore { /** Our log (commons). */ private static Log M_log = LogFactory.getLog(SessionComponent.class); /** The sessions - keyed by session id. */ protected Map<String, Session> m_sessions = new ConcurrentHashMap<String, Session>(); /** * The expected time sessions may be ready for expiration. This is only an optimization * for when Terracotta is in use, to prevent faulting Session objects into the local * JVM when it is not necessary. Session.isInactive() method remains the ultimate authority * to determine if a session is invalid or not. */ protected Map<String,MutableLong> expirationTimeSuggestionMap = new ConcurrentHashMap<String, MutableLong>(); private SessionAttributeListener sessionListener; /** The maintenance. */ protected Maintenance m_maintenance = null; /** Key in the ThreadLocalManager for binding our current session. */ protected final static String CURRENT_SESSION = "org.sakaiproject.api.kernel.session.current"; /** Key in the ThreadLocalManager for binding our current tool session. */ protected final static String CURRENT_TOOL_SESSION = "org.sakaiproject.api.kernel.session.current.tool"; /** Key in the ThreadLocalManager for access to the current servlet context (from tool-util/servlet/RequestFilter). */ protected final static String CURRENT_SERVLET_CONTEXT = "org.sakaiproject.util.RequestFilter.servlet_context"; /** The set of tool ids that represent tools that can be clustered */ protected Set<String> clusterableTools = new HashSet<String>(); /** Salt for predictable session IDs */ protected byte[] salt = null; /********************************************************************************************************************************************************************************************************************************************************** * Dependencies *********************************************************************************************************************************************************************************************************************************************************/ /** Will be used to get the current tool id when checking the whitelist */ protected abstract ToolManager toolManager(); /** * @return the ThreadLocalManager collaborator. */ protected abstract ThreadLocalManager threadLocalManager(); /** * @return the IdManager collaborator. */ protected abstract IdManager idManager(); /********************************************************************************************************************************************************************************************************************************************************** * Configuration *********************************************************************************************************************************************************************************************************************************************************/ /** Configuration: default inactive period for sessions (seconds). */ protected int m_defaultInactiveInterval = 30 * 60; /** * Configuration - set the default inactive period for sessions. * * @param value * The default inactive period for sessions. */ public void setInactiveInterval(String value) { try { m_defaultInactiveInterval = Integer.parseInt(value); } catch (Exception t) { System.out.println(t); } } /** * Configuration - set the default inactive period for sessions. * * @param value * The default inactive period for sessions. */ public void setInactiveInterval(int value) { m_defaultInactiveInterval = value; } /** * Configuration - set the default inactive period for sessions. * * @return The default inactive period for sessions. */ public int getInactiveInterval() { return m_defaultInactiveInterval; } /** Configuration: how often to check for inactive sessions (seconds). */ protected int m_checkEvery = 60; /** * Configuration: set how often to check for inactive sessions (seconds). * * @param value * The how often to check for inactive sessions (seconds) value. */ public void setCheckEvery(String value) { try { m_checkEvery = Integer.parseInt(value); } catch (Exception t) { System.out.println(t); } } /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { // start the maintenance thread if (m_checkEvery > 0) { m_maintenance = new Maintenance(); m_maintenance.start(); } // Salt generation 64 bits long salt = new byte[8]; SecureRandom random; try { random = SecureRandom.getInstance("SHA1PRNG"); random.nextBytes(salt); } catch (NoSuchAlgorithmException e) { M_log.warn("Random number generator not available - using time randomness"); salt = String.valueOf(System.currentTimeMillis()).getBytes(); } M_log.info("init(): interval: " + m_defaultInactiveInterval + " refresh: " + m_checkEvery); } /** * Final cleanup. */ public void destroy() { if (m_maintenance != null) { m_maintenance.stop(); m_maintenance = null; } M_log.info("destroy()"); } /********************************************************************************************************************************************************************************************************************************************************** * Work interface methods: SessionManager *********************************************************************************************************************************************************************************************************************************************************/ /** * @inheritDoc */ public Session getSession(String sessionId) { MySession s = (MySession) m_sessions.get(sessionId); return s; } public String makeSessionId(HttpServletRequest req, Principal principal) { MessageDigest sha; String sessionId; try { sha = MessageDigest.getInstance("SHA-1"); sha.reset(); sha.update(principal.getName().getBytes("UTF-8")); sha.update((byte) 0x0a); String ua = req.getHeader("user-agent"); if (ua != null) { sha.update(ua.getBytes("UTF-8")); } sha.update(salt); sessionId = byteArrayToHexStr(sha.digest()); } catch (NoSuchAlgorithmException e) { // Fallback to new uuid rather than a non-hashed id sessionId = idManager().createUuid(); //This may need to be changed to a debug M_log.warn("makeSessionId fallback to Uuid!",e); } catch (UnsupportedEncodingException e) { sessionId = idManager().createUuid(); //This may need to be changed to a debug M_log.warn("makeSessionId fallback to Uuid!",e); } return sessionId; } public List<Session> getSessions() { return new ArrayList<Session>(m_sessions.values()); } public void remove(String sessionId) { m_sessions.remove(sessionId); expirationTimeSuggestionMap.remove(sessionId); } /** * Checks the current Tool ID to determine if this tool is marked for clustering. * * @return true if the tool is marked for clustering, false otherwise. */ public boolean isCurrentToolClusterable() { ToolManager toolManager = toolManager(); Tool tool = null; // ToolManager should exist. Protect against it being // null and just log a message if it is. if (toolManager != null) { tool = toolManager.getCurrentTool(); // tool can be null, this is common during startup for example if (tool != null) { String toolId = tool.getId(); // if the tool exists, the toolid should. But protect and only // log a message if it is null if (toolId != null) { return clusterableTools.contains(toolId); } else { M_log.error("SessionComponent.isCurrentToolClusterable(): toolId was null."); } } } else { M_log.error("SessionComponent.isCurrentToolClusterable(): toolManager was null."); } return false; } /** * @inheritDoc */ public Session startSession() { String id = idManager().createUuid(); return startSession(id); } /** * @inheritDoc */ public Session startSession(String id) { // create a non portable session object if this is a clustered environment NonPortableSession nPS = new MyNonPortableSession(); // create a new MutableLong object representing the current time that both // the Session and SessionManager can see. MutableLong currentTime = currentTimeMutableLong(); // create a new session Session s = new MySession(this,id,threadLocalManager(),idManager(),this,sessionListener,m_defaultInactiveInterval,nPS,currentTime); // Place session into the main Session Storage, capture any old id Session old = m_sessions.put(s.getId(), s); // Place an entry in the expirationTimeSuggestionMap that corresponds to the entry in m_sessions expirationTimeSuggestionMap.put(id, currentTime); // check for id conflict if (old != null) { M_log.warn("startSession: duplication id: " + s.getId()); } return s; } protected MutableLong currentTimeMutableLong() { return new MutableLong(System.currentTimeMillis()); } /** * @inheritDoc */ public Session getCurrentSession() { Session rv = (Session) threadLocalManager().get(CURRENT_SESSION); // if we don't have one already current, make one and bind it as current, but don't save it in our by-id table - let it just go away after the thread if (rv == null) { String id = idManager().createUuid(); // create a non portable session object if this is a clustered environment NonPortableSession nPS = new MyNonPortableSession(); rv = new MySession(this,id,threadLocalManager(),idManager(),this,sessionListener,m_defaultInactiveInterval,nPS,currentTimeMutableLong()); setCurrentSession(rv); } return rv; } /** * @inheritDoc */ public String getCurrentSessionUserId() { Session s = (Session) threadLocalManager().get(CURRENT_SESSION); if (s != null) { return s.getUserId(); } return null; } /** * @inheritDoc */ public ToolSession getCurrentToolSession() { return (ToolSession) threadLocalManager().get(CURRENT_TOOL_SESSION); } /** * @inheritDoc */ public void setCurrentSession(Session s) { threadLocalManager().set(CURRENT_SESSION, s); } /** * @inheritDoc */ public void setCurrentToolSession(ToolSession s) { threadLocalManager().set(CURRENT_TOOL_SESSION, s); } public String getClusterableTools() { return StringUtils.collectionToCommaDelimitedString(clusterableTools); // return clusterableTools; } public void setClusterableTools(String clusterableToolList) { Set<?> newTools = StringUtils.commaDelimitedListToSet(clusterableToolList); this.clusterableTools.clear(); for (Object o: newTools) { if (o instanceof java.lang.String) { this.clusterableTools.add((String)o); } else { M_log.error("SessionManager.setClusterableTools(String) unable to set value: "+o); } } } /** * @inheritDoc */ public int getActiveUserCount(int secs) { Set<String> activeusers = new HashSet<String>(m_sessions.size()); long now = System.currentTimeMillis(); for (Iterator<Session> i = m_sessions.values().iterator(); i.hasNext();) { MySession s = (MySession) i.next(); if ((now - s.getLastAccessedTime()) < (secs * 1000)) { activeusers.add(s.getUserId()); } } // Ignore admin and postmaster activeusers.remove("admin"); activeusers.remove("postmaster"); activeusers.remove(null); return activeusers.size(); } /********************************************************************************************************************************************************************************************************************************************************** * Maintenance *********************************************************************************************************************************************************************************************************************************************************/ protected class Maintenance implements Runnable { /** My thread running my timeout checker. */ protected Thread m_maintenanceChecker = null; /** Signal to the timeout checker to stop. */ protected boolean m_maintenanceCheckerStop = false; /** * Construct. */ public Maintenance() { } /** * Start the maintenance thread. */ public void start() { if (m_maintenanceChecker != null) return; m_maintenanceChecker = new Thread(this, "Sakai.SessionComponent.Maintenance"); m_maintenanceCheckerStop = false; m_maintenanceChecker.setDaemon(true); m_maintenanceChecker.start(); } /** * Stop the maintenance thread. */ public void stop() { if (m_maintenanceChecker != null) { m_maintenanceCheckerStop = true; m_maintenanceChecker.interrupt(); try { // wait for it to die m_maintenanceChecker.join(); } catch (InterruptedException ignore) { } m_maintenanceChecker = null; } } /** * Run the maintenance thread. Every m_checkEvery seconds, check for expired sessions. */ public void run() { // since we might be running while the component manager is still being created and populated, such as at server // startup, wait here for a complete component manager ComponentManager.waitTillConfigured(); while (!m_maintenanceCheckerStop) { try { for (Map.Entry<String, MutableLong> entry: expirationTimeSuggestionMap.entrySet()) { if (entry.getValue().longValue() < System.currentTimeMillis()) { MySession s = (MySession)m_sessions.get(entry.getKey()); if (M_log.isDebugEnabled()) M_log.debug("checking session " + s.getId()); if (s.isInactive()) { if (M_log.isDebugEnabled()) M_log.debug("invalidating session " + s.getId()); synchronized(s) { s.invalidate(); } } } } } catch (Exception e) { M_log.warn("run(): exception: " + e); } // cycle every REFRESH seconds if (!m_maintenanceCheckerStop) { try { Thread.sleep(m_checkEvery * 1000L); } catch (Exception ignore) { } } } } } public SessionAttributeListener getSessionListener() { return sessionListener; } public void setSessionListener(SessionAttributeListener sessionListener) { this.sessionListener = sessionListener; } private static String byteArrayToHexStr(byte[] data) { char[] chars = new char[data.length * 2]; for (int i = 0; i < data.length; i++) { byte current = data[i]; int hi = (current & 0xF0) >> 4; int lo = current & 0x0F; chars[2*i] = (char) (hi < 10 ? ('0' + hi) : ('A' + hi - 10)); chars[2*i+1] = (char) (lo < 10 ? ('0' + lo) : ('A' + lo - 10)); } return new String(chars); } }