Skip to content

Commit

Permalink
Avoid creating HTTP Sessions for Git-over-HTTP
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lucamilanesio committed Jan 26, 2021
1 parent 74ef125 commit e3f479c
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 12 deletions.
4 changes: 4 additions & 0 deletions java/com/google/gerrit/acceptance/GerritServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,10 @@ public Injector getTestInjector() {
return testInjector;
}

public Injector getHttpdInjector() {
return daemon.getHttpdInjector();
}

Description getDescription() {
return desc;
}
Expand Down
18 changes: 16 additions & 2 deletions java/com/google/gerrit/httpd/GitOverHttpServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,20 @@ static class UploadFilter implements Filter {
private final Provider<CurrentUser> userProvider;
private final GroupAuditService groupAuditService;
private final Metrics metrics;
private final Provider<WebSession> sessionProvider;

@Inject
UploadFilter(
UploadValidators.Factory uploadValidatorsFactory,
PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider,
Provider<WebSession> sessionProvider,
GroupAuditService groupAuditService,
Metrics metrics) {
this.uploadValidatorsFactory = uploadValidatorsFactory;
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
this.sessionProvider = sessionProvider;
this.groupAuditService = groupAuditService;
this.metrics = metrics;
}
Expand All @@ -367,7 +370,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
HttpServletResponseWithStatusWrapper responseWrapper =
new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
HttpServletRequest httpRequest = (HttpServletRequest) request;
String sessionId = httpRequest.getSession().getId();
String sessionId = getSessionIdOrNull(sessionProvider);

try {
try {
Expand Down Expand Up @@ -457,17 +460,20 @@ static class ReceiveFilter implements Filter {
private final Provider<CurrentUser> userProvider;
private final GroupAuditService groupAuditService;
private final Metrics metrics;
private final Provider<WebSession> sessionProvider;

@Inject
ReceiveFilter(
@Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider,
Provider<WebSession> sessionProvider,
GroupAuditService groupAuditService,
Metrics metrics) {
this.cache = cache;
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
this.sessionProvider = sessionProvider;
this.groupAuditService = groupAuditService;
this.metrics = metrics;
}
Expand Down Expand Up @@ -509,7 +515,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
} finally {
groupAuditService.dispatch(
new HttpAuditEvent(
httpRequest.getSession().getId(),
getSessionIdOrNull(sessionProvider),
userProvider.get(),
extractWhat(httpRequest),
TimeUtil.nowMs(),
Expand Down Expand Up @@ -565,4 +571,12 @@ public void init(FilterConfig arg0) {}
@Override
public void destroy() {}
}

private static String getSessionIdOrNull(Provider<WebSession> sessionProvider) {
WebSession session = sessionProvider.get();
if (session.isSignedIn()) {
return session.getSessionId();
}
return null;
}
}
5 changes: 5 additions & 0 deletions java/com/google/gerrit/pgm/Daemon.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ public void setEnableHttpd(boolean enable) {
httpd = enable;
}

@VisibleForTesting
public Injector getHttpdInjector() {
return httpdInjector;
}

public void setSlave(boolean slave) {
this.slave = slave;
}
Expand Down
41 changes: 36 additions & 5 deletions java/com/google/gerrit/pgm/http/jetty/JettyServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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;
Expand All @@ -40,8 +41,11 @@
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.io.ConnectionStatistics;
import org.eclipse.jetty.jmx.MBeanContainer;
Expand Down Expand Up @@ -198,6 +202,8 @@ public long getSentBytes() {
private final Metrics metrics;
private boolean reverseProxy;
private ConnectionStatistics connStats;
private final SessionHandler sessionHandler;
private final AtomicLong sessionsCounter;

@Inject
JettyServer(
Expand All @@ -216,8 +222,27 @@ public long getSentBytes() {
connector.addBean(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)) {
RequestLogHandler handler = new RequestLogHandler();
handler.setRequestLog(httpLogFactory.get());
Expand All @@ -244,6 +269,11 @@ public long getSentBytes() {
httpd.setStopAtShutdown(false);
}

@VisibleForTesting
public long numActiveSessions() {
return sessionsCounter.longValue();
}

Metrics getMetrics() {
return metrics;
}
Expand Down Expand Up @@ -443,7 +473,7 @@ private QueuedThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSetti
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();
Expand All @@ -458,7 +488,7 @@ private Handler makeContext(JettyEnv env, Config cfg) {

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) {
Expand All @@ -476,13 +506,14 @@ private Handler makeContext(JettyEnv env, Config cfg) {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.FakeGroupAuditService;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.pgm.http.jetty.JettyServer;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.audit.HttpAuditEvent;
import com.google.inject.Inject;
import java.util.Optional;
import javax.servlet.http.HttpServletResponse;
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.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec;
Expand All @@ -33,9 +37,11 @@

public class AbstractGitOverHttpServlet extends AbstractPushForReview {
@Inject protected FakeGroupAuditService auditService;
private JettyServer jettyServer;

@Before
public void beforeEach() throws Exception {
jettyServer = server.getHttpdInjector().getInstance(JettyServer.class);
CredentialsProvider.setDefault(
new UsernamePasswordCredentialsProvider(admin.username(), admin.httpPassword()));
selectProtocol(AbstractPushForReview.Protocol.HTTP);
Expand Down Expand Up @@ -71,29 +77,52 @@ public void receivePackAuditEventLog() throws Exception {

@Test
@Sandboxed
public void uploadPackAuditEventLog() throws Exception {
public void anonymousUploadPackAuditEventLog() throws Exception {
uploadPackAuditEventLog(Constants.DEFAULT_REMOTE_NAME, Optional.empty());
}

@Test
@Sandboxed
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();
// testRepo is already a clone. Make a server-side change so we have something to fetch.
try (Repository repo = repoManager.openRepository(project);
TestRepository<?> testRepo = new TestRepository<>(repo)) {
testRepo.branch("master").commit().create();
}
testRepo.git().fetch().call();
testRepo.git().fetch().setRemote(remote).call();

ImmutableList<HttpAuditEvent> auditEvents = auditService.drainHttpAuditEvents();
assertThat(auditEvents).hasSize(2);

HttpAuditEvent lsRemote = auditEvents.get(0);
// Repo URL doesn't include /a, so fetching doesn't cause authentication.
assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class);
assertThat(lsRemote.who.toString())
.isEqualTo(
accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS"));
assertThat(lsRemote.what).endsWith("/info/refs?service=git-upload-pack");
assertThat(lsRemote.params).containsExactly("service", "git-upload-pack");
assertThat(lsRemote.httpStatus).isEqualTo(HttpServletResponse.SC_OK);

HttpAuditEvent uploadPack = auditEvents.get(1);
assertThat(lsRemote.who).isInstanceOf(AnonymousUser.class);
assertThat(lsRemote.who.toString())
.isEqualTo(
accountId.map(id -> "IdentifiedUser[account " + id.get() + "]").orElse("ANONYMOUS"));
assertThat(uploadPack.what).endsWith("/git-upload-pack");
assertThat(uploadPack.params).isEmpty();
assertThat(uploadPack.httpStatus).isEqualTo(HttpServletResponse.SC_OK);
assertThat(jettyServer.numActiveSessions()).isEqualTo(0);
}
}

0 comments on commit e3f479c

Please sign in to comment.