diff options
author | Luca Milanesio <luca.milanesio@gmail.com> | 2021-01-12 21:47:15 +0000 |
---|---|---|
committer | David Ostrovsky <david@ostrovsky.org> | 2021-01-26 13:29:32 +0100 |
commit | 4b2b2d831b39f57720923d5b0c23f1b6d94be0e9 (patch) | |
tree | f3e655ca41ee8c32a7315611556930c33f72e2e5 | |
parent | b5f92f10f99c7374c57bebde21d26bec12c38b33 (diff) |
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
5 files changed, 94 insertions, 10 deletions
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java index eafffcaf49..fe81a2b7fe 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java @@ -468,6 +468,10 @@ public class GerritServer implements AutoCloseable { return testInjector; } + public Injector getHttpdInjector() { + return daemon.getHttpdInjector(); + } + Description getDescription() { return desc; } diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java index 3d6d16a52e..8eddd897b5 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java @@ -18,9 +18,14 @@ import static com.google.common.truth.Truth.assertThat; import com.google.gerrit.audit.AuditEvent; import com.google.gerrit.audit.AuditService; +import com.google.gerrit.pgm.http.jetty.JettyServer; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.testutil.FakeAuditService; import com.google.inject.AbstractModule; import java.util.Collections; +import java.util.Optional; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; @@ -29,6 +34,7 @@ import org.junit.Test; import org.junit.runner.Description; public class GitOverHttpServletIT extends AbstractPushForReview { + private JettyServer jettyServer; @Override protected void beforeTest(Description description) throws Exception { @@ -44,6 +50,7 @@ public class GitOverHttpServletIT extends AbstractPushForReview { @Before public void beforeEach() throws Exception { + jettyServer = server.getHttpdInjector().getInstance(JettyServer.class); CredentialsProvider.setDefault( new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword)); selectProtocol(AbstractPushForReview.Protocol.HTTP); @@ -67,18 +74,40 @@ public class GitOverHttpServletIT extends AbstractPushForReview { assertThat(e.who.getAccountId()).isEqualTo(admin.id); assertThat(e.what).endsWith("/git-receive-pack"); assertThat(e.params).isEmpty(); + assertThat(jettyServer.numActiveSessions()).isEqualTo(0); } @Test - public void uploadPackAuditEventLog() throws Exception { - testRepo.git().fetch().call(); + public void anonymousUploadPackAuditEventLog() 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.getId())); + } + + private void uploadPackAuditEventLog(String remote, Optional<Account.Id> accountId) + throws Exception { + auditService.clearEvents(); + testRepo.git().fetch().setRemote(remote).call(); assertThat(auditService.auditEvents.size()).isEqualTo(1); AuditEvent e = auditService.auditEvents.get(0); - assertThat(e.who.toString()).isEqualTo("ANONYMOUS"); + assertThat(e.who.toString()) + .isEqualTo( + accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS")); assertThat(e.params.get("service")) .containsExactlyElementsIn(Collections.singletonList("git-upload-pack")); assertThat(e.what).endsWith("service=git-upload-pack"); + assertThat(jettyServer.numActiveSessions()).isEqualTo(0); } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java index 0da2f92cd2..0700bbc6a8 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java @@ -274,6 +274,7 @@ public class GitOverHttpServlet extends GitServlet { private final PermissionBackend permissionBackend; private final Provider<CurrentUser> userProvider; private final AuditService auditService; + private final Provider<WebSession> sessionProvider; @Inject UploadFilter( @@ -281,11 +282,13 @@ public class GitOverHttpServlet extends GitServlet { UploadValidators.Factory uploadValidatorsFactory, PermissionBackend permissionBackend, Provider<CurrentUser> userProvider, + Provider<WebSession> sessionProvider, AuditService auditService) { this.refFilterFactory = refFilterFactory; this.uploadValidatorsFactory = uploadValidatorsFactory; this.permissionBackend = permissionBackend; this.userProvider = userProvider; + this.sessionProvider = sessionProvider; this.auditService = auditService; } @@ -316,7 +319,7 @@ public class GitOverHttpServlet extends GitServlet { HttpServletResponse httpResponse = (HttpServletResponse) response; auditService.dispatch( new HttpAuditEvent( - httpRequest.getSession().getId(), + getSessionIdOrNull(sessionProvider), userProvider.get(), extractWhat(httpRequest), TimeUtil.nowMs(), @@ -382,6 +385,7 @@ public class GitOverHttpServlet extends GitServlet { private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache; private final PermissionBackend permissionBackend; private final Provider<CurrentUser> userProvider; + private final Provider<WebSession> sessionProvider; private final AuditService auditService; @Inject @@ -389,10 +393,12 @@ public class GitOverHttpServlet extends GitServlet { @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache, PermissionBackend permissionBackend, Provider<CurrentUser> userProvider, + Provider<WebSession> sessionProvider, AuditService auditService) { this.cache = cache; this.permissionBackend = permissionBackend; this.userProvider = userProvider; + this.sessionProvider = sessionProvider; this.auditService = auditService; } @@ -426,7 +432,7 @@ public class GitOverHttpServlet extends GitServlet { HttpServletResponse httpResponse = (HttpServletResponse) response; auditService.dispatch( new HttpAuditEvent( - httpRequest.getSession().getId(), + getSessionIdOrNull(sessionProvider), userProvider.get(), extractWhat(httpRequest), TimeUtil.nowMs(), @@ -483,4 +489,12 @@ public class GitOverHttpServlet extends GitServlet { @Override public void destroy() {} } + + private static String getSessionIdOrNull(Provider<WebSession> sessionProvider) { + WebSession session = sessionProvider.get(); + if (session.isSignedIn()) { + return session.getSessionId(); + } + return null; + } } diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index 4dd54ad922..c733be66cf 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java @@ -223,6 +223,11 @@ public class Daemon extends SiteProgram { httpd = enable; } + @VisibleForTesting + public Injector getHttpdInjector() { + return httpdInjector; + } + @Override public int run() throws Exception { if (stopOnly) { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java index 25a28a4cfd..f7b81fbee7 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java @@ -17,6 +17,7 @@ package com.google.gerrit.pgm.http.jetty; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.events.LifecycleListener; @@ -40,8 +41,11 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import javax.servlet.DispatcherType; import javax.servlet.Filter; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.server.Connector; @@ -118,6 +122,8 @@ public class JettyServer { private final SitePaths site; private final Server httpd; + private final SessionHandler sessionHandler; + private final AtomicLong sessionsCounter; private boolean reverseProxy; @@ -133,7 +139,27 @@ public class JettyServer { httpd = new Server(threadPool(cfg, threadSettingsConfig)); httpd.setConnectors(listen(httpd, cfg)); - Handler app = makeContext(env, cfg); + sessionHandler = new SessionHandler(); + sessionsCounter = new AtomicLong(); + + /* 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)) { RequestLogHandler handler = new RequestLogHandler(); handler.setRequestLog(httpLogFactory.get()); @@ -160,6 +186,11 @@ public class JettyServer { httpd.setStopAtShutdown(false); } + @VisibleForTesting + public long numActiveSessions() { + return sessionsCounter.longValue(); + } + private Connector[] listen(Server server, Config cfg) { // OpenID and certain web-based single-sign-on products can cause // some very long headers, especially in the Referer header. We @@ -359,7 +390,7 @@ public class JettyServer { return pool; } - private Handler makeContext(JettyEnv env, Config cfg) { + private Handler makeContext(JettyEnv env, Config cfg, SessionHandler sessionHandler) { final Set<String> paths = new HashSet<>(); for (URI u : listenURLs(cfg)) { String p = u.getPath(); @@ -374,7 +405,7 @@ public class JettyServer { final List<ContextHandler> all = new ArrayList<>(); for (String path : paths) { - all.add(makeContext(path, env, cfg)); + all.add(makeContext(path, env, cfg, sessionHandler)); } if (all.size() == 1) { @@ -392,13 +423,14 @@ public class JettyServer { 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(); // This enables the use of sessions in Jetty, feature available // for Gerrit plug-ins to enable user-level sessions. // - app.setSessionHandler(new SessionHandler()); + app.setSessionHandler(sessionHandler); app.setErrorHandler(new HiddenErrorHandler()); // This is the path we are accessed by clients within our domain. |