blob: 87bf3a645119aad651b3fc7d70be2d9815ac5380 [file] [log] [blame]
// Copyright (C) 2009 The Android Open Source Project
//
// 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.gerrit.httpd;
import static com.google.gerrit.httpd.CacheBasedWebSession.MAX_AGE_MINUTES;
import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
import static com.google.gerrit.server.util.time.TimeUtil.nowMs;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.cache.Cache;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
public class WebSessionManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String CACHE_NAME = "web_sessions";
private final long sessionMaxAgeMillis;
private final SecureRandom prng;
private final Cache<String, Val> self;
@Inject
WebSessionManager(@GerritServerConfig Config cfg, @Assisted Cache<String, Val> cache) {
prng = new SecureRandom();
self = cache;
sessionMaxAgeMillis =
SECONDS.toMillis(
ConfigUtil.getTimeUnit(
cfg,
"cache",
CACHE_NAME,
"maxAge",
SECONDS.convert(MAX_AGE_MINUTES, MINUTES),
SECONDS));
if (sessionMaxAgeMillis < MINUTES.toMillis(5)) {
logger.atWarning().log(
"cache.%s.maxAge is set to %d milliseconds; it should be at least 5 minutes.",
CACHE_NAME, sessionMaxAgeMillis);
}
}
Key createKey(Account.Id who) {
return new Key(newUniqueToken(who));
}
private String newUniqueToken(Account.Id who) {
try {
final int nonceLen = 20;
final ByteArrayOutputStream buf;
final byte[] rnd = new byte[nonceLen];
prng.nextBytes(rnd);
buf = new ByteArrayOutputStream(3 + nonceLen);
writeVarInt32(buf, (int) Val.serialVersionUID);
writeVarInt32(buf, who.get());
writeBytes(buf, rnd);
return CookieBase64.encode(buf.toByteArray());
} catch (IOException e) {
throw new RuntimeException("Cannot produce new account cookie", e);
}
}
Val createVal(Key key, Val val) {
Account.Id who = val.getAccountId();
boolean remember = val.isPersistentCookie();
ExternalId.Key lastLogin = val.getExternalId();
return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
}
Val createVal(
Key key,
Account.Id who,
boolean remember,
ExternalId.Key lastLogin,
String sid,
String auth) {
// Refresh the cookie every hour or when it is half-expired.
// This reduces the odds that the user session will be kicked
// early but also avoids us needing to refresh the cookie on
// every single request.
//
final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
final long minRefresh = MILLISECONDS.convert(1, HOURS);
final long refresh = Math.min(halfAgeRefresh, minRefresh);
final long now = nowMs();
final long refreshCookieAt = now + refresh;
final long expiresAt = now + sessionMaxAgeMillis;
if (sid == null) {
sid = newUniqueToken(who);
}
if (auth == null) {
auth = newUniqueToken(who);
}
Val val = new Val(who, refreshCookieAt, remember, lastLogin, expiresAt, sid, auth);
self.put(key.token, val);
return val;
}
int getCookieAge(Val val) {
if (val.isPersistentCookie()) {
// Client may store the cookie until we would remove it from our
// own cache, after which it will certainly be invalid.
//
return (int) MILLISECONDS.toSeconds(sessionMaxAgeMillis);
}
// Client should not store the cookie, as the user asked for us
// to not remember them long-term. Sending -1 as the age will
// cause the cookie to be only for this "browser session", which
// is usually until the user exits their browser.
//
return -1;
}
Val get(Key key) {
Val val = self.getIfPresent(key.token);
if (val != null && val.expiresAt <= nowMs()) {
self.invalidate(key.token);
return null;
}
return val;
}
void destroy(Key key) {
self.invalidate(key.token);
}
static final class Key {
private transient String token;
Key(String t) {
token = t;
}
String getToken() {
return token;
}
@Override
public int hashCode() {
return token.hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof Key && token.equals(((Key) obj).token);
}
}
public static final class Val implements Serializable {
static final long serialVersionUID = 2L;
@Inject private static transient ExternalIdKeyFactory externalIdKeyFactory;
private transient Account.Id accountId;
private transient long refreshCookieAt;
private transient boolean persistentCookie;
private transient ExternalId.Key externalId;
private transient long expiresAt;
private transient String sessionId;
private transient String auth;
Val(
Account.Id accountId,
long refreshCookieAt,
boolean persistentCookie,
ExternalId.Key externalId,
long expiresAt,
String sessionId,
String auth) {
this.accountId = accountId;
this.refreshCookieAt = refreshCookieAt;
this.persistentCookie = persistentCookie;
this.externalId = externalId;
this.expiresAt = expiresAt;
this.sessionId = sessionId;
this.auth = auth;
}
public long getExpiresAt() {
return expiresAt;
}
/**
* Parse an Account.Id.
*
* <p>This is public so that plugins that implement a web session, can also implement a way to
* clear per user sessions.
*
* @return account ID.
*/
public Account.Id getAccountId() {
return accountId;
}
ExternalId.Key getExternalId() {
return externalId;
}
String getSessionId() {
return sessionId;
}
String getAuth() {
return auth;
}
boolean needsCookieRefresh() {
return refreshCookieAt <= nowMs();
}
boolean isPersistentCookie() {
return persistentCookie;
}
private void writeObject(ObjectOutputStream out) throws IOException {
writeVarInt32(out, 1);
writeVarInt32(out, accountId.get());
writeVarInt32(out, 2);
writeFixInt64(out, refreshCookieAt);
writeVarInt32(out, 3);
writeVarInt32(out, persistentCookie ? 1 : 0);
if (externalId != null) {
writeVarInt32(out, 4);
writeString(out, externalId.toString());
}
if (sessionId != null) {
writeVarInt32(out, 5);
writeString(out, sessionId);
}
writeVarInt32(out, 6);
writeFixInt64(out, expiresAt);
if (auth != null) {
writeVarInt32(out, 7);
writeString(out, auth);
}
writeVarInt32(out, 0);
}
private void readObject(ObjectInputStream in) throws IOException {
PARSE:
for (; ; ) {
final int tag = readVarInt32(in);
switch (tag) {
case 0:
break PARSE;
case 1:
accountId = Account.id(readVarInt32(in));
continue;
case 2:
refreshCookieAt = readFixInt64(in);
continue;
case 3:
persistentCookie = readVarInt32(in) != 0;
continue;
case 4:
externalId = externalIdKeyFactory.parse(readString(in));
continue;
case 5:
sessionId = readString(in);
continue;
case 6:
expiresAt = readFixInt64(in);
continue;
case 7:
auth = readString(in);
continue;
default:
throw new IOException("Unknown tag found in object: " + tag);
}
}
if (expiresAt == 0) {
expiresAt = refreshCookieAt + TimeUnit.HOURS.toMillis(2);
}
}
}
}