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 }