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} and {@link KeyStore}. 044 * It provides the functionality to open and close sessions, expire them automatically after 045 * a certain time etc. 046 * </p> 047 * <p> 048 * This is not API! Use the classes and interfaces provided by <code>org.cumulus4j.keymanager.api</code> instead. 049 * </p> 050 * 051 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de 052 */ 053 public class SessionManager 054 { 055 private static final Logger logger = LoggerFactory.getLogger(SessionManager.class); 056 057 private static final long EXPIRY_AGE_MSEC = 3L * 60L * 1000L; // TODO make configurable 058 059 private static Timer expireSessionTimer = new Timer(SessionManager.class.getSimpleName(), true); 060 061 private TimerTask expireSessionTimerTask = new ExpireSessionTimerTask(this); 062 063 private static class ExpireSessionTimerTask extends TimerTask 064 { 065 private static final Logger logger = LoggerFactory.getLogger(ExpireSessionTimerTask.class); 066 067 private WeakReference<SessionManager> sessionManagerRef; 068 069 public ExpireSessionTimerTask(SessionManager sessionManager) 070 { 071 if (sessionManager == null) 072 throw new IllegalArgumentException("sessionManager == null"); 073 074 this.sessionManagerRef = new WeakReference<SessionManager>(sessionManager); 075 } 076 077 @Override 078 public void run() 079 { 080 try { 081 SessionManager sessionManager = sessionManagerRef.get(); 082 if (sessionManager == null) { 083 logger.info("run: SessionManager has been garbage-collected. Removing this ExpireSessionTimerTask."); 084 this.cancel(); 085 return; 086 } 087 088 Date now = new Date(); 089 090 LinkedList<Session> sessionsToExpire = new LinkedList<Session>(); 091 synchronized (sessionManager) { 092 for (Session session : sessionManager.cryptoSessionID2Session.values()) { 093 if (session.getExpiry().before(now)) 094 sessionsToExpire.add(session); 095 } 096 } 097 098 for (Session session : sessionsToExpire) { 099 logger.info("run: Expiring session: userName='{}' cryptoSessionID='{}'.", session.getUserName(), session.getCryptoSessionID()); 100 session.destroy(); 101 } 102 103 if (logger.isDebugEnabled()) { 104 synchronized (sessionManager) { 105 logger.debug("run: {} sessions left.", sessionManager.cryptoSessionID2Session.size()); 106 } 107 } 108 } catch (Throwable x) { 109 // The TimerThread is cancelled, if a task throws an exception. Furthermore, they are not logged at all. 110 // Since we do not want the TimerThread to die, we catch everything (Throwable - not only Exception) and log 111 // it here. IMHO there's nothing better we can do. Marco :-) 112 logger.error("run: " + x, x); 113 } 114 } 115 } 116 117 private String cryptoSessionIDPrefix; 118 private KeyStore keyStore; 119 120 private Map<String, List<Session>> userName2SessionList = new HashMap<String, List<Session>>(); 121 private Map<String, Session> cryptoSessionID2Session = new HashMap<String, Session>(); 122 123 public SessionManager(KeyStore keyStore) 124 { 125 logger.info("Creating instance of SessionManager."); 126 this.keyStore = keyStore; 127 // TODO it should be possible to configure the clusterNodeID somehow to make it shorter. 128 // This default is unique enough (see IdentifierUtilTest#simpleUniquenessTest). 129 // I tested generating 100000 IDs many many times and there was no collision in 130 // these 100k randomIDs. Since we'll never have a key-server-cluster with more 131 // 100 nodes, such uniqueness should be absolutely sufficient. 132 String clusterNodeID = IdentifierUtil.createRandomID(8); 133 134 // see org.cumulus4j.store.crypto.AbstractCryptoSession#getKeyStoreID() 135 this.cryptoSessionIDPrefix = keyStore.getKeyStoreID() + '_' + clusterNodeID; 136 expireSessionTimer.schedule(expireSessionTimerTask, 60000, 60000); // TODO make this configurable 137 } 138 139 private AtomicLong lastCryptoSessionSerial = new AtomicLong(); 140 141 protected long nextCryptoSessionSerial() 142 { 143 return lastCryptoSessionSerial.incrementAndGet(); 144 } 145 146 public String getCryptoSessionIDPrefix() { 147 return cryptoSessionIDPrefix; 148 } 149 150 public KeyStore getKeyStore() { 151 return keyStore; 152 } 153 154 private static final void doNothing() { } 155 156 protected synchronized void onReacquireSession(Session session) 157 { 158 if (session == null) 159 throw new IllegalArgumentException("session == null"); 160 161 if (cryptoSessionID2Session.get(session.getCryptoSessionID()) != session) 162 throw new IllegalStateException("The session with cryptoSessionID=\"" + session.getCryptoSessionID() + "\" is not known. Dead reference already expired and destroyed?"); 163 164 if (session.getExpiry().before(new Date())) 165 throw new IllegalStateException("The session with cryptoSessionID=\"" + session.getCryptoSessionID() + "\" is already expired. It is still known, but cannot be reacquired anymore!"); 166 167 session.updateLastUse(EXPIRY_AGE_MSEC); 168 } 169 170 /** 171 * Create a new unlocked session or open (unlock) a cached & currently locked session. 172 * 173 * @return the {@link Session}. 174 * @throws AuthenticationException if the login fails 175 */ 176 public synchronized Session acquireSession(String userName, char[] password) throws AuthenticationException 177 { 178 try { 179 keyStore.getKey(userName, password, Long.MAX_VALUE); 180 } catch (KeyNotFoundException e) { 181 // very likely, the key does not exist - this is expected and OK! 182 doNothing(); // Remove warning from PMD report: http://cumulus4j.org/latest-dev/pmd.html 183 } 184 185 List<Session> sessionList = userName2SessionList.get(userName); 186 if (sessionList == null) { 187 sessionList = new LinkedList<Session>(); 188 userName2SessionList.put(userName, sessionList); 189 } 190 191 Session session = null; 192 List<Session> sessionsToClose = null; 193 for (Session s : sessionList) { 194 // We make sure we never re-use an expired session, even if it hasn't been closed by the timer yet. 195 if (s.getExpiry().before(new Date())) { 196 if (sessionsToClose == null) 197 sessionsToClose = new LinkedList<Session>(); 198 199 sessionsToClose.add(s); 200 continue; 201 } 202 203 if (s.isReleased()) { 204 session = s; 205 break; 206 } 207 } 208 209 if (sessionsToClose != null) { 210 for (Session s : sessionsToClose) 211 s.destroy(); 212 } 213 214 if (session == null) { 215 session = new Session(this, userName, password); 216 sessionList.add(session); 217 cryptoSessionID2Session.put(session.getCryptoSessionID(), session); 218 219 // TODO notify listeners - maybe always notify listeners (i.e. when an existing session is refreshed, too)?! 220 } 221 222 session.setReleased(false); 223 session.updateLastUse(EXPIRY_AGE_MSEC); 224 225 return session; 226 } 227 228 protected synchronized void onDestroySession(Session session) 229 { 230 if (session == null) 231 throw new IllegalArgumentException("session == null"); 232 233 // TODO notify listeners 234 List<Session> sessionList = userName2SessionList.get(session.getUserName()); 235 if (sessionList == null) 236 logger.warn("onDestroySession: userName2SessionList.get(\"{}\") returned null!", session.getUserName()); 237 else { 238 for (Iterator<Session> it = sessionList.iterator(); it.hasNext();) { 239 Session s = it.next(); 240 if (s == session) { 241 it.remove(); 242 break; 243 } 244 } 245 } 246 247 cryptoSessionID2Session.remove(session.getCryptoSessionID()); 248 249 if (sessionList == null || sessionList.isEmpty()) { 250 userName2SessionList.remove(session.getUserName()); 251 keyStore.clearCache(session.getUserName()); 252 } 253 } 254 255 // public synchronized Session getSessionForUserName(String userName) 256 // { 257 // Session session = userName2Session.get(userName); 258 // return session; 259 // } 260 261 public synchronized Session getSessionForCryptoSessionID(String cryptoSessionID) 262 { 263 Session session = cryptoSessionID2Session.get(cryptoSessionID); 264 return session; 265 } 266 267 public synchronized void onReleaseSession(Session session) 268 { 269 if (session == null) 270 throw new IllegalArgumentException("session == null"); 271 272 session.setReleased(true); 273 } 274 }