blob: 830d78db25af36cf2e7d35bb169f7ff1525a8e97 [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 com.google.enterprise.adaptor;
import com.sun.net.httpserver.HttpExchange;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.WeakHashMap;
import javax.xml.bind.DatatypeConverter;
/**
* 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 final TimeProvider timeProvider;
private final ClientStore<E> clientStore;
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 clientStore storage for communicating session id with client
* @param sessionLifetime lifetime of sessions, in milliseconds
* @param cleanupFrequency maximum frequency to check for expired sessions, in
* milliseconds
*/
public SessionManager(ClientStore<E> clientStore, long sessionLifetime,
long cleanupFrequency) {
this(new SystemTimeProvider(), clientStore, sessionLifetime,
cleanupFrequency);
}
protected SessionManager(TimeProvider timeProvider,
ClientStore<E> clientStore, long sessionLifetime,
long cleanupFrequency) {
this.timeProvider = timeProvider;
this.clientStore = clientStore;
this.sessionLifetime = sessionLifetime;
this.cleanupFrequency = cleanupFrequency;
}
public Session getSession(E clientState) {
return getSession(clientState, true);
}
public Session getSession(E clientState, boolean create) {
String value = clientStore.retrieve(clientState);
if (value == null) {
// No pre-existing session found.
return create ? createSession(clientState) : null;
}
synchronized (this) {
// Check for expiration now.
cleanupIfExpiredSession(value);
Session session = sessions.get(value);
if (session != null) {
updateLastAccess(value);
return session;
}
// Could not find session specified. Assume it expired.
return create ? createSession(clientState) : null;
}
}
protected synchronized Session createSession(E clientState) {
cleanupExpiredSessions();
Session session = new HashMapSession();
String id = generateRandomIdentifier();
sessions.put(id, session);
clientStore.store(clientState, 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 lastAccessLimit = currentTime - sessionLifetime;
for (Map.Entry<String, Long> me : lastAccess.entrySet()) {
if (lastAccessLimit > me.getValue()) {
toRemove.add(me.getKey());
}
}
for (String id : toRemove) {
sessions.remove(id);
lastAccess.remove(id);
}
nextCleanup = currentTime + cleanupFrequency;
}
protected synchronized void cleanupIfExpiredSession(String id) {
Long lastAccessForId = lastAccess.get(id);
if (lastAccessForId == null) {
return;
}
long currentTime = timeProvider.currentTimeMillis();
long lastAccessLimit = currentTime - sessionLifetime;
if (lastAccessLimit > lastAccessForId) {
sessions.remove(id);
lastAccess.remove(id);
}
}
/**
* Generate a secure, random, 128-bit, base64-encoded identifier.
*/
synchronized String generateRandomIdentifier() {
byte[] rawId = new byte[16];
random.nextBytes(rawId);
return DatatypeConverter.printBase64Binary(rawId);
}
int getSessionCount() {
return sessions.size();
}
/** A single-value storage per client. */
public static interface ClientStore<E> {
/** Returns the previously-stored value or {@code null}. */
public String retrieve(E clientState);
/** Stores the value for this client. */
public void store(E clientState, String value);
}
public static class HttpExchangeClientStore
implements ClientStore<HttpExchange> {
private final String cookieName;
private final boolean secure;
private Map<HttpExchange, String> exchangeCookieMap
= Collections.synchronizedMap(new WeakHashMap<HttpExchange, String>());
public HttpExchangeClientStore() {
this("sessid");
}
public HttpExchangeClientStore(String cookieName) {
this(cookieName, false);
}
public HttpExchangeClientStore(String cookieName, boolean secure) {
if (cookieName == null) {
throw new NullPointerException();
}
this.cookieName = cookieName;
this.secure = secure;
}
@Override
public String retrieve(HttpExchange ex) {
String value = exchangeCookieMap.get(ex);
if (value != null) {
return value;
}
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;
}
@Override
public void store(HttpExchange ex, String value) {
exchangeCookieMap.put(ex, value);
ex.getResponseHeaders().set("Set-Cookie", cookieName + "=" + value
+ "; Path=/; HttpOnly" + (secure ? "; Secure" : ""));
}
}
}