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 : }
|