1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.server.session;
20
21 import io.undertow.UndertowLogger;
22 import io.undertow.UndertowMessages;
23 import io.undertow.server.HttpServerExchange;
24 import io.undertow.util.AttachmentKey;
25 import io.undertow.util.ConcurrentDirectDeque;
26 import io.undertow.util.WorkerUtils;
27
28 import java.math.BigDecimal;
29 import java.math.BigInteger;
30 import java.math.MathContext;
31 import java.security.AccessController;
32 import java.security.PrivilegedAction;
33 import java.util.HashSet;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.concurrent.ConcurrentMap;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.atomic.AtomicInteger;
40 import java.util.concurrent.atomic.AtomicLong;
41 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
42
43 import org.xnio.XnioExecutor;
44 import org.xnio.XnioIoThread;
45 import org.xnio.XnioWorker;
46
47 /**
48  * The default in memory session manager. This basically just stores sessions in an in memory hash map.
49  * <p>
50  *
51  * @author Stuart Douglas
52  */

53 public class InMemorySessionManager implements SessionManager, SessionManagerStatistics {
54
55     private final AttachmentKey<SessionImpl> NEW_SESSION = AttachmentKey.create(SessionImpl.class);
56
57     private final SessionIdGenerator sessionIdGenerator;
58
59     private final ConcurrentMap<String, SessionImpl> sessions;
60
61     private final SessionListeners sessionListeners = new SessionListeners();
62
63     /**
64      * 30 minute default
65      */

66     private volatile int defaultSessionTimeout = 30 * 60;
67
68     private final int maxSize;
69
70     private final ConcurrentDirectDeque<String> evictionQueue;
71
72     private final String deploymentName;
73
74     private final AtomicLong createdSessionCount = new AtomicLong();
75     private final AtomicLong rejectedSessionCount = new AtomicLong();
76     private volatile long longestSessionLifetime = 0;
77     private volatile long expiredSessionCount = 0;
78     private volatile BigInteger totalSessionLifetime = BigInteger.ZERO;
79     private final AtomicInteger highestSessionCount = new AtomicInteger();
80
81     private final boolean statisticsEnabled;
82
83     private volatile long startTime;
84
85     private final boolean expireOldestUnusedSessionOnMax;
86
87
88     public InMemorySessionManager(String deploymentName, int maxSessions, boolean expireOldestUnusedSessionOnMax) {
89         this(new SecureRandomSessionIdGenerator(), deploymentName, maxSessions, expireOldestUnusedSessionOnMax);
90     }
91
92     public InMemorySessionManager(SessionIdGenerator sessionIdGenerator, String deploymentName, int maxSessions, boolean expireOldestUnusedSessionOnMax) {
93         this(sessionIdGenerator, deploymentName, maxSessions, expireOldestUnusedSessionOnMax, true);
94     }
95
96     public InMemorySessionManager(SessionIdGenerator sessionIdGenerator, String deploymentName, int maxSessions, boolean expireOldestUnusedSessionOnMax, boolean statisticsEnabled) {
97         this.sessionIdGenerator = sessionIdGenerator;
98         this.deploymentName = deploymentName;
99         this.statisticsEnabled = statisticsEnabled;
100         this.expireOldestUnusedSessionOnMax = expireOldestUnusedSessionOnMax;
101         this.sessions = new ConcurrentHashMap<>();
102         this.maxSize = maxSessions;
103         ConcurrentDirectDeque<String> evictionQueue = null;
104         if (maxSessions > 0 && expireOldestUnusedSessionOnMax) {
105             evictionQueue = ConcurrentDirectDeque.newInstance();
106         }
107         this.evictionQueue = evictionQueue;
108     }
109
110     public InMemorySessionManager(String deploymentName, int maxSessions) {
111         this(deploymentName, maxSessions, false);
112     }
113
114     public InMemorySessionManager(String id) {
115         this(id, -1);
116     }
117
118     @Override
119     public String getDeploymentName() {
120         return this.deploymentName;
121     }
122
123     @Override
124     public void start() {
125         createdSessionCount.set(0);
126         expiredSessionCount = 0;
127         rejectedSessionCount.set(0);
128         totalSessionLifetime = BigInteger.ZERO;
129         startTime = System.currentTimeMillis();
130     }
131
132     @Override
133     public void stop() {
134         for (Map.Entry<String, SessionImpl> session : sessions.entrySet()) {
135             session.getValue().destroy();
136             sessionListeners.sessionDestroyed(session.getValue(), null, SessionListener.SessionDestroyedReason.UNDEPLOY);
137         }
138         sessions.clear();
139     }
140
141     @Override
142     public Session createSession(final HttpServerExchange serverExchange, final SessionConfig config) {
143         if (maxSize > 0) {
144             if(expireOldestUnusedSessionOnMax) {
145                 while (sessions.size() >= maxSize && !evictionQueue.isEmpty()) {
146
147                     String key = evictionQueue.poll();
148                     UndertowLogger.REQUEST_LOGGER.debugf("Removing session %s as max size has been hit", key);
149                     SessionImpl toRemove = sessions.get(key);
150                     if (toRemove != null) {
151                         toRemove.invalidate(null, SessionListener.SessionDestroyedReason.TIMEOUT); //todo: better reason
152                     }
153                 }
154             } else if(sessions.size() >= maxSize) {
155                 if(statisticsEnabled) {
156                     rejectedSessionCount.incrementAndGet();
157                 }
158                 throw UndertowMessages.MESSAGES.tooManySessions(maxSize);
159             }
160         }
161         if (config == null) {
162             throw UndertowMessages.MESSAGES.couldNotFindSessionCookieConfig();
163         }
164         String sessionID = config.findSessionId(serverExchange);
165         if (sessionID == null) {
166             int count = 0;
167             while (sessionID == null) {
168                 sessionID = sessionIdGenerator.createSessionId();
169                 if (sessions.containsKey(sessionID)) {
170                     sessionID = null;
171                 }
172                 if (count++ == 100) {
173                     //this should never happen
174                     //but we guard against pathalogical session id generators to prevent an infinite loop
175                     throw UndertowMessages.MESSAGES.couldNotGenerateUniqueSessionId();
176                 }
177             }
178         } else {
179             if (sessions.containsKey(sessionID)) {
180                 throw UndertowMessages.MESSAGES.sessionWithIdAlreadyExists(sessionID);
181             }
182         }
183         Object evictionToken;
184         if (evictionQueue != null) {
185             evictionToken = evictionQueue.offerLastAndReturnToken(sessionID);
186         } else {
187             evictionToken = null;
188         }
189         final SessionImpl session = new SessionImpl(this, sessionID, config, serverExchange.getIoThread(), serverExchange.getConnection().getWorker(), evictionToken, defaultSessionTimeout);
190
191         UndertowLogger.SESSION_LOGGER.debugf("Created session with id %s for exchange %s", sessionID, serverExchange);
192         sessions.put(sessionID, session);
193         config.setSessionId(serverExchange, session.getId());
194         session.bumpTimeout();
195         sessionListeners.sessionCreated(session, serverExchange);
196         serverExchange.putAttachment(NEW_SESSION, session);
197
198         if(statisticsEnabled) {
199             createdSessionCount.incrementAndGet();
200             int highest;
201             int sessionSize;
202             do {
203                 highest = highestSessionCount.get();
204                 sessionSize = sessions.size();
205                 if(sessionSize <= highest) {
206                     break;
207                 }
208             } while (!highestSessionCount.compareAndSet(highest, sessionSize));
209         }
210         return session;
211     }
212
213     @Override
214     public Session getSession(final HttpServerExchange serverExchange, final SessionConfig config) {
215         if (serverExchange != null) {
216             SessionImpl newSession = serverExchange.getAttachment(NEW_SESSION);
217             if(newSession != null) {
218                 return newSession;
219             }
220         }
221         String sessionId = config.findSessionId(serverExchange);
222         InMemorySessionManager.SessionImpl session = (SessionImpl) getSession(sessionId);
223         if(session != null && serverExchange != null) {
224             session.requestStarted(serverExchange);
225         }
226         return session;
227     }
228
229     @Override
230     public Session getSession(String sessionId) {
231         if (sessionId == null) {
232             return null;
233         }
234         final SessionImpl sess = sessions.get(sessionId);
235         if (sess == null) {
236             return null;
237         } else {
238             return sess;
239         }
240     }
241
242
243     @Override
244     public synchronized void registerSessionListener(final SessionListener listener) {
245         UndertowLogger.SESSION_LOGGER.debugf("Registered session listener %s", listener);
246         sessionListeners.addSessionListener(listener);
247     }
248
249     @Override
250     public synchronized void removeSessionListener(final SessionListener listener) {
251         UndertowLogger.SESSION_LOGGER.debugf("Removed session listener %s", listener);
252         sessionListeners.removeSessionListener(listener);
253     }
254
255     @Override
256     public void setDefaultSessionTimeout(final int timeout) {
257         UndertowLogger.SESSION_LOGGER.debugf("Setting default session timeout to %s", timeout);
258         defaultSessionTimeout = timeout;
259     }
260
261     @Override
262     public Set<String> getTransientSessions() {
263         return getAllSessions();
264     }
265
266     @Override
267     public Set<String> getActiveSessions() {
268         return getAllSessions();
269     }
270
271     @Override
272     public Set<String> getAllSessions() {
273         return new HashSet<>(sessions.keySet());
274     }
275
276     @Override
277     public boolean equals(Object object) {
278         if (!(object instanceof SessionManager)) return false;
279         SessionManager manager = (SessionManager) object;
280         return this.deploymentName.equals(manager.getDeploymentName());
281     }
282
283     @Override
284     public int hashCode() {
285         return this.deploymentName.hashCode();
286     }
287
288     @Override
289     public String toString() {
290         return this.deploymentName;
291     }
292
293     @Override
294     public SessionManagerStatistics getStatistics() {
295         return this;
296     }
297
298     public long getCreatedSessionCount() {
299         return createdSessionCount.get();
300     }
301
302     @Override
303     public long getMaxActiveSessions() {
304         return maxSize;
305     }
306
307     @Override
308     public long getHighestSessionCount() {
309         return highestSessionCount.get();
310     }
311
312     @Override
313     public long getActiveSessionCount() {
314         return sessions.size();
315     }
316
317     @Override
318     public long getExpiredSessionCount() {
319         return expiredSessionCount;
320     }
321
322     @Override
323     public long getRejectedSessions() {
324         return rejectedSessionCount.get();
325
326     }
327
328     @Override
329     public long getMaxSessionAliveTime() {
330         return longestSessionLifetime;
331     }
332
333     @Override
334     public synchronized long getAverageSessionAliveTime() {
335         //this method needs to be synchronised to make sure the session count and the total are in sync
336         if(expiredSessionCount == 0) {
337             return 0;
338         }
339         return new BigDecimal(totalSessionLifetime).divide(BigDecimal.valueOf(expiredSessionCount), MathContext.DECIMAL128).longValue();
340     }
341
342     @Override
343     public long getStartTime() {
344         return startTime;
345     }
346
347
348     /**
349      * session implementation for the in memory session manager
350      */

351     private static class SessionImpl implements Session {
352
353
354         final AttachmentKey<Long> FIRST_REQUEST_ACCESS = AttachmentKey.create(Long.class);
355         final InMemorySessionManager sessionManager;
356         final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
357         volatile long lastAccessed;
358         final long creationTime;
359         volatile int maxInactiveInterval;
360
361         static volatile AtomicReferenceFieldUpdater<SessionImpl, Object> evictionTokenUpdater;
362         static {
363             //this is needed in case there is unprivileged code on the stack
364             //it needs to delegate to the createTokenUpdater() method otherwise the creation will fail
365             //as the inner class cannot access the member
366             evictionTokenUpdater = AccessController.doPrivileged(new PrivilegedAction<AtomicReferenceFieldUpdater<SessionImpl, Object>>() {
367                 @Override
368                 public AtomicReferenceFieldUpdater<SessionImpl, Object> run() {
369                     return createTokenUpdater();
370                 }
371             });
372         }
373
374         private static AtomicReferenceFieldUpdater<SessionImpl, Object> createTokenUpdater() {
375             return AtomicReferenceFieldUpdater.newUpdater(SessionImpl.class, Object.class"evictionToken");
376         }
377
378
379         private String sessionId;
380         private volatile Object evictionToken;
381         private final SessionConfig sessionCookieConfig;
382         private volatile long expireTime = -1;
383         private volatile boolean invalid = false;
384         private volatile boolean invalidationStarted = false;
385
386         final XnioIoThread executor;
387         final XnioWorker worker;
388
389         XnioExecutor.Key timerCancelKey;
390
391         Runnable cancelTask = new Runnable() {
392             @Override
393             public void run() {
394                 worker.execute(new Runnable() {
395                     @Override
396                     public void run() {
397                         long currentTime = System.currentTimeMillis();
398                         if(currentTime >= expireTime) {
399                             invalidate(null, SessionListener.SessionDestroyedReason.TIMEOUT);
400                         } else {
401                             timerCancelKey = WorkerUtils.executeAfter(executor, cancelTask, expireTime - currentTime, TimeUnit.MILLISECONDS);
402                         }
403                     }
404                 });
405             }
406         };
407
408         private SessionImpl(final InMemorySessionManager sessionManager, final String sessionId, final SessionConfig sessionCookieConfig, final XnioIoThread executor, final XnioWorker worker, final Object evictionToken, final int maxInactiveInterval) {
409             this.sessionManager = sessionManager;
410             this.sessionId = sessionId;
411             this.sessionCookieConfig = sessionCookieConfig;
412             this.executor = executor;
413             this.worker = worker;
414             this.evictionToken = evictionToken;
415             creationTime = lastAccessed = System.currentTimeMillis();
416             this.maxInactiveInterval = maxInactiveInterval;
417         }
418
419         synchronized void bumpTimeout() {
420             if(invalidationStarted) {
421                 return;
422             }
423
424             final int maxInactiveInterval = getMaxInactiveInterval();
425             if (maxInactiveInterval > 0) {
426                 long newExpireTime = System.currentTimeMillis() + (maxInactiveInterval * 1000L);
427                 if(timerCancelKey != null && (newExpireTime < expireTime)) {
428                     // We have to re-schedule as the new maxInactiveInterval is lower than the old one
429                     if (!timerCancelKey.remove()) {
430                         return;
431                     }
432                     timerCancelKey = null;
433                 }
434                 expireTime = newExpireTime;
435                 UndertowLogger.SESSION_LOGGER.tracef("Bumping timeout for session %s to %s", sessionId, expireTime);
436                 if(timerCancelKey == null) {
437                     //+1, to make sure that the time has actually expired
438                     //we don't re-schedule every time, as it is expensive
439                     //instead when it expires we check if the timeout has been bumped, and if so we re-schedule
440                     timerCancelKey = executor.executeAfter(cancelTask, (maxInactiveInterval * 1000L) + 1L, TimeUnit.MILLISECONDS);
441                 }
442             } else {
443                 expireTime = -1;
444                 if(timerCancelKey != null) {
445                     timerCancelKey.remove();
446                     timerCancelKey = null;
447                 }
448             }
449             if (evictionToken != null) {
450                 Object token = evictionToken;
451                 if (evictionTokenUpdater.compareAndSet(this, token, null)) {
452                     sessionManager.evictionQueue.removeToken(token);
453                     this.evictionToken = sessionManager.evictionQueue.offerLastAndReturnToken(sessionId);
454                 }
455             }
456         }
457
458
459         @Override
460         public String getId() {
461             return sessionId;
462         }
463
464         void requestStarted(HttpServerExchange serverExchange) {
465             Long existing = serverExchange.getAttachment(FIRST_REQUEST_ACCESS);
466             if(existing == null) {
467                 if (!invalid) {
468                     serverExchange.putAttachment(FIRST_REQUEST_ACCESS, System.currentTimeMillis());
469                 }
470             }
471         }
472
473         @Override
474         public void requestDone(final HttpServerExchange serverExchange) {
475             Long existing = serverExchange.getAttachment(FIRST_REQUEST_ACCESS);
476             if(existing != null) {
477                 lastAccessed = existing;
478             }
479             bumpTimeout();
480         }
481
482         @Override
483         public long getCreationTime() {
484             if (invalid) {
485                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
486             }
487             return creationTime;
488         }
489
490         @Override
491         public long getLastAccessedTime() {
492             if (invalid) {
493                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
494             }
495             return lastAccessed;
496         }
497
498         @Override
499         public void setMaxInactiveInterval(final int interval) {
500             if (invalid) {
501                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
502             }
503             UndertowLogger.SESSION_LOGGER.debugf("Setting max inactive interval for %s to %s", sessionId, interval);
504             maxInactiveInterval = interval;
505         }
506
507         @Override
508         public int getMaxInactiveInterval() {
509             if (invalid) {
510                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
511             }
512             return maxInactiveInterval;
513         }
514
515         @Override
516         public Object getAttribute(final String name) {
517             if (invalid) {
518                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
519             }
520             return attributes.get(name);
521         }
522
523         @Override
524         public Set<String> getAttributeNames() {
525             if (invalid) {
526                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
527             }
528             return attributes.keySet();
529         }
530
531         @Override
532         public Object setAttribute(final String name, final Object value) {
533             if (value == null) {
534                 return removeAttribute(name);
535             }
536             if (invalid) {
537                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
538             }
539             final Object existing = attributes.put(name, value);
540             if (existing == null) {
541                 sessionManager.sessionListeners.attributeAdded(this, name, value);
542             } else {
543                sessionManager.sessionListeners.attributeUpdated(this, name, value, existing);
544             }
545             UndertowLogger.SESSION_LOGGER.tracef("Setting session attribute %s to %s for session %s", name, value, sessionId);
546             return existing;
547         }
548
549         @Override
550         public Object removeAttribute(final String name) {
551             if (invalid) {
552                 throw UndertowMessages.MESSAGES.sessionIsInvalid(sessionId);
553             }
554             final Object existing = attributes.remove(name);
555             sessionManager.sessionListeners.attributeRemoved(this, name, existing);
556             UndertowLogger.SESSION_LOGGER.tracef("Removing session attribute %s for session %s", name, sessionId);
557             return existing;
558         }
559
560         @Override
561         public void invalidate(final HttpServerExchange exchange) {
562             invalidate(exchange, SessionListener.SessionDestroyedReason.INVALIDATED);
563             if(exchange != null) {
564                 exchange.removeAttachment(sessionManager.NEW_SESSION);
565             }
566             Object evictionToken = this.evictionToken;
567             if(evictionToken != null) {
568                 sessionManager.evictionQueue.removeToken(evictionToken);
569             }
570         }
571
572         void invalidate(final HttpServerExchange exchange, SessionListener.SessionDestroyedReason reason) {
573             synchronized(SessionImpl.this) {
574                 if (timerCancelKey != null) {
575                     timerCancelKey.remove();
576                 }
577                 SessionImpl sess = sessionManager.sessions.remove(sessionId);
578                 if (sess == null) {
579                     if (reason == SessionListener.SessionDestroyedReason.INVALIDATED) {
580                         throw UndertowMessages.MESSAGES.sessionAlreadyInvalidated();
581                     }
582                     return;
583                 }
584                 invalidationStarted = true;
585             }
586             UndertowLogger.SESSION_LOGGER.debugf("Invalidating session %s for exchange %s", sessionId, exchange);
587
588             sessionManager.sessionListeners.sessionDestroyed(this, exchange, reason);
589             invalid = true;
590
591             if(sessionManager.statisticsEnabled) {
592                 long life = System.currentTimeMillis() - creationTime;
593                 synchronized (sessionManager) {
594                     sessionManager.expiredSessionCount++;
595                     sessionManager.totalSessionLifetime = sessionManager.totalSessionLifetime.add(BigInteger.valueOf(life));
596                     if(sessionManager.longestSessionLifetime < life) {
597                         sessionManager.longestSessionLifetime = life;
598                     }
599                 }
600             }
601             if (exchange != null) {
602                 sessionCookieConfig.clearSession(exchange, this.getId());
603             }
604         }
605
606         @Override
607         public SessionManager getSessionManager() {
608             return sessionManager;
609         }
610
611         @Override
612         public String changeSessionId(final HttpServerExchange exchange, final SessionConfig config) {
613             final String oldId = sessionId;
614             String newId = sessionManager.sessionIdGenerator.createSessionId();
615             this.sessionId = newId;
616             if(!invalid) {
617                 sessionManager.sessions.put(newId, this);
618                 config.setSessionId(exchange, this.getId());
619             }
620             sessionManager.sessions.remove(oldId);
621             sessionManager.sessionListeners.sessionIdChanged(this, oldId);
622             UndertowLogger.SESSION_LOGGER.debugf("Changing session id %s to %s", oldId, newId);
623
624             return newId;
625         }
626
627         private synchronized void destroy() {
628             if (timerCancelKey != null) {
629                 timerCancelKey.remove();
630             }
631             cancelTask = null;
632         }
633
634     }
635 }
636