blob: b502a7c4c9af19c7f96d15e2a691c9e85705bdd0 [file] [log] [blame]
// 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=/");
}
}
}