Files
gerrit/java/com/google/gerrit/httpd/GitOverHttpServlet.java
David Pursehouse 437d7ef616 Allow to enable git protocol version 2 for upload pack
When receive.enableProtocolV2 is set to true, set the necessary extra
parameters on the UploadPack to tell JGit to handle the request with
protocol v2.

Note that according to JGit's implementation, the git config on the
repository on the server must also be configured to use protocol v2.

This can be achieved either by setting it globally in the gerrit
user's ~/.gitconfig or per repository in the repository's .git/config:

  [protocol]
    version = 2

Test plan:
  - Set protocol.version to 2 in the project's server-side config (or
    in the gerrit user's ~/.gitconfig)

  - Clone the project over SSH or HTTP

  - From the client, run:
    GIT_TRACE_PACKET=1 git -c protocol.version=2 ls-remote

    (one can also configure this permanently on the local project by
     running `git config protocol.version 2`)

  - Observe the packet output including:
    git< version 2

Feature: Issue 9046
Helped-by: David Ostrovsky <david@ostrovsky.org>
Helped-by: Jonathan Nieder <jrn@google.com>
Change-Id: I30290e8f060c1ee11b170aac2baeed10f213aad1
2018-11-14 15:52:04 -08:00

492 lines
18 KiB
Java

// Copyright (C) 2010 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd;
import com.google.common.cache.Cache;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.audit.HttpAuditEvent;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.UploadPackInitializer;
import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.group.GroupAuditService;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.http.server.GitSmartHttpTools;
import org.eclipse.jgit.http.server.ServletUtils;
import org.eclipse.jgit.http.server.resolver.AsIsFileService;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PostUploadHook;
import org.eclipse.jgit.transport.PostUploadHookChain;
import org.eclipse.jgit.transport.PreUploadHook;
import org.eclipse.jgit.transport.PreUploadHookChain;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.ServiceMayNotContinueException;
import org.eclipse.jgit.transport.UploadPack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.RepositoryResolver;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
/** Serves Git repositories over HTTP. */
@Singleton
public class GitOverHttpServlet extends GitServlet {
private static final long serialVersionUID = 1L;
private static final String ATT_STATE = ProjectState.class.getName();
private static final String ATT_ARC = AsyncReceiveCommits.class.getName();
private static final String ID_CACHE = "adv_bases";
public static final String URL_REGEX;
static {
StringBuilder url = new StringBuilder();
url.append("^(?:/a)?(?:/p/|/)(.*/(?:info/refs");
for (String name : GitSmartHttpTools.VALID_SERVICES) {
url.append('|').append(name);
}
url.append("))$");
URL_REGEX = url.toString();
}
static class Module extends AbstractModule {
private final boolean enableReceive;
Module(boolean enableReceive) {
this.enableReceive = enableReceive;
}
@Override
protected void configure() {
bind(Resolver.class);
bind(UploadFactory.class);
bind(UploadFilter.class);
bind(new TypeLiteral<ReceivePackFactory<HttpServletRequest>>() {})
.to(enableReceive ? ReceiveFactory.class : DisabledReceiveFactory.class);
bind(ReceiveFilter.class);
install(
new CacheModule() {
@Override
protected void configure() {
cache(ID_CACHE, AdvertisedObjectsCacheKey.class, new TypeLiteral<Set<ObjectId>>() {})
.maximumWeight(4096)
.expireAfterWrite(Duration.ofMinutes(10));
}
});
}
}
@Inject
GitOverHttpServlet(
Resolver resolver,
UploadFactory upload,
UploadFilter uploadFilter,
ReceivePackFactory<HttpServletRequest> receive,
ReceiveFilter receiveFilter) {
setRepositoryResolver(resolver);
setAsIsFileService(AsIsFileService.DISABLED);
setUploadPackFactory(upload);
addUploadPackFilter(uploadFilter);
setReceivePackFactory(receive);
addReceivePackFilter(receiveFilter);
}
private static String extractWhat(HttpServletRequest request) {
StringBuilder commandName = new StringBuilder(request.getRequestURL());
if (request.getQueryString() != null) {
commandName.append("?").append(request.getQueryString());
}
return commandName.toString();
}
private static ListMultimap<String, String> extractParameters(HttpServletRequest request) {
ListMultimap<String, String> multiMap = ArrayListMultimap.create();
if (request.getQueryString() != null) {
request
.getParameterMap()
.forEach(
(k, v) -> {
for (int i = 0; i < v.length; i++) {
multiMap.put(k, v[i]);
}
});
}
return multiMap;
}
static class Resolver implements RepositoryResolver<HttpServletRequest> {
private final GitRepositoryManager manager;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> userProvider;
private final ProjectCache projectCache;
@Inject
Resolver(
GitRepositoryManager manager,
PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider,
ProjectCache projectCache) {
this.manager = manager;
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
this.projectCache = projectCache;
}
@Override
public Repository open(HttpServletRequest req, String projectName)
throws RepositoryNotFoundException, ServiceNotAuthorizedException,
ServiceNotEnabledException, ServiceMayNotContinueException {
while (projectName.endsWith("/")) {
projectName = projectName.substring(0, projectName.length() - 1);
}
if (projectName.endsWith(".git")) {
// Be nice and drop the trailing ".git" suffix, which we never keep
// in our database, but clients might mistakenly provide anyway.
//
projectName = projectName.substring(0, projectName.length() - 4);
while (projectName.endsWith("/")) {
projectName = projectName.substring(0, projectName.length() - 1);
}
}
CurrentUser user = userProvider.get();
user.setAccessPath(AccessPath.GIT);
try {
Project.NameKey nameKey = new Project.NameKey(projectName);
ProjectState state = projectCache.checkedGet(nameKey);
if (state == null || !state.statePermitsRead()) {
throw new RepositoryNotFoundException(nameKey.get());
}
req.setAttribute(ATT_STATE, state);
try {
permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS);
} catch (AuthException e) {
if (user instanceof AnonymousUser) {
throw new ServiceNotAuthorizedException();
}
throw new ServiceNotEnabledException(e.getMessage());
}
return manager.openRepository(nameKey);
} catch (IOException | PermissionBackendException err) {
throw new ServiceMayNotContinueException(projectName + " unavailable", err);
}
}
}
static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
private final TransferConfig config;
private final DynamicSet<PreUploadHook> preUploadHooks;
private final DynamicSet<PostUploadHook> postUploadHooks;
private final DynamicSet<UploadPackInitializer> uploadPackInitializers;
@Inject
UploadFactory(
TransferConfig tc,
DynamicSet<PreUploadHook> preUploadHooks,
DynamicSet<PostUploadHook> postUploadHooks,
DynamicSet<UploadPackInitializer> uploadPackInitializers) {
this.config = tc;
this.preUploadHooks = preUploadHooks;
this.postUploadHooks = postUploadHooks;
this.uploadPackInitializers = uploadPackInitializers;
}
@Override
public UploadPack create(HttpServletRequest req, Repository repo) {
UploadPack up = new UploadPack(repo);
up.setPackConfig(config.getPackConfig());
up.setTimeout(config.getTimeout());
up.setPreUploadHook(PreUploadHookChain.newChain(Lists.newArrayList(preUploadHooks)));
up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
if (config.enableProtocolV2()) {
String header = req.getHeader("Git-Protocol");
if (header != null) {
String[] params = header.split(":");
up.setExtraParameters(Arrays.asList(params));
}
}
ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
for (UploadPackInitializer initializer : uploadPackInitializers) {
initializer.init(state.getNameKey(), up);
}
return up;
}
}
static class UploadFilter implements Filter {
private final UploadValidators.Factory uploadValidatorsFactory;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> userProvider;
private final GroupAuditService groupAuditService;
@Inject
UploadFilter(
UploadValidators.Factory uploadValidatorsFactory,
PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider,
GroupAuditService groupAuditService) {
this.uploadValidatorsFactory = uploadValidatorsFactory;
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
this.groupAuditService = groupAuditService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain next)
throws IOException, ServletException {
// The Resolver above already checked READ access for us.
Repository repo = ServletUtils.getRepository(request);
ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
PermissionBackend.ForProject perm =
permissionBackend.currentUser().project(state.getNameKey());
try {
perm.check(ProjectPermission.RUN_UPLOAD_PACK);
} catch (AuthException e) {
GitSmartHttpTools.sendError(
(HttpServletRequest) request,
(HttpServletResponse) response,
HttpServletResponse.SC_FORBIDDEN,
"upload-pack not permitted on this server");
return;
} catch (PermissionBackendException e) {
throw new ServletException(e);
} finally {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
groupAuditService.dispatch(
new HttpAuditEvent(
httpRequest.getSession().getId(),
userProvider.get(),
extractWhat(httpRequest),
TimeUtil.nowMs(),
extractParameters(httpRequest),
httpRequest.getMethod(),
httpRequest,
httpResponse.getStatus(),
httpResponse));
}
// We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
// may have been overridden by a proxy server -- we'll try to avoid this.
UploadValidators uploadValidators =
uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
up.setPreUploadHook(
PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
next.doFilter(request, response);
}
@Override
public void init(FilterConfig config) {}
@Override
public void destroy() {}
}
static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
private final AsyncReceiveCommits.Factory factory;
private final Provider<CurrentUser> userProvider;
@Inject
ReceiveFactory(AsyncReceiveCommits.Factory factory, Provider<CurrentUser> userProvider) {
this.factory = factory;
this.userProvider = userProvider;
}
@Override
public ReceivePack create(HttpServletRequest req, Repository db)
throws ServiceNotAuthorizedException {
final ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
if (!(userProvider.get().isIdentifiedUser())) {
// Anonymous users are not permitted to push.
throw new ServiceNotAuthorizedException();
}
AsyncReceiveCommits arc =
factory.create(state, userProvider.get().asIdentifiedUser(), db, null);
ReceivePack rp = arc.getReceivePack();
req.setAttribute(ATT_ARC, arc);
return rp;
}
}
static class DisabledReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
@Override
public ReceivePack create(HttpServletRequest req, Repository db)
throws ServiceNotEnabledException {
throw new ServiceNotEnabledException();
}
}
static class ReceiveFilter implements Filter {
private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> userProvider;
private final GroupAuditService groupAuditService;
@Inject
ReceiveFilter(
@Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider,
GroupAuditService groupAuditService) {
this.cache = cache;
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
this.groupAuditService = groupAuditService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean isGet = "GET".equalsIgnoreCase(((HttpServletRequest) request).getMethod());
AsyncReceiveCommits arc = (AsyncReceiveCommits) request.getAttribute(ATT_ARC);
// Send refs down the wire.
ReceivePack rp = arc.getReceivePack();
rp.getAdvertiseRefsHook().advertiseRefs(rp);
ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
Capable canUpload;
try {
permissionBackend
.currentUser()
.project(state.getNameKey())
.check(ProjectPermission.RUN_RECEIVE_PACK);
canUpload = arc.canUpload();
} catch (AuthException e) {
GitSmartHttpTools.sendError(
(HttpServletRequest) request,
(HttpServletResponse) response,
HttpServletResponse.SC_FORBIDDEN,
"receive-pack not permitted on this server");
return;
} catch (PermissionBackendException e) {
throw new RuntimeException(e);
} finally {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
groupAuditService.dispatch(
new HttpAuditEvent(
httpRequest.getSession().getId(),
userProvider.get(),
extractWhat(httpRequest),
TimeUtil.nowMs(),
extractParameters(httpRequest),
httpRequest.getMethod(),
httpRequest,
httpResponse.getStatus(),
httpResponse));
}
if (canUpload != Capable.OK) {
GitSmartHttpTools.sendError(
(HttpServletRequest) request,
(HttpServletResponse) response,
HttpServletResponse.SC_FORBIDDEN,
"\n" + canUpload.getMessage());
return;
}
if (!rp.isCheckReferencedObjectsAreReachable()) {
chain.doFilter(request, response);
return;
}
if (!(userProvider.get().isIdentifiedUser())) {
chain.doFilter(request, response);
return;
}
AdvertisedObjectsCacheKey cacheKey =
AdvertisedObjectsCacheKey.create(userProvider.get().getAccountId(), state.getNameKey());
if (isGet) {
cache.invalidate(cacheKey);
} else {
Set<ObjectId> ids = cache.getIfPresent(cacheKey);
if (ids != null) {
rp.getAdvertisedObjects().addAll(ids);
cache.invalidate(cacheKey);
}
}
chain.doFilter(request, response);
if (isGet) {
cache.put(cacheKey, Collections.unmodifiableSet(new HashSet<>(rp.getAdvertisedObjects())));
}
}
@Override
public void init(FilterConfig arg0) {}
@Override
public void destroy() {}
}
}