Add bearer token to the git fetch.

Implement org.eclipse.jgit.transport.TransportHttpWithBearerToken as a
hack to allow git operations over http/https with Bearer Token
Authentication. The package org.eclipse.jgit.transport has been created
in pull-replication plugin in order to provide access to protected instance variables, methods and classes in the jgit module.

Add TransportProvider to intantiate transparently git transport client.

Filter git upload pack request in BearerAuthenticationFilter.

Bug: Issue 15605
Change-Id: Iba5e33e9674d97abd386e4d3060d0acc2d8e5c21
diff --git a/BUILD b/BUILD
index 82112d1..5c5f240 100644
--- a/BUILD
+++ b/BUILD
@@ -46,6 +46,6 @@
     ),
     deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
         ":pull-replication__plugin",
-         "//plugins/replication:replication",
+        "//plugins/replication:replication",
     ],
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
index f196dbe..8147149 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
@@ -82,7 +82,7 @@
 
     if (isBasicAuthenticationRequest(requestURI)) {
       filterChain.doFilter(servletRequest, servletResponse);
-    } else if (isPullReplicationApiRequest(requestURI)) {
+    } else if (isPullReplicationApiRequest(requestURI) || isGitUploadPackRequest(httpRequest)) {
       Optional<String> authorizationHeader =
           Optional.ofNullable(httpRequest.getHeader("Authorization"));
 
@@ -100,6 +100,13 @@
     }
   }
 
+  private boolean isGitUploadPackRequest(HttpServletRequest requestURI) {
+    return requestURI.getRequestURI().contains("git-upload-pack")
+        || Optional.ofNullable(requestURI.getQueryString())
+            .map(q -> q.contains("git-upload-pack"))
+            .orElse(false);
+  }
+
   private boolean isBearerTokenAuthenticated(
       Optional<String> authorizationHeader, String bearerToken) {
     return authorizationHeader
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
index 5de82f1..89972cf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
@@ -18,34 +18,22 @@
 
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
-import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
 import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.FetchResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.*;
 
 public class JGitFetch implements Fetch {
-  private final RemoteConfig config;
-  private final CredentialsProvider credentialsProvider;
   URIish uri;
   Repository git;
+  private final TransportProvider transportProvider;
 
   @Inject
   public JGitFetch(
-      SourceConfiguration sourceConfig,
-      CredentialsFactory cpFactory,
-      @Assisted URIish uri,
-      @Assisted Repository git) {
-    this.config = sourceConfig.getRemoteConfig();
-    this.credentialsProvider = cpFactory.create(config.getName());
+      TransportProvider transportProvider, @Assisted URIish uri, @Assisted Repository git) {
+    this.transportProvider = transportProvider;
     this.uri = uri;
     this.git = git;
   }
@@ -53,7 +41,7 @@
   @Override
   public List<RefUpdateState> fetch(List<RefSpec> refs) throws IOException {
     FetchResult res;
-    try (Transport tn = Transport.open(git, uri)) {
+    try (Transport tn = transportProvider.open(git, uri)) {
       res = fetchVia(tn, refs);
     }
     return res.getTrackingRefUpdates().stream()
@@ -62,9 +50,6 @@
   }
 
   private FetchResult fetchVia(Transport tn, List<RefSpec> fetchRefSpecs) throws IOException {
-    tn.applyConfig(config);
-    tn.setCredentialsProvider(credentialsProvider);
-
     repLog.info("Fetch references {} from {}", fetchRefSpecs, uri);
     return tn.fetch(NullProgressMonitor.INSTANCE, fetchRefSpecs);
   }
diff --git a/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java b/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java
new file mode 100644
index 0000000..68ff6f8
--- /dev/null
+++ b/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2022 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 org.eclipse.jgit.transport;
+
+import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Set;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.http.HttpConnection;
+
+/**
+ * This is a hack in order to allow git over http/https with Bearer Token Authentication.
+ *
+ * <p>Currently {@link org.eclipse.jgit.transport.TransportHttp} does NOT provide Bearer Token
+ * Authentication and unfortunately it is not possible to extend the functionality because some
+ * classes, methods and instance variables are private or protected. This package in the pull
+ * replication plugin provides the visibility needed and allows to extend the functionality.
+ *
+ * <p>It is important to mention that in the case of git push operation, this class cannot be used
+ * because the push operation needs to initialise a push hook: {@link
+ * org.eclipse.jgit.transport.Transport#push(ProgressMonitor, Collection, OutputStream)} and it is
+ * defined as a private in the code.
+ */
+public class TransportHttpWithBearerToken extends TransportHttp {
+
+  private static final String SCHEME_HTTP = "http";
+  private static final String SCHEME_HTTPS = "https";
+  private static final Set<String> SCHEMES_ALLOWED = ImmutableSet.of(SCHEME_HTTP, SCHEME_HTTPS);
+  private final String bearerToken;
+
+  public TransportHttpWithBearerToken(Repository local, URIish uri, String bearerToken)
+      throws NotSupportedException {
+    super(local, uri);
+    this.bearerToken = bearerToken;
+  }
+
+  protected HttpConnection httpOpen(String method, URL u, AcceptEncoding acceptEncoding)
+      throws IOException {
+    HttpConnection conn = super.httpOpen(method, u, acceptEncoding);
+    conn.setRequestProperty(HDR_AUTHORIZATION, "Bearer " + bearerToken); // $NON-NLS-1$
+    return conn;
+  }
+
+  /**
+   * This method copies the behaviour of {@link
+   * org.eclipse.jgit.transport.TransportProtocol#canHandle(URIish, Repository, String)} in the case
+   * of {@link org.eclipse.jgit.transport.TransportHttp} where scheme, host and path are compulsory.
+   */
+  public static boolean canHandle(URIish uri) {
+    return SCHEMES_ALLOWED.contains(uri.getScheme())
+        && !Strings.isNullOrEmpty(uri.getHost())
+        && !Strings.isNullOrEmpty(uri.getPath());
+  }
+}
diff --git a/src/main/java/org/eclipse/jgit/transport/TransportProvider.java b/src/main/java/org/eclipse/jgit/transport/TransportProvider.java
new file mode 100644
index 0000000..ca15b6c
--- /dev/null
+++ b/src/main/java/org/eclipse/jgit/transport/TransportProvider.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2022 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 org.eclipse.jgit.transport;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
+import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
+import java.util.Optional;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * This class is responsible for providing a Custom Git HTTP Transport with Bearer Token
+ * Authentication or a concrete implementation of {@link org.eclipse.jgit.transport.Transport}.
+ */
+@Singleton
+public class TransportProvider {
+  private final RemoteConfig remoteConfig;
+  private final CredentialsProvider credentialsProvider;
+  private final Optional<String> bearerToken;
+
+  @Inject
+  public TransportProvider(
+      SourceConfiguration sourceConfig,
+      CredentialsFactory cpFactory,
+      BearerTokenProvider bearerTokenProvider) {
+    this.remoteConfig = sourceConfig.getRemoteConfig();
+    this.credentialsProvider = cpFactory.create(remoteConfig.getName());
+    this.bearerToken = bearerTokenProvider.get();
+  }
+
+  public Transport open(Repository local, URIish uri)
+      throws NotSupportedException, TransportException {
+    return (bearerToken.isPresent() && TransportHttpWithBearerToken.canHandle(uri))
+        ? provideTransportHttpWithBearerToken(local, uri)
+        : provideNativeTransport(local, uri);
+  }
+
+  private Transport provideTransportHttpWithBearerToken(Repository local, URIish uri)
+      throws NotSupportedException {
+    Transport tn = new TransportHttpWithBearerToken(local, uri, bearerToken.get());
+    tn.applyConfig(remoteConfig);
+    return tn;
+  }
+
+  private Transport provideNativeTransport(Repository local, URIish uri)
+      throws NotSupportedException, TransportException {
+    Transport tn = Transport.open(local, uri);
+    tn.applyConfig(remoteConfig);
+    tn.setCredentialsProvider(credentialsProvider);
+    return tn;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index 187af3d..f3c62c5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -19,61 +19,37 @@
 import static com.google.gerrit.acceptance.GitUtil.pushOne;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.util.stream.Collectors.toList;
 
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.SkipProjectClone;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Permission;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.project.ProjectResource;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.time.Duration;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.function.Supplier;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
-import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
 @SkipProjectClone
@@ -82,48 +58,29 @@
     name = "pull-replication",
     sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
     httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
-public class PullReplicationIT extends LightweightPluginDaemonTest {
-  private static final Optional<String> ALL_PROJECTS = Optional.empty();
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private static final int TEST_REPLICATION_DELAY = 1;
-  private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000);
-  private static final String TEST_REPLICATION_SUFFIX = "suffix1";
-  private static final String TEST_REPLICATION_REMOTE = "remote1";
+public class PullReplicationIT extends PullReplicationSetupBase {
 
-  @Inject private SitePaths sitePaths;
-  @Inject private ProjectOperations projectOperations;
-  @Inject private DynamicSet<ProjectDeletedListener> deletedListeners;
-  private Path gitPath;
-  private FileBasedConfig config;
-  private FileBasedConfig secureConfig;
+  @Override
+  protected void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+    List<String> fetchUrls =
+        buildReplicaURLs(replicaSuffixes, s -> gitPath.resolve("${name}" + s + ".git").toString());
+    config.setStringList("remote", remoteName, "url", fetchUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
+    config.setInt("remote", remoteName, "timeout", 600);
+    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
+    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
+    config.setBoolean("gerrit", null, "autoReload", true);
+    config.save();
+  }
 
   @Override
   public void setUpTestPlugin() throws Exception {
     setUpTestPlugin(false);
   }
 
-  protected void setUpTestPlugin(boolean loadExisting) throws Exception {
-    gitPath = sitePaths.site_path.resolve("git");
-
-    File configFile = sitePaths.etc_dir.resolve("replication.config").toFile();
-    config = new FileBasedConfig(configFile, FS.DETECTED);
-    if (loadExisting && configFile.exists()) {
-      config.load();
-    }
-    setReplicationSource(
-        TEST_REPLICATION_REMOTE,
-        TEST_REPLICATION_SUFFIX,
-        ALL_PROJECTS); // Simulates a full replication.config initialization
-    config.save();
-
-    secureConfig =
-        new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
-    setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
-    secureConfig.save();
-
-    super.setUpTestPlugin();
-  }
-
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
   public void shouldReplicateNewChangeRef() throws Exception {
@@ -425,80 +382,4 @@
       assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
     }
   }
-
-  private Ref getRef(Repository repo, String branchName) throws IOException {
-    return repo.getRefDatabase().exactRef(branchName);
-  }
-
-  private Ref checkedGetRef(Repository repo, String branchName) {
-    try {
-      return repo.getRefDatabase().exactRef(branchName);
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo);
-      return null;
-    }
-  }
-
-  private void setReplicationSource(
-      String remoteName, String replicaSuffix, Optional<String> project)
-      throws IOException, ConfigInvalidException {
-    setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project);
-  }
-
-  private void setReplicationSource(
-      String remoteName, List<String> replicaSuffixes, Optional<String> project)
-      throws IOException, ConfigInvalidException {
-
-    List<String> replicaUrls =
-        replicaSuffixes.stream()
-            .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString())
-            .collect(toList());
-    config.setStringList("remote", remoteName, "url", replicaUrls);
-    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
-    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
-    config.setInt("remote", remoteName, "timeout", 600);
-    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
-    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
-    config.setBoolean("gerrit", null, "autoReload", true);
-    config.save();
-  }
-
-  private void setReplicationCredentials(String remoteName, String username, String password)
-      throws IOException {
-    secureConfig.setString("remote", remoteName, "username", username);
-    secureConfig.setString("remote", remoteName, "password", password);
-    secureConfig.save();
-  }
-
-  private void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
-    WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT);
-  }
-
-  private <T> T getInstance(Class<T> classObj) {
-    return plugin.getSysInjector().getInstance(classObj);
-  }
-
-  private Project.NameKey createTestProject(String name) throws Exception {
-    return projectOperations.newProject().name(name).create();
-  }
-
-  @Singleton
-  public static class FakeDeleteProjectPlugin implements RestModifyView<ProjectResource, Input> {
-    private int deleteEndpointCalls;
-
-    FakeDeleteProjectPlugin() {
-      this.deleteEndpointCalls = 0;
-    }
-
-    @Override
-    public Response<?> apply(ProjectResource resource, Input input)
-        throws AuthException, BadRequestException, ResourceConflictException, Exception {
-      deleteEndpointCalls += 1;
-      return Response.ok();
-    }
-
-    int getDeleteEndpointCalls() {
-      return deleteEndpointCalls;
-    }
-  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java
new file mode 100644
index 0000000..e07d481
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.replication.pull;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+public abstract class PullReplicationSetupBase extends LightweightPluginDaemonTest {
+
+  protected static final Optional<String> ALL_PROJECTS = Optional.empty();
+  protected static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  protected static final int TEST_REPLICATION_DELAY = 1;
+  protected static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000);
+  protected static final String TEST_REPLICATION_SUFFIX = "suffix1";
+  protected static final String TEST_REPLICATION_REMOTE = "remote1";
+  @Inject protected SitePaths sitePaths;
+  @Inject protected ProjectOperations projectOperations;
+  @Inject protected DynamicSet<ProjectDeletedListener> deletedListeners;
+  protected Path gitPath;
+  protected FileBasedConfig config;
+  protected FileBasedConfig secureConfig;
+
+  protected void setUpTestPlugin(boolean loadExisting) throws Exception {
+    gitPath = sitePaths.site_path.resolve("git");
+
+    File configFile = sitePaths.etc_dir.resolve("replication.config").toFile();
+    config = new FileBasedConfig(configFile, FS.DETECTED);
+    if (loadExisting && configFile.exists()) {
+      config.load();
+    }
+    setReplicationSource(
+        TEST_REPLICATION_REMOTE,
+        TEST_REPLICATION_SUFFIX,
+        ALL_PROJECTS); // Simulates a full replication.config initialization
+    config.save();
+
+    secureConfig =
+        new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
+    setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
+    secureConfig.save();
+
+    super.setUpTestPlugin();
+  }
+
+  protected Ref getRef(Repository repo, String branchName) throws IOException {
+    return repo.getRefDatabase().exactRef(branchName);
+  }
+
+  protected Ref checkedGetRef(Repository repo, String branchName) {
+    try {
+      return repo.getRefDatabase().exactRef(branchName);
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo);
+      return null;
+    }
+  }
+
+  protected void setReplicationSource(
+      String remoteName, String replicaSuffix, Optional<String> project)
+      throws IOException, ConfigInvalidException {
+    setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project);
+  }
+
+  protected abstract void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project) throws IOException;
+
+  protected void setReplicationCredentials(String remoteName, String username, String password)
+      throws IOException {
+    secureConfig.setString("remote", remoteName, "username", username);
+    secureConfig.setString("remote", remoteName, "password", password);
+    secureConfig.save();
+  }
+
+  protected void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
+    WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT);
+  }
+
+  protected <T> T getInstance(Class<T> classObj) {
+    return plugin.getSysInjector().getInstance(classObj);
+  }
+
+  protected NameKey createTestProject(String name) throws Exception {
+    return projectOperations.newProject().name(name).create();
+  }
+
+  protected List<String> buildReplicaURLs(
+      List<String> replicaSuffixes, Function<String, String> toURL) {
+    return replicaSuffixes.stream().map(suffix -> toURL.apply(suffix)).collect(toList());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
new file mode 100644
index 0000000..d8e5947
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.replication.pull;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Test;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+    name = "pull-replication",
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+    httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
+public class PullReplicationWithGitHttpTransportProtocolIT extends PullReplicationSetupBase {
+
+  @Override
+  protected void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+    List<String> fetchUrls =
+        buildReplicaURLs(replicaSuffixes, s -> adminRestSession.url() + "/${name}" + s + ".git");
+    config.setStringList("remote", remoteName, "url", fetchUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
+    config.setInt("remote", remoteName, "timeout", 600);
+    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
+    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
+    config.setBoolean("gerrit", null, "autoReload", true);
+    config.save();
+  }
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    setUpTestPlugin(false);
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateWithBasicAuthentication() throws Exception {
+    runTest();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token")
+  public void shouldReplicateWithBearerTokenAuthentication() throws Exception {
+    runTest();
+  }
+
+  private void runTest() throws Exception {
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    GitReferenceUpdatedListener.Event event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            ReceiveCommand.Type.CREATE);
+    pullReplicationQueue.onGitReferenceUpdated(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
index 96c83a1..824496a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
@@ -15,6 +15,8 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -25,6 +27,7 @@
 import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser;
 import java.io.IOException;
+import java.util.Optional;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -37,6 +40,9 @@
 @RunWith(MockitoJUnitRunner.class)
 public class BearerAuthenticationFilterTest {
 
+  private final Optional<String> NO_QUERY_PARAMETERS = Optional.empty();
+  private final Optional<String> GIT_UPLOAD_PACK_QUERY_PARAMETER =
+      Optional.of("service=git-upload-pack");
   @Mock private DynamicItem<WebSession> session;
   @Mock private WebSession webSession;
   @Mock private Provider<ThreadLocalRequestContext> threadLocalRequestContextProvider;
@@ -47,9 +53,11 @@
   @Mock private FilterChain filterChain;
   private final String pluginName = "pull-replication";
 
-  private void authenticateWithURI(String uri) throws ServletException, IOException {
+  private void authenticateAndFilter(String uri, Optional<String> queryStringMaybe)
+      throws ServletException, IOException {
     final String bearerToken = "some-bearer-token";
     when(httpServletRequest.getRequestURI()).thenReturn(uri);
+    queryStringMaybe.ifPresent(qs -> when(httpServletRequest.getQueryString()).thenReturn(qs));
     when(httpServletRequest.getHeader("Authorization"))
         .thenReturn(String.format("Bearer %s", bearerToken));
     when(threadLocalRequestContextProvider.get()).thenReturn(threadLocalRequestContext);
@@ -59,7 +67,8 @@
             session, pluginName, pluginUser, threadLocalRequestContextProvider, bearerToken);
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
 
-    verify(httpServletRequest).getRequestURI();
+    verify(httpServletRequest, atMost(2)).getRequestURI();
+    verify(httpServletRequest, atMost(1)).getQueryString();
     verify(httpServletRequest).getHeader("Authorization");
     verify(threadLocalRequestContextProvider).get();
     verify(session).get();
@@ -68,38 +77,45 @@
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenFetch() throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~fetch");
+  public void shouldAuthenticateWhenFetch() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~fetch", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenApplyObject()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~apply-object");
+  public void shouldAuthenticateWhenApplyObject() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~apply-object", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenApplyObjects()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~apply-objects");
+  public void shouldAuthenticateWhenApplyObjects() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~apply-objects", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenDeleteProject()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~delete-project");
+  public void shouldAuthenticateWhenDeleteProject() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~delete-project", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenUpdateHead()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/projects/my-project/HEAD");
+  public void shouldAuthenticateWhenUpdateHead() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/projects/my-project/HEAD", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenInitProject()
+  public void shouldAuthenticateWhenInitProject() throws ServletException, IOException {
+    authenticateAndFilter(
+        "any-prefix/pull-replication/init-project/my-project.git", NO_QUERY_PARAMETERS);
+  }
+
+  @Test
+  public void shouldAuthenticateWhenGitUploadPacket() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/git-upload-pack", NO_QUERY_PARAMETERS);
+  }
+
+  @Test
+  public void shouldAuthenticateWhenGitUploadPacketInQueryParameter()
       throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication/init-project/my-project.git");
+    authenticateAndFilter("any-prefix", GIT_UPLOAD_PACK_QUERY_PARAMETER);
   }
 
   @Test
@@ -171,7 +187,7 @@
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
 
-    verify(httpServletRequest).getRequestURI();
+    verify(httpServletRequest, times(2)).getRequestURI();
     verify(filterChain).doFilter(httpServletRequest, httpServletResponse);
   }
 
diff --git a/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java b/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java
new file mode 100644
index 0000000..5bf1047
--- /dev/null
+++ b/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2022 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 org.eclipse.jgit.transport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.net.URISyntaxException;
+import org.junit.Test;
+
+public class TransportHttpWithBearerTokenTest {
+
+  @Test
+  public void cannotHandleURIWhenSchemaIsNeitherHttpNorHttps() throws URISyntaxException {
+    URIish uriUnderTest = new URIish("some-uri").setScheme(null);
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void cannotHandleURIWhenHostIsNotPresent() throws URISyntaxException {
+    URIish uriUnderTest = new URIish("some-uri").setScheme("http").setHost(null);
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void cannotHandleURIWhenPathIsNotPresent() throws URISyntaxException {
+    URIish uriUnderTest =
+        new URIish("some-uri").setScheme("http").setHost("some-host").setPath(null);
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void canHandleURIWhenIsWellFormed() throws URISyntaxException {
+    URIish uriUnderTest = new URIish("http://some-host/some-path");
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isTrue();
+  }
+}
diff --git a/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java b/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java
new file mode 100644
index 0000000..ea70c40
--- /dev/null
+++ b/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2022 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 org.eclipse.jgit.transport;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.transport.HttpConfig.EXTRA_HEADER;
+import static org.eclipse.jgit.transport.HttpConfig.HTTP;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
+import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TransportProviderTest {
+  @Mock private SourceConfiguration sourceConfig;
+  @Mock private CredentialsFactory cpFactory;
+  @Mock private RemoteConfig remoteConfig;
+  @Mock private BearerTokenProvider bearerTokenProvider;
+  @Mock private Repository repository;
+  @Mock private StoredConfig storedConfig;
+  @Mock private org.eclipse.jgit.transport.TransferConfig transferConfig;
+
+  @Before
+  public void setup() {
+    when(sourceConfig.getRemoteConfig()).thenReturn(remoteConfig);
+    when(repository.getConfig()).thenReturn(storedConfig);
+    String[] emptyHeaders = {};
+    when(storedConfig.getStringList(HTTP, null, EXTRA_HEADER)).thenReturn(emptyHeaders);
+    when(storedConfig.get(TransferConfig.KEY)).thenReturn(transferConfig);
+  }
+
+  private void verifyConstructor() {
+    verify(sourceConfig).getRemoteConfig();
+    verify(remoteConfig).getName();
+    verify(bearerTokenProvider).get();
+  }
+
+  @Test
+  public void shouldProvideTransportHttpWithBearerToken() throws URISyntaxException, IOException {
+    when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token"));
+
+    TransportProvider transportProvider =
+        new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider);
+    verifyConstructor();
+
+    URIish urIish = new URIish("http://some-host/some-path");
+    Transport transport = transportProvider.open(repository, urIish);
+    assertThat(transport).isInstanceOf(TransportHttpWithBearerToken.class);
+  }
+
+  @Test
+  public void shouldProvideNativeTransportWhenNoBearerTokenProvided()
+      throws URISyntaxException, IOException {
+
+    when(bearerTokenProvider.get()).thenReturn(Optional.empty());
+
+    TransportProvider transportProvider =
+        new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider);
+    verifyConstructor();
+
+    URIish urIish = new URIish("ssh://some-host/some-path");
+    Transport transport = transportProvider.open(repository, urIish);
+    assertThat(transport).isNotInstanceOf(TransportHttpWithBearerToken.class);
+  }
+
+  @Test
+  public void shouldProvideNativeTransportWhenNoHttpSchemeProvided()
+      throws URISyntaxException, IOException {
+    when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token"));
+
+    TransportProvider transportProvider =
+        new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider);
+    verifyConstructor();
+
+    URIish urIish = new URIish("ssh://some-host/some-path");
+    Transport transport = transportProvider.open(repository, urIish);
+    assertThat(transport).isNotInstanceOf(TransportHttpWithBearerToken.class);
+  }
+}