Avoid creating HTTP Sessions for Git-over-HTTP

The Change-Id: Iffcd0fbd7 has involuntarily triggered the
creation of a new HTTP Session for every invocation a Git-over-HTTP
request.

All came from the mistake of tracing the HTTP session instead
of the Gerrit session in the audit record.
The HTTP Servlet API specs say that any attempt to access
the current session of an incoming request would result
in the creation of a brand-new session.

The session involuntarily created also had an expiry time
equal to zero, which prevented the session housekeeper
to reclaim them later on, even though they were unused.

The consequence of creating an empty session for every
Git-over-HTTP request isn't immediately tangible, because
the session is empty and doesn't occupy a significant
amount of memory. However, longer-term, the in-memory
hashtable that records all the sessions, each one using
750 bytes on average, will be causing the overload
of the JVM heap and the crash of the process because of
lack of available memory.

Use the correct Gerrit session-id, retrieving
from the Provider<WebSession> the proper session, if active
and logged in, and make sure in tests that no HTTP sessions
are created as a result of a Git-over-http request.

Bug: Issue 13858
Change-Id: I8c086fed54b196c3f46fa88ac78c127784524d30
This commit is contained in:
Luca Milanesio
2021-01-18 21:45:47 +01:00
parent 58f8d6e318
commit a5938b1c60
5 changed files with 97 additions and 16 deletions

View File

@@ -602,6 +602,10 @@ public class GerritServer implements AutoCloseable {
return testInjector; return testInjector;
} }
public Injector getHttpdInjector() {
return daemon.getHttpdInjector();
}
Description getDescription() { Description getDescription() {
return desc; return desc;
} }

View File

@@ -360,6 +360,7 @@ public class GitOverHttpServlet extends GitServlet {
private final Metrics metrics; private final Metrics metrics;
private final PluginSetContext<RequestListener> requestListeners; private final PluginSetContext<RequestListener> requestListeners;
private final UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook; private final UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook;
private final Provider<WebSession> sessionProvider;
@Inject @Inject
UploadFilter( UploadFilter(
@@ -369,7 +370,8 @@ public class GitOverHttpServlet extends GitServlet {
GroupAuditService groupAuditService, GroupAuditService groupAuditService,
Metrics metrics, Metrics metrics,
PluginSetContext<RequestListener> requestListeners, PluginSetContext<RequestListener> requestListeners,
UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook) { UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook,
Provider<WebSession> sessionProvider) {
this.uploadValidatorsFactory = uploadValidatorsFactory; this.uploadValidatorsFactory = uploadValidatorsFactory;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
this.userProvider = userProvider; this.userProvider = userProvider;
@@ -377,6 +379,7 @@ public class GitOverHttpServlet extends GitServlet {
this.metrics = metrics; this.metrics = metrics;
this.requestListeners = requestListeners; this.requestListeners = requestListeners;
this.usersSelfAdvertiseRefsHook = usersSelfAdvertiseRefsHook; this.usersSelfAdvertiseRefsHook = usersSelfAdvertiseRefsHook;
this.sessionProvider = sessionProvider;
} }
@Override @Override
@@ -392,7 +395,7 @@ public class GitOverHttpServlet extends GitServlet {
HttpServletResponseWithStatusWrapper responseWrapper = HttpServletResponseWithStatusWrapper responseWrapper =
new HttpServletResponseWithStatusWrapper((HttpServletResponse) response); new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletRequest httpRequest = (HttpServletRequest) request;
String sessionId = httpRequest.getSession().getId(); String sessionId = getSessionIdOrNull(sessionProvider);
try (TraceContext traceContext = TraceContext.open()) { try (TraceContext traceContext = TraceContext.open()) {
RequestInfo requestInfo = RequestInfo requestInfo =
@@ -495,6 +498,7 @@ public class GitOverHttpServlet extends GitServlet {
private final Provider<CurrentUser> userProvider; private final Provider<CurrentUser> userProvider;
private final GroupAuditService groupAuditService; private final GroupAuditService groupAuditService;
private final Metrics metrics; private final Metrics metrics;
private final Provider<WebSession> sessionProvider;
@Inject @Inject
ReceiveFilter( ReceiveFilter(
@@ -502,12 +506,14 @@ public class GitOverHttpServlet extends GitServlet {
PermissionBackend permissionBackend, PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider, Provider<CurrentUser> userProvider,
GroupAuditService groupAuditService, GroupAuditService groupAuditService,
Metrics metrics) { Metrics metrics,
Provider<WebSession> sessionProvider) {
this.cache = cache; this.cache = cache;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
this.userProvider = userProvider; this.userProvider = userProvider;
this.groupAuditService = groupAuditService; this.groupAuditService = groupAuditService;
this.metrics = metrics; this.metrics = metrics;
this.sessionProvider = sessionProvider;
} }
@Override @Override
@@ -547,7 +553,7 @@ public class GitOverHttpServlet extends GitServlet {
} finally { } finally {
groupAuditService.dispatch( groupAuditService.dispatch(
new HttpAuditEvent( new HttpAuditEvent(
httpRequest.getSession().getId(), getSessionIdOrNull(sessionProvider),
userProvider.get(), userProvider.get(),
extractWhat(httpRequest), extractWhat(httpRequest),
TimeUtil.nowMs(), TimeUtil.nowMs(),
@@ -603,4 +609,12 @@ public class GitOverHttpServlet extends GitServlet {
@Override @Override
public void destroy() {} public void destroy() {}
} }
private static String getSessionIdOrNull(Provider<WebSession> sessionProvider) {
WebSession session = sessionProvider.get();
if (session.isSignedIn()) {
return session.getSessionId();
}
return null;
}
} }

View File

@@ -238,6 +238,11 @@ public class Daemon extends SiteProgram {
this.replica = replica; this.replica = replica;
} }
@VisibleForTesting
public Injector getHttpdInjector() {
return httpdInjector;
}
@Override @Override
public int run() throws Exception { public int run() throws Exception {
if (stopOnly) { if (stopOnly) {

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.pgm.http.jetty;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.extensions.events.LifecycleListener;
@@ -42,8 +43,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ConnectionStatistics; import org.eclipse.jetty.io.ConnectionStatistics;
import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.jmx.MBeanContainer;
@@ -200,6 +204,8 @@ public class JettyServer {
private final Metrics metrics; private final Metrics metrics;
private boolean reverseProxy; private boolean reverseProxy;
private ConnectionStatistics connStats; private ConnectionStatistics connStats;
private final SessionHandler sessionHandler;
private final AtomicLong sessionsCounter;
@Inject @Inject
JettyServer( JettyServer(
@@ -218,8 +224,27 @@ public class JettyServer {
connector.addBean(connStats); connector.addBean(connStats);
} }
metrics = new Metrics(pool, connStats); metrics = new Metrics(pool, connStats);
sessionHandler = new SessionHandler();
sessionsCounter = new AtomicLong();
Handler app = makeContext(env, cfg); /* Code used for testing purposes for making assertions
* on the number of active HTTP sessions.
*/
sessionHandler.addEventListener(
new HttpSessionListener() {
@Override
public void sessionDestroyed(HttpSessionEvent se) {
sessionsCounter.decrementAndGet();
}
@Override
public void sessionCreated(HttpSessionEvent se) {
sessionsCounter.incrementAndGet();
}
});
Handler app = makeContext(env, cfg, sessionHandler);
if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) { if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) {
RequestLogHandler handler = new RequestLogHandler(); RequestLogHandler handler = new RequestLogHandler();
handler.setRequestLog(httpLogFactory.get()); handler.setRequestLog(httpLogFactory.get());
@@ -246,6 +271,11 @@ public class JettyServer {
httpd.setStopAtShutdown(false); httpd.setStopAtShutdown(false);
} }
@VisibleForTesting
public long numActiveSessions() {
return sessionsCounter.longValue();
}
Metrics getMetrics() { Metrics getMetrics() {
return metrics; return metrics;
} }
@@ -445,7 +475,7 @@ public class JettyServer {
return pool; return pool;
} }
private Handler makeContext(JettyEnv env, Config cfg) { private Handler makeContext(JettyEnv env, Config cfg, SessionHandler sessionHandler) {
final Set<String> paths = new HashSet<>(); final Set<String> paths = new HashSet<>();
for (URI u : listenURLs(cfg)) { for (URI u : listenURLs(cfg)) {
String p = u.getPath(); String p = u.getPath();
@@ -460,7 +490,7 @@ public class JettyServer {
final List<ContextHandler> all = new ArrayList<>(); final List<ContextHandler> all = new ArrayList<>();
for (String path : paths) { for (String path : paths) {
all.add(makeContext(path, env, cfg)); all.add(makeContext(path, env, cfg, sessionHandler));
} }
if (all.size() == 1) { if (all.size() == 1) {
@@ -478,13 +508,14 @@ public class JettyServer {
return r; return r;
} }
private ContextHandler makeContext(final String contextPath, JettyEnv env, Config cfg) { private ContextHandler makeContext(
final String contextPath, JettyEnv env, Config cfg, SessionHandler sessionHandler) {
final ServletContextHandler app = new ServletContextHandler(); final ServletContextHandler app = new ServletContextHandler();
// This enables the use of sessions in Jetty, feature available // This enables the use of sessions in Jetty, feature available
// for Gerrit plug-ins to enable user-level sessions. // for Gerrit plug-ins to enable user-level sessions.
// //
app.setSessionHandler(new SessionHandler()); app.setSessionHandler(sessionHandler);
app.setErrorHandler(new HiddenErrorHandler()); app.setErrorHandler(new HiddenErrorHandler());
// This is the path we are accessed by clients within our domain. // This is the path we are accessed by clients within our domain.

View File

@@ -19,11 +19,15 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.FakeGroupAuditService; import com.google.gerrit.acceptance.FakeGroupAuditService;
import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.entities.Account;
import com.google.gerrit.pgm.http.jetty.JettyServer;
import com.google.gerrit.server.audit.HttpAuditEvent; import com.google.gerrit.server.audit.HttpAuditEvent;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Optional;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RefSpec;
@@ -33,9 +37,11 @@ import org.junit.Test;
public class AbstractGitOverHttpServlet extends AbstractPushForReview { public class AbstractGitOverHttpServlet extends AbstractPushForReview {
@Inject protected FakeGroupAuditService auditService; @Inject protected FakeGroupAuditService auditService;
private JettyServer jettyServer;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
jettyServer = server.getHttpdInjector().getInstance(JettyServer.class);
CredentialsProvider.setDefault( CredentialsProvider.setDefault(
new UsernamePasswordCredentialsProvider(admin.username(), admin.httpPassword())); new UsernamePasswordCredentialsProvider(admin.username(), admin.httpPassword()));
selectProtocol(AbstractPushForReview.Protocol.HTTP); selectProtocol(AbstractPushForReview.Protocol.HTTP);
@@ -67,33 +73,54 @@ public class AbstractGitOverHttpServlet extends AbstractPushForReview {
assertThat(receivePack.what).endsWith("/git-receive-pack"); assertThat(receivePack.what).endsWith("/git-receive-pack");
assertThat(receivePack.params).isEmpty(); assertThat(receivePack.params).isEmpty();
assertThat(receivePack.httpStatus).isEqualTo(HttpServletResponse.SC_OK); assertThat(receivePack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
assertThat(jettyServer.numActiveSessions()).isEqualTo(0);
} }
@Test @Test
@Sandboxed public void anonymousUploadPackAuditEventLog() throws Exception {
public void uploadPackAuditEventLog() throws Exception { uploadPackAuditEventLog(Constants.DEFAULT_REMOTE_NAME, Optional.empty());
}
@Test
public void authenticatedUploadPackAuditEventLog() throws Exception {
String remote = "authenticated";
Config cfg = testRepo.git().getRepository().getConfig();
String uri = admin.getHttpUrl(server) + "/a/" + project.get();
cfg.setString("remote", remote, "url", uri);
cfg.setString("remote", remote, "fetch", "+refs/heads/*:refs/remotes/origin/*");
uploadPackAuditEventLog(remote, Optional.of(admin.id()));
}
private void uploadPackAuditEventLog(String remote, Optional<Account.Id> accountId)
throws Exception {
auditService.drainHttpAuditEvents(); auditService.drainHttpAuditEvents();
// testRepo is already a clone. Make a server-side change so we have something to fetch. // testRepo is already a clone. Make a server-side change so we have something to fetch.
try (Repository repo = repoManager.openRepository(project); try (Repository repo = repoManager.openRepository(project);
TestRepository<?> testRepo = new TestRepository<>(repo)) { TestRepository<?> testRepo = new TestRepository<>(repo)) {
testRepo.branch("master").commit().create(); testRepo.branch("master").commit().create();
} }
testRepo.git().fetch().call(); testRepo.git().fetch().setRemote(remote).call();
ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents(); ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents();
assertThat(auditEvents).hasSize(2); assertThat(auditEvents).hasSize(2);
HttpAuditEvent lsRemote = auditEvents.get(0); HttpAuditEvent lsRemote = auditEvents.get(0);
// Repo URL doesn't include /a, so fetching doesn't cause authentication. assertThat(lsRemote.who.toString())
assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class); .isEqualTo(
accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS"));
assertThat(lsRemote.what).endsWith("/info/refs?service=git-upload-pack"); assertThat(lsRemote.what).endsWith("/info/refs?service=git-upload-pack");
assertThat(lsRemote.params).containsExactly("service", "git-upload-pack"); assertThat(lsRemote.params).containsExactly("service", "git-upload-pack");
assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK); assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
HttpAuditEvent uploadPack = auditEvents.get(1); HttpAuditEvent uploadPack = auditEvents.get(1);
assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class); assertThat(uploadPack.who.toString())
.isEqualTo(
accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS"));
assertThat(uploadPack.what).endsWith("/git-upload-pack"); assertThat(uploadPack.what).endsWith("/git-upload-pack");
assertThat(uploadPack.params).isEmpty(); assertThat(uploadPack.params).isEmpty();
assertThat(uploadPack.httpStatus).isEqualTo(HttpServletResponse.SC_OK); assertThat(uploadPack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
assertThat(jettyServer.numActiveSessions()).isEqualTo(0);
} }
} }