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 }