| // 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.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; |
| |
| 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 = ExternalId.Key.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); |
| } |
| } |
| } |
| } |