001 /* 002 * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org 003 * Copyright (C) 2011 NightLabs Consulting GmbH 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 package org.cumulus4j.keymanager; 019 020 import java.lang.ref.WeakReference; 021 import java.util.Date; 022 import java.util.HashMap; 023 import java.util.Iterator; 024 import java.util.LinkedList; 025 import java.util.List; 026 import java.util.Map; 027 import java.util.Timer; 028 import java.util.TimerTask; 029 import java.util.concurrent.atomic.AtomicLong; 030 031 import org.cumulus4j.keymanager.back.shared.IdentifierUtil; 032 import org.cumulus4j.keystore.AuthenticationException; 033 import org.cumulus4j.keystore.KeyNotFoundException; 034 import org.cumulus4j.keystore.KeyStore; 035 import org.slf4j.Logger; 036 import org.slf4j.LoggerFactory; 037 038 /** 039 * <p> 040 * Manager for {@link Session}s. 041 * </p> 042 * <p> 043 * There is one <code>SessionManager</code> for each {@link AppServer}. It provides the functionality to 044 * open and close sessions, expire them automatically after a certain time etc. 045 * </p> 046 * <p> 047 * This is not API! Use the classes and interfaces provided by <code>org.cumulus4j.keymanager.api</code> instead. 048 * </p> 049 * 050 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de 051 */ 052 public class SessionManager 053 { 054 private static final Logger logger = LoggerFactory.getLogger(SessionManager.class); 055 056 private static final long EXPIRY_AGE_MSEC = 3L * 60L * 1000L; // TODO make configurable 057 058 private static Timer expireSessionTimer = new Timer(); 059 060 private TimerTask expireSessionTimerTask = new ExpireSessionTimerTask(this); 061 062 private static class ExpireSessionTimerTask extends TimerTask 063 { 064 private static final Logger logger = LoggerFactory.getLogger(ExpireSessionTimerTask.class); 065 066 private WeakReference<SessionManager> sessionManagerRef; 067 068 public ExpireSessionTimerTask(SessionManager sessionManager) 069 { 070 if (sessionManager == null) 071 throw new IllegalArgumentException("sessionManager == null"); 072 073 this.sessionManagerRef = new WeakReference<SessionManager>(sessionManager); 074 } 075 076 @Override 077 public void run() 078 { 079 try { 080 SessionManager sessionManager = sessionManagerRef.get(); 081 if (sessionManager == null) { 082 logger.info("run: SessionManager has been garbage-collected. Removing this ExpireSessionTimerTask."); 083 this.cancel(); 084 return; 085 } 086 087 Date now = new Date(); 088 089 LinkedList<Session> sessionsToExpire = new LinkedList<Session>(); 090 synchronized (sessionManager) { 091 for (Session session : sessionManager.cryptoSessionID2Session.values()) { 092 if (session.getExpiry().before(now)) 093 sessionsToExpire.add(session); 094 } 095 } 096 097 for (Session session : sessionsToExpire) { 098 logger.info("run: Expiring session: userName='{}' cryptoSessionID='{}'.", session.getUserName(), session.getCryptoSessionID()); 099 session.destroy(); 100 } 101 102 if (logger.isDebugEnabled()) { 103 synchronized (sessionManager) { 104 logger.debug("run: {} sessions left.", sessionManager.cryptoSessionID2Session.size()); 105 } 106 } 107 } catch (Throwable x) { 108 // The TimerThread is cancelled, if a task throws an exception. Furthermore, they are not logged at all. 109 // Since we do not want the TimerThread to die, we catch everything (Throwable - not only Exception) and log 110 // it here. IMHO there's nothing better we can do. Marco :-) 111 logger.error("run: " + x, x); 112 } 113 } 114 } 115 116 private String cryptoSessionIDPrefix; 117 private KeyStore keyStore; 118 119 private Map<String, List<Session>> userName2SessionList = new HashMap<String, List<Session>>(); 120 private Map<String, Session> cryptoSessionID2Session = new HashMap<String, Session>(); 121 122 public SessionManager(KeyStore keyStore) 123 { 124 logger.info("Creating instance of SessionManager."); 125 this.keyStore = keyStore; 126 this.cryptoSessionIDPrefix = IdentifierUtil.createRandomID(); 127 expireSessionTimer.schedule(expireSessionTimerTask, 60000, 60000); // TODO make this configurable 128 } 129 130 private AtomicLong lastCryptoSessionSerial = new AtomicLong(); 131 132 protected long nextCryptoSessionSerial() 133 { 134 return lastCryptoSessionSerial.incrementAndGet(); 135 } 136 137 public String getCryptoSessionIDPrefix() { 138 return cryptoSessionIDPrefix; 139 } 140 141 public KeyStore getKeyStore() { 142 return keyStore; 143 } 144 145 private static final void doNothing() { } 146 147 protected synchronized void onReacquireSession(Session session) 148 { 149 if (session == null) 150 throw new IllegalArgumentException("session == null"); 151 152 if (cryptoSessionID2Session.get(session.getCryptoSessionID()) != session) 153 throw new IllegalStateException("The session with cryptoSessionID=\"" + session.getCryptoSessionID() + "\" is not known. Dead reference already expired and destroyed?"); 154 155 if (session.getExpiry().before(new Date())) 156 throw new IllegalStateException("The session with cryptoSessionID=\"" + session.getCryptoSessionID() + "\" is already expired. It is still known, but cannot be reacquired anymore!"); 157 158 session.updateLastUse(EXPIRY_AGE_MSEC); 159 } 160 161 /** 162 * Create a new unlocked session or open (unlock) a cached & currently locked session. 163 * 164 * @return the {@link Session}. 165 * @throws AuthenticationException if the login fails 166 */ 167 public synchronized Session acquireSession(String userName, char[] password) throws AuthenticationException 168 { 169 try { 170 keyStore.getKey(userName, password, Long.MAX_VALUE); 171 } catch (KeyNotFoundException e) { 172 // very likely, the key does not exist - this is expected and OK! 173 doNothing(); // Remove warning from PMD report: http://cumulus4j.org/latest-dev/pmd.html 174 } 175 176 List<Session> sessionList = userName2SessionList.get(userName); 177 if (sessionList == null) { 178 sessionList = new LinkedList<Session>(); 179 userName2SessionList.put(userName, sessionList); 180 } 181 182 Session session = null; 183 List<Session> sessionsToClose = null; 184 for (Session s : sessionList) { 185 // We make sure we never re-use an expired session, even if it hasn't been closed by the timer yet. 186 if (s.getExpiry().before(new Date())) { 187 if (sessionsToClose == null) 188 sessionsToClose = new LinkedList<Session>(); 189 190 sessionsToClose.add(s); 191 continue; 192 } 193 194 if (s.isReleased()) { 195 session = s; 196 break; 197 } 198 } 199 200 if (sessionsToClose != null) { 201 for (Session s : sessionsToClose) 202 s.destroy(); 203 } 204 205 if (session == null) { 206 session = new Session(this, userName, password); 207 sessionList.add(session); 208 cryptoSessionID2Session.put(session.getCryptoSessionID(), session); 209 210 // TODO notify listeners - maybe always notify listeners (i.e. when an existing session is refreshed, too)?! 211 } 212 213 session.setReleased(false); 214 session.updateLastUse(EXPIRY_AGE_MSEC); 215 216 return session; 217 } 218 219 protected synchronized void onDestroySession(Session session) 220 { 221 if (session == null) 222 throw new IllegalArgumentException("session == null"); 223 224 // TODO notify listeners 225 List<Session> sessionList = userName2SessionList.get(session.getUserName()); 226 if (sessionList == null) 227 logger.warn("onDestroySession: userName2SessionList.get(\"{}\") returned null!", session.getUserName()); 228 else { 229 for (Iterator<Session> it = sessionList.iterator(); it.hasNext();) { 230 Session s = it.next(); 231 if (s == session) { 232 it.remove(); 233 break; 234 } 235 } 236 } 237 238 cryptoSessionID2Session.remove(session.getCryptoSessionID()); 239 240 if (sessionList == null || sessionList.isEmpty()) { 241 userName2SessionList.remove(session.getUserName()); 242 keyStore.clearCache(session.getUserName()); 243 } 244 } 245 246 // public synchronized Session getSessionForUserName(String userName) 247 // { 248 // Session session = userName2Session.get(userName); 249 // return session; 250 // } 251 252 public synchronized Session getSessionForCryptoSessionID(String cryptoSessionID) 253 { 254 Session session = cryptoSessionID2Session.get(cryptoSessionID); 255 return session; 256 } 257 258 public synchronized void onReleaseSession(Session session) 259 { 260 if (session == null) 261 throw new IllegalArgumentException("session == null"); 262 263 session.setReleased(true); 264 } 265 }