LCOV - code coverage report
Current view: top level - httpd - WebSessionManager.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 55 111 49.5 %
Date: 2022-11-19 15:00:39 Functions: 15 23 65.2 %

          Line data    Source code
       1             : // Copyright (C) 2009 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.httpd;
      16             : 
      17             : import static com.google.gerrit.httpd.CacheBasedWebSession.MAX_AGE_MINUTES;
      18             : import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
      19             : import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
      20             : import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
      21             : import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
      22             : import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
      23             : import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
      24             : import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
      25             : import static com.google.gerrit.server.util.time.TimeUtil.nowMs;
      26             : import static java.util.concurrent.TimeUnit.HOURS;
      27             : import static java.util.concurrent.TimeUnit.MILLISECONDS;
      28             : import static java.util.concurrent.TimeUnit.MINUTES;
      29             : import static java.util.concurrent.TimeUnit.SECONDS;
      30             : 
      31             : import com.google.common.cache.Cache;
      32             : import com.google.common.flogger.FluentLogger;
      33             : import com.google.gerrit.common.Nullable;
      34             : import com.google.gerrit.entities.Account;
      35             : import com.google.gerrit.server.account.externalids.ExternalId;
      36             : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
      37             : import com.google.gerrit.server.config.ConfigUtil;
      38             : import com.google.gerrit.server.config.GerritServerConfig;
      39             : import com.google.inject.Inject;
      40             : import com.google.inject.assistedinject.Assisted;
      41             : import java.io.ByteArrayOutputStream;
      42             : import java.io.IOException;
      43             : import java.io.ObjectInputStream;
      44             : import java.io.ObjectOutputStream;
      45             : import java.io.Serializable;
      46             : import java.security.SecureRandom;
      47             : import java.util.concurrent.TimeUnit;
      48             : import org.eclipse.jgit.lib.Config;
      49             : 
      50             : public class WebSessionManager {
      51          39 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      52             :   public static final String CACHE_NAME = "web_sessions";
      53             : 
      54             :   private final long sessionMaxAgeMillis;
      55             :   private final SecureRandom prng;
      56             :   private final Cache<String, Val> self;
      57             : 
      58             :   @Inject
      59          38 :   WebSessionManager(@GerritServerConfig Config cfg, @Assisted Cache<String, Val> cache) {
      60          38 :     prng = new SecureRandom();
      61          38 :     self = cache;
      62             : 
      63          38 :     sessionMaxAgeMillis =
      64          38 :         SECONDS.toMillis(
      65          38 :             ConfigUtil.getTimeUnit(
      66             :                 cfg,
      67             :                 "cache",
      68             :                 CACHE_NAME,
      69             :                 "maxAge",
      70          38 :                 SECONDS.convert(MAX_AGE_MINUTES, MINUTES),
      71             :                 SECONDS));
      72          38 :     if (sessionMaxAgeMillis < MINUTES.toMillis(5)) {
      73           0 :       logger.atWarning().log(
      74             :           "cache.%s.maxAge is set to %d milliseconds; it should be at least 5 minutes.",
      75             :           CACHE_NAME, sessionMaxAgeMillis);
      76             :     }
      77          38 :   }
      78             : 
      79             :   Key createKey(Account.Id who) {
      80           1 :     return new Key(newUniqueToken(who));
      81             :   }
      82             : 
      83             :   private String newUniqueToken(Account.Id who) {
      84             :     try {
      85           1 :       final int nonceLen = 20;
      86             :       final ByteArrayOutputStream buf;
      87           1 :       final byte[] rnd = new byte[nonceLen];
      88           1 :       prng.nextBytes(rnd);
      89             : 
      90           1 :       buf = new ByteArrayOutputStream(3 + nonceLen);
      91           1 :       writeVarInt32(buf, (int) Val.serialVersionUID);
      92           1 :       writeVarInt32(buf, who.get());
      93           1 :       writeBytes(buf, rnd);
      94             : 
      95           1 :       return CookieBase64.encode(buf.toByteArray());
      96           0 :     } catch (IOException e) {
      97           0 :       throw new RuntimeException("Cannot produce new account cookie", e);
      98             :     }
      99             :   }
     100             : 
     101             :   Val createVal(Key key, Val val) {
     102           0 :     Account.Id who = val.getAccountId();
     103           0 :     boolean remember = val.isPersistentCookie();
     104           0 :     ExternalId.Key lastLogin = val.getExternalId();
     105           0 :     return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
     106             :   }
     107             : 
     108             :   Val createVal(
     109             :       Key key,
     110             :       Account.Id who,
     111             :       boolean remember,
     112             :       ExternalId.Key lastLogin,
     113             :       String sid,
     114             :       String auth) {
     115             :     // Refresh the cookie every hour or when it is half-expired.
     116             :     // This reduces the odds that the user session will be kicked
     117             :     // early but also avoids us needing to refresh the cookie on
     118             :     // every single request.
     119             :     //
     120           1 :     final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
     121           1 :     final long minRefresh = MILLISECONDS.convert(1, HOURS);
     122           1 :     final long refresh = Math.min(halfAgeRefresh, minRefresh);
     123           1 :     final long now = nowMs();
     124           1 :     final long refreshCookieAt = now + refresh;
     125           1 :     final long expiresAt = now + sessionMaxAgeMillis;
     126           1 :     if (sid == null) {
     127           1 :       sid = newUniqueToken(who);
     128             :     }
     129           1 :     if (auth == null) {
     130           1 :       auth = newUniqueToken(who);
     131             :     }
     132             : 
     133           1 :     Val val = new Val(who, refreshCookieAt, remember, lastLogin, expiresAt, sid, auth);
     134           1 :     self.put(key.token, val);
     135           1 :     return val;
     136             :   }
     137             : 
     138             :   int getCookieAge(Val val) {
     139           1 :     if (val.isPersistentCookie()) {
     140             :       // Client may store the cookie until we would remove it from our
     141             :       // own cache, after which it will certainly be invalid.
     142             :       //
     143           0 :       return (int) MILLISECONDS.toSeconds(sessionMaxAgeMillis);
     144             :     }
     145             :     // Client should not store the cookie, as the user asked for us
     146             :     // to not remember them long-term. Sending -1 as the age will
     147             :     // cause the cookie to be only for this "browser session", which
     148             :     // is usually until the user exits their browser.
     149             :     //
     150           1 :     return -1;
     151             :   }
     152             : 
     153             :   @Nullable
     154             :   Val get(Key key) {
     155           1 :     Val val = self.getIfPresent(key.token);
     156           1 :     if (val != null && val.expiresAt <= nowMs()) {
     157           0 :       self.invalidate(key.token);
     158           0 :       return null;
     159             :     }
     160           1 :     return val;
     161             :   }
     162             : 
     163             :   void destroy(Key key) {
     164           0 :     self.invalidate(key.token);
     165           0 :   }
     166             : 
     167             :   static final class Key {
     168             :     private transient String token;
     169             : 
     170          38 :     Key(String t) {
     171          38 :       token = t;
     172          38 :     }
     173             : 
     174             :     String getToken() {
     175           2 :       return token;
     176             :     }
     177             : 
     178             :     @Override
     179             :     public int hashCode() {
     180           0 :       return token.hashCode();
     181             :     }
     182             : 
     183             :     @Override
     184             :     public boolean equals(Object obj) {
     185           0 :       return obj instanceof Key && token.equals(((Key) obj).token);
     186             :     }
     187             :   }
     188             : 
     189             :   public static final class Val implements Serializable {
     190             :     static final long serialVersionUID = 2L;
     191             : 
     192             :     @Inject private static transient ExternalIdKeyFactory externalIdKeyFactory;
     193             : 
     194             :     private transient Account.Id accountId;
     195             :     private transient long refreshCookieAt;
     196             :     private transient boolean persistentCookie;
     197             :     private transient ExternalId.Key externalId;
     198             :     private transient long expiresAt;
     199             :     private transient String sessionId;
     200             :     private transient String auth;
     201             : 
     202             :     Val(
     203             :         Account.Id accountId,
     204             :         long refreshCookieAt,
     205             :         boolean persistentCookie,
     206             :         ExternalId.Key externalId,
     207             :         long expiresAt,
     208             :         String sessionId,
     209          38 :         String auth) {
     210          38 :       this.accountId = accountId;
     211          38 :       this.refreshCookieAt = refreshCookieAt;
     212          38 :       this.persistentCookie = persistentCookie;
     213          38 :       this.externalId = externalId;
     214          38 :       this.expiresAt = expiresAt;
     215          38 :       this.sessionId = sessionId;
     216          38 :       this.auth = auth;
     217          38 :     }
     218             : 
     219             :     public long getExpiresAt() {
     220           0 :       return expiresAt;
     221             :     }
     222             : 
     223             :     /**
     224             :      * Parse an Account.Id.
     225             :      *
     226             :      * <p>This is public so that plugins that implement a web session, can also implement a way to
     227             :      * clear per user sessions.
     228             :      *
     229             :      * @return account ID.
     230             :      */
     231             :     public Account.Id getAccountId() {
     232           2 :       return accountId;
     233             :     }
     234             : 
     235             :     ExternalId.Key getExternalId() {
     236           2 :       return externalId;
     237             :     }
     238             : 
     239             :     String getSessionId() {
     240          36 :       return sessionId;
     241             :     }
     242             : 
     243             :     String getAuth() {
     244           0 :       return auth;
     245             :     }
     246             : 
     247             :     boolean needsCookieRefresh() {
     248           1 :       return refreshCookieAt <= nowMs();
     249             :     }
     250             : 
     251             :     boolean isPersistentCookie() {
     252           1 :       return persistentCookie;
     253             :     }
     254             : 
     255             :     private void writeObject(ObjectOutputStream out) throws IOException {
     256           0 :       writeVarInt32(out, 1);
     257           0 :       writeVarInt32(out, accountId.get());
     258             : 
     259           0 :       writeVarInt32(out, 2);
     260           0 :       writeFixInt64(out, refreshCookieAt);
     261             : 
     262           0 :       writeVarInt32(out, 3);
     263           0 :       writeVarInt32(out, persistentCookie ? 1 : 0);
     264             : 
     265           0 :       if (externalId != null) {
     266           0 :         writeVarInt32(out, 4);
     267           0 :         writeString(out, externalId.toString());
     268             :       }
     269             : 
     270           0 :       if (sessionId != null) {
     271           0 :         writeVarInt32(out, 5);
     272           0 :         writeString(out, sessionId);
     273             :       }
     274             : 
     275           0 :       writeVarInt32(out, 6);
     276           0 :       writeFixInt64(out, expiresAt);
     277             : 
     278           0 :       if (auth != null) {
     279           0 :         writeVarInt32(out, 7);
     280           0 :         writeString(out, auth);
     281             :       }
     282             : 
     283           0 :       writeVarInt32(out, 0);
     284           0 :     }
     285             : 
     286             :     private void readObject(ObjectInputStream in) throws IOException {
     287             :       PARSE:
     288             :       for (; ; ) {
     289           0 :         final int tag = readVarInt32(in);
     290           0 :         switch (tag) {
     291             :           case 0:
     292           0 :             break PARSE;
     293             :           case 1:
     294           0 :             accountId = Account.id(readVarInt32(in));
     295           0 :             continue;
     296             :           case 2:
     297           0 :             refreshCookieAt = readFixInt64(in);
     298           0 :             continue;
     299             :           case 3:
     300           0 :             persistentCookie = readVarInt32(in) != 0;
     301           0 :             continue;
     302             :           case 4:
     303           0 :             externalId = externalIdKeyFactory.parse(readString(in));
     304           0 :             continue;
     305             :           case 5:
     306           0 :             sessionId = readString(in);
     307           0 :             continue;
     308             :           case 6:
     309           0 :             expiresAt = readFixInt64(in);
     310           0 :             continue;
     311             :           case 7:
     312           0 :             auth = readString(in);
     313           0 :             continue;
     314             :           default:
     315           0 :             throw new IOException("Unknown tag found in object: " + tag);
     316             :         }
     317             :       }
     318           0 :       if (expiresAt == 0) {
     319           0 :         expiresAt = refreshCookieAt + TimeUnit.HOURS.toMillis(2);
     320             :       }
     321           0 :     }
     322             :   }
     323             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750