| // Copyright 2011 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package adaptorlib; |
| |
| import com.sun.net.httpserver.HttpExchange; |
| |
| import java.security.SecureRandom; |
| import java.util.*; |
| |
| /** |
| * Generic session management, but intended for authn bookkeeping. It implements |
| * very lazy session cleanup and does not use any other threads. It cleans up |
| * old sessions as it creates a new session, so it is fine with keeping a |
| * session around for days past its expiration time if no new sessions are being |
| * created. |
| */ |
| class SessionManager<E> { |
| private static final String COOKIE_NAME = "sessid"; |
| |
| private final TimeProvider timeProvider; |
| private final CookieAccess<E> cookieAccess; |
| private final Map<String, Session> sessions = new HashMap<String, Session>(); |
| private final Map<String, Long> lastAccess = new HashMap<String, Long>(); |
| /** Lifetime of sessions, in milliseconds. */ |
| private final long sessionLifetime; |
| /** Maximum frequency to check for expired sessions, in milliseconds. */ |
| private final long cleanupFrequency; |
| private long nextCleanup; |
| private final Random random = new SecureRandom(); |
| |
| /** |
| * @param sessionLifetime lifetime of sessions, in milliseconds |
| * @param cleanupFrequency maximum frequency to check for expired sessions, in |
| * milliseconds |
| */ |
| public SessionManager(CookieAccess<E> cookieAccess, long sessionLifetime, |
| long cleanupFrequency) { |
| this(new SystemTimeProvider(), cookieAccess, sessionLifetime, |
| cleanupFrequency); |
| } |
| |
| protected SessionManager(TimeProvider timeProvider, |
| CookieAccess<E> cookieAccess, long sessionLifetime, |
| long cleanupFrequency) { |
| this.timeProvider = timeProvider; |
| this.cookieAccess = cookieAccess; |
| this.sessionLifetime = sessionLifetime; |
| this.cleanupFrequency = cleanupFrequency; |
| } |
| |
| public Session getSession(E cookieState) { |
| return getSession(cookieState, true); |
| } |
| |
| public Session getSession(E cookieState, boolean create) { |
| String value = cookieAccess.getCookie(cookieState, COOKIE_NAME); |
| if (value == null) { |
| // No pre-existing session found. |
| return create ? createSession(cookieState) : null; |
| } |
| |
| synchronized (this) { |
| Session session = sessions.get(value); |
| if (session != null) { |
| updateLastAccess(value); |
| return session; |
| } |
| |
| // Could not find session specified. Assume it expired. |
| return createSession(cookieState); |
| } |
| } |
| |
| protected synchronized Session createSession(E cookieState) { |
| cleanupExpiredSessions(); |
| Session session = new Session(); |
| byte[] rawId = new byte[16]; |
| random.nextBytes(rawId); |
| String id = bytesToString(rawId); |
| sessions.put(id, session); |
| cookieAccess.setCookie(cookieState, COOKIE_NAME, id); |
| updateLastAccess(id); |
| return session; |
| } |
| |
| protected synchronized void updateLastAccess(String id) { |
| lastAccess.put(id, timeProvider.currentTimeMillis()); |
| } |
| |
| protected synchronized void cleanupExpiredSessions() { |
| long currentTime = timeProvider.currentTimeMillis(); |
| if (nextCleanup > currentTime) { |
| return; |
| } |
| |
| List<String> toRemove = new LinkedList<String>(); |
| long ageLimit = currentTime - sessionLifetime; |
| for (Map.Entry<String, Long> me : lastAccess.entrySet()) { |
| if (ageLimit > (long) me.getValue()) { |
| toRemove.add(me.getKey()); |
| } |
| } |
| |
| for (String id : toRemove) { |
| sessions.remove(id); |
| lastAccess.remove(id); |
| } |
| |
| nextCleanup = currentTime + cleanupFrequency; |
| } |
| |
| /** |
| * Formats the provided bytes as a string. {@code b} must be a multiple of 4. |
| */ |
| private static String bytesToString(byte[] b) { |
| String s = ""; |
| for (int i = 0; i < b.length; i += 4) { |
| int j = bytesToInt(b, i); |
| s += toFixedSizeHexString(j); |
| } |
| return s; |
| } |
| |
| /** |
| * Packs the first four bytes starting at {@code start} into a single integer. |
| */ |
| private static int bytesToInt(byte[] b, int start) { |
| return ((b[start + 0] & 0xff) << 24) |
| | ((b[start + 1] & 0xff) << 16) |
| | ((b[start + 2] & 0xff) << 8) |
| | ((b[start + 3] & 0xff) << 0); |
| } |
| |
| /** |
| * Converts the provided integer to an eight character string. |
| */ |
| private static String toFixedSizeHexString(int i) { |
| String s = Integer.toHexString(i); |
| int padding = Integer.SIZE / 4 - s.length(); |
| for (int j = 0; j < padding; j++) { |
| s = "0" + s; |
| } |
| return s; |
| } |
| |
| public static interface CookieAccess<E> { |
| public String getCookie(E cookieState, String cookieName); |
| |
| public void setCookie(E cookieState, String cookieName, String cookieValue); |
| } |
| |
| public static class HttpExchangeCookieAccess |
| implements CookieAccess<HttpExchange> { |
| public String getCookie(HttpExchange ex, String cookieName) { |
| String cookies = ex.getRequestHeaders().getFirst("Cookie"); |
| if (cookies == null) { |
| return null; |
| } |
| |
| String[] cookieArray = cookies.split(";"); |
| for (String cookie : cookieArray) { |
| String[] keyValue = cookie.split("=", 2); |
| if (keyValue.length == 1) { |
| continue; |
| } |
| keyValue[0] = keyValue[0].trim(); |
| if (cookieName.equals(keyValue[0])) { |
| return keyValue[1]; |
| } |
| } |
| |
| return null; |
| } |
| |
| public void setCookie(HttpExchange ex, String cookieName, |
| String cookieValue) { |
| ex.getResponseHeaders().set("Set-Cookie", cookieName + "=" + cookieValue |
| + "; Path=/"); |
| } |
| } |
| } |