Added support for rename in replication

Now rename-project also works in replica mode for http.

Added feature to automatically replicate rename-project to urls in
gerrit.config via rest api.

HttpClientProvider, HttpResponseHandler and HttpSession are adapted
from rest forwarding mechanism of high-availability plugin to perform
http requests since they can already perform http requests to other
gerrit instances successfully.

Adaptations were mainly done on access modifiers of HttpResponseHandler
and changing the success code for isSuccessful from SC_NO_CONTENT to
SC_OK. HttpSession has delete requests removed, since rename-project
only needs post request.

Mainly based on previous attemp from Icbe98eb6af9bffc38f4b882c149b9092f9701f3c

Change-Id: I1f4aa638a565a1f85fb1f710a65ac659b5273107
diff --git a/BUILD b/BUILD
index 6c480ea..3f9f94a 100644
--- a/BUILD
+++ b/BUILD
@@ -14,6 +14,7 @@
         "Gerrit-PluginName: rename-project",
         "Gerrit-Module: com.googlesource.gerrit.plugins.renameproject.Module",
         "Gerrit-SshModule: com.googlesource.gerrit.plugins.renameproject.SshModule",
+        "Gerrit-HttpModule: com.googlesource.gerrit.plugins.renameproject.HttpModule",
     ],
     resources = glob(["src/main/resources/**/*"]),
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java
index c25152e..a8e4c02 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Configuration.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.renameproject;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -24,17 +25,31 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Configuration {
+  private static final Logger log = LoggerFactory.getLogger(Configuration.class);
   private static final int DEFAULT_SSH_CONNECTION_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
+  private static final int DEFAULT_TIMEOUT_MS = 5000;
   private static final String URL_KEY = "url";
+  private static final String USER_KEY = "user";
+  private static final String PASSWORD_KEY = "password";
+  private static final String CONNECTION_TIMEOUT_KEY = "connectionTimeout";
+  private static final String SOCKET_TIMEOUT_KEY = "socketTimeout";
+  private static final String HTTP_SECTION = "http";
+  private static final String REPLICA_SECTION = "replicaInfo";
 
   private final int indexThreads;
   private final int sshCommandTimeout;
   private final int sshConnectionTimeout;
-  private final String renameRegex;
   private final int renameReplicationRetries;
+  private final int connectionTimeout;
+  private final int socketTimeout;
+  private final String renameRegex;
+  private final String user;
+  private final String password;
 
   private final Set<String> urls;
 
@@ -46,7 +61,10 @@
     sshConnectionTimeout = cfg.getInt("sshConnectionTimeout", DEFAULT_SSH_CONNECTION_TIMEOUT_MS);
     renameRegex = cfg.getString("renameRegex", ".+");
     renameReplicationRetries = cfg.getInt("renameReplicationRetries", 3);
-
+    user = Strings.nullToEmpty(cfg.getString(USER_KEY, null));
+    password = Strings.nullToEmpty(cfg.getString(PASSWORD_KEY, null));
+    connectionTimeout = cfg.getInt(CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS);
+    socketTimeout = cfg.getInt(SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS);
     urls =
         Arrays.stream(cfg.getStringList(URL_KEY))
             .filter(Objects::nonNull)
@@ -78,4 +96,20 @@
   public int getRenameReplicationRetries() {
     return renameReplicationRetries;
   }
+
+  public String getUser() {
+    return user;
+  }
+
+  public String getPassword() {
+    return password;
+  }
+
+  public int getConnectionTimeout() {
+    return connectionTimeout;
+  }
+
+  public int getSocketTimeout() {
+    return socketTimeout;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpClientProvider.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpClientProvider.java
new file mode 100644
index 0000000..6fdf5dd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpClientProvider.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2023 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.renameproject;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Provides an HTTP client with SSL capabilities. */
+class HttpClientProvider implements Provider<CloseableHttpClient> {
+  private static final Logger log = LoggerFactory.getLogger(HttpClientProvider.class);
+  private static final int CONNECTIONS_PER_ROUTE = 100;
+  // Up to 2 target instances with the max number of connections per host:
+  private static final int MAX_CONNECTIONS = 2 * CONNECTIONS_PER_ROUTE;
+  private static final int MAX_CONNECTION_INACTIVITY = 10000;
+  private final Configuration cfg;
+  private final SSLConnectionSocketFactory sslSocketFactory;
+
+  @Inject
+  HttpClientProvider(Configuration cfg) {
+    this.cfg = cfg;
+    this.sslSocketFactory = buildSslSocketFactory();
+  }
+
+  @Override
+  public CloseableHttpClient get() {
+    return HttpClients.custom()
+        .setSSLSocketFactory(sslSocketFactory)
+        .setConnectionManager(customConnectionManager())
+        .setDefaultCredentialsProvider(buildCredentials())
+        .setDefaultRequestConfig(customRequestConfig())
+        .build();
+  }
+
+  private RequestConfig customRequestConfig() {
+    return RequestConfig.custom()
+        .setConnectTimeout(cfg.getConnectionTimeout())
+        .setSocketTimeout(cfg.getSocketTimeout())
+        .setConnectionRequestTimeout(cfg.getConnectionTimeout())
+        .build();
+  }
+
+  private HttpClientConnectionManager customConnectionManager() {
+    Registry<ConnectionSocketFactory> socketFactoryRegistry =
+        RegistryBuilder.<ConnectionSocketFactory>create()
+            .register("https", sslSocketFactory)
+            .register("http", PlainConnectionSocketFactory.INSTANCE)
+            .build();
+    PoolingHttpClientConnectionManager connManager =
+        new PoolingHttpClientConnectionManager(socketFactoryRegistry);
+    connManager.setDefaultMaxPerRoute(CONNECTIONS_PER_ROUTE);
+    connManager.setMaxTotal(MAX_CONNECTIONS);
+    connManager.setValidateAfterInactivity(MAX_CONNECTION_INACTIVITY);
+    return connManager;
+  }
+
+  private static SSLConnectionSocketFactory buildSslSocketFactory() {
+    return new SSLConnectionSocketFactory(buildSslContext(), NoopHostnameVerifier.INSTANCE);
+  }
+
+  private static SSLContext buildSslContext() {
+    try {
+      TrustManager[] trustAllCerts = new TrustManager[] {new DummyX509TrustManager()};
+      SSLContext context = SSLContext.getInstance("TLS");
+      context.init(null, trustAllCerts, null);
+      return context;
+    } catch (KeyManagementException | NoSuchAlgorithmException e) {
+      log.warn("Error building SSLContext object", e);
+      return null;
+    }
+  }
+
+  private BasicCredentialsProvider buildCredentials() {
+    BasicCredentialsProvider creds = new BasicCredentialsProvider();
+    log.info("Build creds: " + cfg.getUser() + "  " + cfg.getPassword());
+    creds.setCredentials(
+        AuthScope.ANY, new UsernamePasswordCredentials(cfg.getUser(), cfg.getPassword()));
+    return creds;
+  }
+
+  private static class DummyX509TrustManager implements X509TrustManager {
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+      return new X509Certificate[0];
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] certs, String authType) {
+      // no check
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] certs, String authType) {
+      // no check
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpModule.java
new file mode 100644
index 0000000..ebd1acd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpModule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 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.renameproject;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.server.config.GerritIsReplica;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+public class HttpModule extends ServletModule {
+  private boolean isReplica;
+
+  @Inject
+  public HttpModule(@GerritIsReplica Boolean isReplica) {
+    this.isReplica = isReplica;
+  }
+
+  @Override
+  protected void configureServlets() {
+    if (isReplica) {
+      DynamicSet.bind(binder(), AllRequestFilter.class)
+          .to(RenameProjectFilter.class)
+          .in(Scopes.SINGLETON);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpResponseHandler.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpResponseHandler.java
new file mode 100644
index 0000000..e8e4ce0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpResponseHandler.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2023 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.renameproject;
+
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.flogger.FluentLogger;
+import com.googlesource.gerrit.plugins.renameproject.HttpResponseHandler.HttpResult;
+import java.io.IOException;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+
+public class HttpResponseHandler implements ResponseHandler<HttpResult> {
+  public static class HttpResult {
+    private final boolean successful;
+    private final String message;
+
+    HttpResult(boolean successful, String message) {
+      this.successful = successful;
+      this.message = message;
+    }
+
+    public boolean isSuccessful() {
+      return successful;
+    }
+
+    public String getMessage() {
+      return message;
+    }
+  }
+
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
+
+  @Override
+  public HttpResult handleResponse(HttpResponse response) {
+    return new HttpResult(isSuccessful(response), parseResponse(response));
+  }
+
+  private static boolean isSuccessful(HttpResponse response) {
+    return response.getStatusLine().getStatusCode() == SC_OK;
+  }
+
+  private static String parseResponse(HttpResponse response) {
+    HttpEntity entity = response.getEntity();
+    String asString = "";
+    if (entity != null) {
+      try {
+        asString = EntityUtils.toString(entity);
+      } catch (IOException e) {
+        log.atSevere().withCause(e).log("Error parsing entity");
+      }
+    }
+    return asString;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpSession.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpSession.java
new file mode 100644
index 0000000..7ae2847
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/HttpSession.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2023 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.renameproject;
+
+import com.google.common.base.Supplier;
+import com.google.common.net.MediaType;
+import com.google.gerrit.server.events.SupplierSerializer;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.renameproject.HttpResponseHandler.HttpResult;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+public class HttpSession {
+  private final CloseableHttpClient httpClient;
+  private final Configuration cfg;
+  private final Gson gson =
+      new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
+
+  @Inject
+  HttpSession(CloseableHttpClient httpClient, Configuration cfg) {
+    this.httpClient = httpClient;
+    this.cfg = cfg;
+  }
+
+  public HttpResult post(String uri, Object content) throws IOException, AuthenticationException {
+    HttpPost post = new HttpPost(uri);
+    UsernamePasswordCredentials creds =
+        new UsernamePasswordCredentials(cfg.getUser(), cfg.getPassword());
+    post.addHeader(new BasicScheme().authenticate(creds, post, null));
+    setContent(post, content);
+
+    return httpClient.execute(post, new HttpResponseHandler());
+  }
+
+  private void setContent(HttpEntityEnclosingRequestBase request, Object content) {
+    if (content != null) {
+      request.addHeader("Content-Type", MediaType.JSON_UTF_8.toString());
+      request.setEntity(new StringEntity(jsonEncode(content), StandardCharsets.UTF_8));
+    }
+  }
+
+  private String jsonEncode(Object content) {
+    if (content instanceof String) {
+      return (String) content;
+    }
+    return gson.toJson(content);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
index f35c55e..4a65e31 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/Module.java
@@ -49,7 +49,7 @@
     bind(IndexUpdateHandler.class);
     bind(RevertRenameProject.class);
     bind(SshSessionFactory.class).toProvider(RenameReplicationSshSessionFactoryProvider.class);
-
+    install(new RestRenameReplicationModule());
     install(
         new RestApiModule() {
           @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
index d56d3f3..6ec97c9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProject.java
@@ -18,6 +18,7 @@
 import static com.googlesource.gerrit.plugins.renameproject.RenameProjectCapability.RENAME_PROJECT;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.gerrit.entities.Change;
@@ -32,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritIsReplica;
 import com.google.gerrit.server.extensions.events.PluginEvent;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -57,6 +59,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import org.apache.http.auth.AuthenticationException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.transport.URIish;
 import org.slf4j.Logger;
@@ -69,7 +72,18 @@
   public Response<?> apply(ProjectResource resource, Input input)
       throws IOException, AuthException, BadRequestException, ResourceConflictException,
           InterruptedException, ConfigInvalidException, RenameRevertException {
-    assertCanRename(resource, input, Optional.empty());
+    if (!isReplica) {
+      assertCanRename(resource, input, Optional.empty());
+      return renameAction(resource, input);
+    }
+    if (isAdmin()) {
+      return renameAction(resource, input);
+    }
+    throw new AuthException("You do not have enough privileges for this operation");
+  }
+
+  private Response<?> renameAction(ProjectResource resource, Input input)
+      throws IOException, ConfigInvalidException, RenameRevertException, InterruptedException {
     List<Id> changeIds = getChanges(resource, Optional.empty());
 
     if (changeIds == null || changeIds.size() <= WARNING_LIMIT || input.continueWithRename) {
@@ -81,7 +95,7 @@
     return Response.ok("");
   }
 
-  static class Input {
+  public static class Input {
 
     String name;
     boolean continueWithRename;
@@ -93,6 +107,9 @@
 
   private static final Logger log = LoggerFactory.getLogger(RenameProject.class);
   private static final String CACHE_NAME = "changeid_project";
+  private static final String WITH_AUTHENTICATION = "a";
+  public static final String RENAME_ACTION = "rename";
+  public static final String PROJECTS_ENDPOINT = "projects";
 
   private final DatabaseRenameHandler dbHandler;
   private final FilesystemRenameHandler fsHandler;
@@ -104,10 +121,12 @@
   private final PluginEvent pluginEvent;
   private final String pluginName;
   private final RenameLog renameLog;
+  private final boolean isReplica;
   private final PermissionBackend permissionBackend;
   private final Cache<Change.Id, String> changeIdProjectCache;
   private final RevertRenameProject revertRenameProject;
   private final SshHelper sshHelper;
+  private HttpSession httpSession;
   private final Configuration cfg;
 
   private List<Step> stepsPerformed;
@@ -123,11 +142,13 @@
       LockUnlockProject lockUnlockProject,
       PluginEvent pluginEvent,
       @PluginName String pluginName,
+      @GerritIsReplica Boolean isReplica,
       RenameLog renameLog,
       PermissionBackend permissionBackend,
       @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache,
       RevertRenameProject revertRenameProject,
       SshHelper sshHelper,
+      HttpSession httpSession,
       Configuration cfg) {
     this.dbHandler = dbHandler;
     this.fsHandler = fsHandler;
@@ -143,8 +164,10 @@
     this.changeIdProjectCache = changeIdProjectCache;
     this.revertRenameProject = revertRenameProject;
     this.sshHelper = sshHelper;
+    this.httpSession = httpSession;
     this.cfg = cfg;
     this.stepsPerformed = new ArrayList<>();
+    this.isReplica = isReplica;
   }
 
   private void assertNewNameNotNull(Input input) throws BadRequestException {
@@ -224,24 +247,28 @@
     try {
       fsRenameStep(oldProjectKey, newProjectKey, pm);
 
-      cacheRenameStep(rsrc.getNameKey(), newProjectKey);
+      if (!isReplica) {
 
-      List<Change.Id> updatedChangeIds = dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
+        cacheRenameStep(rsrc.getNameKey(), newProjectKey);
 
-      // if the DB update is successful, update the secondary index
-      indexRenameStep(updatedChangeIds, oldProjectKey, newProjectKey, pm);
+        List<Change.Id> updatedChangeIds =
+            dbRenameStep(changeIds, oldProjectKey, newProjectKey, pm);
 
-      // no need to revert this since newProjectKey will be removed from project cache before
-      lockUnlockProject.unlock(newProjectKey);
-      log.debug("Unlocked the repo {} after rename operation.", newProjectKey.get());
+        // if the DB update is successful, update the secondary index
+        indexRenameStep(updatedChangeIds, oldProjectKey, newProjectKey, pm);
 
-      // flush old changeId -> Project cache for given changeIds
-      changeIdProjectCache.invalidateAll(changeIds);
+        // no need to revert this since newProjectKey will be removed from project cache before
+        lockUnlockProject.unlock(newProjectKey);
+        log.debug("Unlocked the repo {} after rename operation.", newProjectKey.get());
 
-      pluginEvent.fire(pluginName, pluginName, oldProjectKey.get() + ":" + newProjectKey.get());
+        // flush old changeId -> Project cache for given changeIds
+        changeIdProjectCache.invalidateAll(changeIds);
 
-      // replicate rename-project operation to other replica instances
-      replicateRename(sshHelper, input, oldProjectKey, pm);
+        pluginEvent.fire(pluginName, pluginName, oldProjectKey.get() + ":" + newProjectKey.get());
+
+        // replicate rename-project operation to other replica instances
+        replicateRename(sshHelper, httpSession, input, oldProjectKey, pm);
+      }
     } catch (Exception e) {
       if (stepsPerformed.isEmpty()) {
         log.error("Renaming procedure failed. Exception caught: {}", e.toString());
@@ -342,6 +369,7 @@
 
   void replicateRename(
       SshHelper sshHelper,
+      HttpSession httpSession,
       Input input,
       Project.NameKey oldProjectKey,
       Optional<ProgressMonitor> opm) {
@@ -355,7 +383,7 @@
     int nbRetries = cfg.getRenameReplicationRetries();
 
     for (int i = 0; i < nbRetries && urls.size() > 0; ++i) {
-      urls = tryRenameReplication(urls, sshHelper, input, oldProjectKey);
+      urls = tryRenameReplication(urls, sshHelper, httpSession, input, oldProjectKey);
     }
     for (String url : urls) {
       log.error(
@@ -367,21 +395,57 @@
     }
   }
 
+  void sshReplicateRename(
+      SshHelper sshHelper, Input input, Project.NameKey oldProjectKey, String url)
+      throws RenameReplicationException, URISyntaxException, IOException {
+    OutputStream errStream = sshHelper.newErrorBufferStream();
+    sshHelper.executeRemoteSsh(
+        new URIish(url),
+        pluginName + " " + oldProjectKey.get() + " " + input.name + " --replication",
+        errStream);
+    String errorMessage = errStream.toString();
+    if (!errorMessage.isEmpty()) {
+      throw new RenameReplicationException(errorMessage);
+    }
+  }
+
+  void httpReplicateRename(
+      HttpSession httpSession, Input input, Project.NameKey oldProjectKey, String url)
+      throws AuthenticationException, IOException, RenameReplicationException {
+    String request =
+        Joiner.on("/")
+            .join(
+                url,
+                WITH_AUTHENTICATION,
+                PROJECTS_ENDPOINT,
+                oldProjectKey.get(),
+                pluginName + "~" + RENAME_ACTION);
+    HttpResponseHandler.HttpResult result = httpSession.post(request, input);
+    if (!result.isSuccessful()) {
+      throw new RenameReplicationException(
+          String.format("Unable to replicate rename to %s : %s", url, result.getMessage()));
+    }
+  }
+
   private Set<String> tryRenameReplication(
-      Set<String> replicas, SshHelper sshHelper, Input input, Project.NameKey oldProjectKey) {
+      Set<String> replicas,
+      SshHelper sshHelper,
+      HttpSession httpSession,
+      Input input,
+      Project.NameKey oldProjectKey) {
     Set<String> failedReplicas = new HashSet<>();
     for (String url : replicas) {
       try {
-        OutputStream errStream = sshHelper.newErrorBufferStream();
-        sshHelper.executeRemoteSsh(
-            new URIish(url),
-            pluginName + " " + oldProjectKey.get() + " " + input.name + " --replication",
-            errStream);
-        String errorMessage = errStream.toString();
-        if (!errorMessage.isEmpty()) {
-          throw new RenameReplicationException(errorMessage);
+        if (url.matches("http(.*)")) {
+          httpReplicateRename(httpSession, input, oldProjectKey, url);
         }
-      } catch (IOException | URISyntaxException | RenameReplicationException e) {
+        if (url.matches("ssh(.*)")) {
+          sshReplicateRename(sshHelper, input, oldProjectKey, url);
+        }
+      } catch (AuthenticationException
+          | IOException
+          | URISyntaxException
+          | RenameReplicationException e) {
         log.info(
             "Rescheduling a rename replication for retry for {} on project {}",
             url,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProjectFilter.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProjectFilter.java
new file mode 100644
index 0000000..f733885
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RenameProjectFilter.java
@@ -0,0 +1,167 @@
+// Copyright (C) 2023 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.renameproject;
+
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.restapi.*;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
+import com.google.gson.Gson;
+import com.google.gson.stream.JsonReader;
+import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.FilterChain;
+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.ConfigInvalidException;
+
+public class RenameProjectFilter extends AllRequestFilter {
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final Pattern projectNameInGerritUrl = Pattern.compile(".*/projects/([^/]+)/.*");
+
+  private final String pluginName;
+  private RenameProject renameProject;
+  private Gson gson;
+  private ProjectsCollection projectsCollection;
+
+  @Inject
+  public RenameProjectFilter(
+      @PluginName String pluginName,
+      ProjectsCollection projectsCollection,
+      RenameProject renameProject) {
+    this.pluginName = pluginName;
+    this.projectsCollection = projectsCollection;
+    this.renameProject = renameProject;
+    this.gson = OutputFormat.JSON.newGsonBuilder().create();
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
+      chain.doFilter(request, response);
+      return;
+    }
+
+    HttpServletResponse httpResponse = (HttpServletResponse) response;
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+    if (isRenameAction(httpRequest)) {
+      try {
+        writeResponse(httpResponse, renameProject(httpRequest));
+      } catch (RestApiException
+          | PermissionBackendException
+          | ConfigInvalidException
+          | RenameRevertException
+          | InterruptedException e) {
+        throw new ServletException(e);
+      }
+    } else {
+      chain.doFilter(request, response);
+    }
+  }
+
+  private <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
+      throws IOException {
+    String responseJson = gson.toJson(response);
+    if (response.statusCode() == SC_OK) {
+
+      httpResponse.setContentType("application/json");
+      httpResponse.setStatus(response.statusCode());
+      PrintWriter writer = httpResponse.getWriter();
+      writer.print(new String(RestApiServlet.JSON_MAGIC));
+      writer.print(responseJson);
+    } else {
+      httpResponse.sendError(response.statusCode(), responseJson);
+    }
+  }
+
+  private boolean isRenameAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().endsWith(String.format("/%s~rename", pluginName));
+  }
+
+  private Response<String> renameProject(HttpServletRequest httpRequest)
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException,
+          RenameRevertException, InterruptedException {
+    RenameProject.Input input = readJson(httpRequest, TypeLiteral.get(RenameProject.Input.class));
+    IdString id = getProjectName(httpRequest).get();
+
+    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
+
+    return (Response<String>) renameProject.apply(projectResource, input);
+  }
+
+  private Optional<IdString> getProjectName(HttpServletRequest req) {
+    return extractProjectName(req, projectNameInGerritUrl);
+  }
+
+  private Optional<IdString> extractProjectName(HttpServletRequest req, Pattern urlPattern) {
+    String path = req.getRequestURI();
+    Matcher projectGroupMatcher = urlPattern.matcher(path);
+
+    if (projectGroupMatcher.find()) {
+      return Optional.of(IdString.fromUrl(projectGroupMatcher.group(1)));
+    }
+
+    return Optional.empty();
+  }
+
+  private <T> T readJson(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
+      throws IOException, BadRequestException {
+
+    try (BufferedReader br = httpRequest.getReader();
+        JsonReader json = new JsonReader(br)) {
+      try {
+        json.setLenient(true);
+
+        try {
+          json.peek();
+        } catch (EOFException e) {
+          throw new BadRequestException("Expected JSON object", e);
+        }
+
+        return gson.fromJson(json, typeLiteral.getType());
+      } finally {
+        try {
+          // Reader.close won't consume the rest of the input. Explicitly consume the request
+          // body.
+          br.skip(Long.MAX_VALUE);
+        } catch (Exception e) {
+          // ignore, e.g. trying to consume the rest of the input may fail if the request was
+          // cancelled
+          logger.atFine().withCause(e).log("Exception during the parsing of the request json");
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/renameproject/RestRenameReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RestRenameReplicationModule.java
new file mode 100644
index 0000000..57e7f7e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/renameproject/RestRenameReplicationModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2023 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.renameproject;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+public class RestRenameReplicationModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class).in(Scopes.SINGLETON);
+    bind(HttpSession.class);
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e8ee6ae..2be087c 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -21,9 +21,11 @@
   [plugin "@PLUGIN@"]
     url = ssh://admin@mirror1.us.some.org
     url = ssh://mirror2.us.some.org:29418
+    url = http://localhost:8080
 ```
+The plugin supports both http and ssh replication.
 
-To specify the port number, it is required to put the `ssh://` prefix followed by hostname and then
+To configure ssh replication specify the port number, and it is required to put the `ssh://` prefix followed by hostname and then
 port number after `:`. It is also possible to specify the ssh user by passing `USERNAME@` as a
 prefix for hostname.
 
@@ -34,15 +36,22 @@
 ```
   sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
 ```
-
 @PLUGIN@ plugin uses the ssh rename command towards the replica(s) with `--replication` option to
 replicate the rename operation. It is possible to customize the parameters of the underlying ssh
 client doing these calls by specifying the following fields:
-
 * `sshCommandTimeout` : Timeout for SSH command execution. If 0, there is no timeout, and
-the client waits indefinitely. By default, 0.
+  the client waits indefinitely. By default, 0.
 * `sshConnectionTimeout` : Timeout for SSH connections in minutes. If 0, there is no timeout, and
-the client waits indefinitely. By default, 2 minutes.
+  the client waits indefinitely. By default, 2 minutes.
+
+To configure http replication, provide the correct url. To cpecify username and password for replication for rename, add
+password and username in gerrit.config or secure.config.
+for example:
+```
+  [plugin "@PLUGIN@"]
+    user = username
+    password = userpassword
+```
 
 Provides a configuration to customize the number of rename replication retries. By default, 3.
 
diff --git a/src/main/resources/Documentation/rest-api-rename.md b/src/main/resources/Documentation/rest-api-rename.md
index 70ecf15..b82549e 100644
--- a/src/main/resources/Documentation/rest-api-rename.md
+++ b/src/main/resources/Documentation/rest-api-rename.md
@@ -22,6 +22,8 @@
 ```
 to rename project-1 to project-2.
 
+The same request is used in replica mode.
+
 By default, if project-1 has more than 5000 changes, the rename procedure will be cancelled as it
 can take longer time and can degrade in performance in that time frame.
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
index f022707..54bf14a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/renameproject/RenameIT.java
@@ -15,6 +15,8 @@
 package com.googlesource.gerrit.plugins.renameproject;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.renameproject.RenameProject.RENAME_ACTION;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atMostOnce;
 import static org.mockito.Mockito.mock;
@@ -23,7 +25,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.google.common.base.Joiner;
 import com.google.common.cache.Cache;
+import com.google.common.net.MediaType;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
@@ -36,13 +40,24 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.renameproject.RenameProject.Input;
+import java.io.IOException;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
 import java.util.List;
 import java.util.Optional;
 import javax.inject.Named;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.transport.RemoteSession;
 import org.eclipse.jgit.transport.URIish;
@@ -51,7 +66,8 @@
 @TestPlugin(
     name = "rename-project",
     sysModule = "com.googlesource.gerrit.plugins.renameproject.Module",
-    sshModule = "com.googlesource.gerrit.plugins.renameproject.SshModule")
+    sshModule = "com.googlesource.gerrit.plugins.renameproject.SshModule",
+    httpModule = "com.googlesource.gerrit.plugins.renameproject.HttpModule")
 @UseSsh
 public class RenameIT extends LightweightPluginDaemonTest {
 
@@ -218,6 +234,7 @@
   public void testReplicateRenameSucceedsThenEnds() throws Exception {
     RenameProject renameProject = plugin.getSysInjector().getInstance(RenameProject.class);
     SshHelper sshHelper = mock(SshHelper.class);
+    HttpSession httpSession = mock(HttpSession.class);
     OutputStream errStream = mock(OutputStream.class);
     Input input = new Input();
     input.name = NEW_PROJECT_NAME;
@@ -227,7 +244,7 @@
     when(sshHelper.newErrorBufferStream()).thenReturn(errStream);
     when(errStream.toString()).thenReturn("");
 
-    renameProject.replicateRename(sshHelper, input, project, Optional.empty());
+    renameProject.replicateRename(sshHelper, httpSession, input, project, Optional.empty());
     verify(sshHelper, atMostOnce())
         .executeRemoteSsh(eq(new URIish(URL)), eq(expectedCommand), eq(errStream));
   }
@@ -239,13 +256,14 @@
     RenameProject renameProject = plugin.getSysInjector().getInstance(RenameProject.class);
     RemoteSession session = mock(RemoteSession.class);
     SshHelper sshHelper = mock(SshHelper.class);
+    HttpSession httpSession = mock(HttpSession.class);
     OutputStream errStream = mock(OutputStream.class);
     when(sshHelper.newErrorBufferStream()).thenReturn(errStream);
     URIish urish = new URIish(URL);
     Input input = new Input();
     input.name = NEW_PROJECT_NAME;
     when(sshHelper.connect(eq(urish))).thenReturn(session);
-    renameProject.replicateRename(sshHelper, input, project, Optional.empty());
+    renameProject.replicateRename(sshHelper, httpSession, input, project, Optional.empty());
     String expectedCommand =
         PLUGIN_NAME + " " + project.get() + " " + NEW_PROJECT_NAME + " " + REPLICATION_OPTION;
     verify(sshHelper, times(3)).executeRemoteSsh(eq(urish), eq(expectedCommand), eq(errStream));
@@ -256,6 +274,7 @@
   public void testReplicateRenameNeverCalled() throws Exception {
     RenameProject renameProject = plugin.getSysInjector().getInstance(RenameProject.class);
     SshHelper sshHelper = mock(SshHelper.class);
+    HttpSession httpSession = mock(HttpSession.class);
     OutputStream errStream = mock(OutputStream.class);
 
     Input input = new Input();
@@ -266,7 +285,7 @@
     when(sshHelper.newErrorBufferStream()).thenReturn(errStream);
     when(errStream.toString()).thenReturn("");
 
-    renameProject.replicateRename(sshHelper, input, project, Optional.empty());
+    renameProject.replicateRename(sshHelper, httpSession, input, project, Optional.empty());
     verify(sshHelper, never())
         .executeRemoteSsh(eq(new URIish(URL)), eq(expectedCommand), eq(errStream));
   }
@@ -322,6 +341,74 @@
     assertThat(queryProvider.get().byProject(Project.nameKey(NEW_PROJECT_NAME))).isNotEmpty();
   }
 
+  @Test
+  @UseLocalDisk
+  @GerritConfig(name = "container.replica", value = "true")
+  public void testRenameViaHttpInReplica() {
+    try {
+      assertThat(renameTest()).isTrue();
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    } catch (AuthenticationException e) {
+      System.out.println("auth");
+    }
+  }
+
+  @Test
+  @UseLocalDisk
+  //  @GerritConfig(name = "container.replica", value = "false")
+  @GerritConfig(name = "plugin.rename-project.url", value = "http://localhost:39959/")
+  public void replicateRenameViaHttp()
+      throws AuthenticationException, IOException, RenameReplicationException {
+    RenameProject renameProject = plugin.getSysInjector().getInstance(RenameProject.class);
+    String request =
+        Joiner.on("/")
+            .join(
+                "http://localhost:39959",
+                "a",
+                "projects",
+                project.get(),
+                PLUGIN_NAME + "~" + RENAME_ACTION);
+    HttpSession httpSession = mock(HttpSession.class);
+    HttpResponseHandler.HttpResult dummyResult = mock(HttpResponseHandler.HttpResult.class);
+    Input input = new Input();
+    input.name = NEW_PROJECT_NAME;
+    when(httpSession.post(any(), any())).thenReturn(dummyResult);
+    when(dummyResult.isSuccessful()).thenReturn(true);
+    renameProject.httpReplicateRename(httpSession, input, project, "http://localhost:39959");
+    verify(httpSession, times(1)).post(eq(request), eq(input));
+  }
+
+  private boolean renameTest() throws UnsupportedEncodingException, AuthenticationException {
+    String body = "{\"name\"=\"" + NEW_PROJECT_NAME + "\"}";
+    String endPoint = "a/projects/" + project.get() + "/" + PLUGIN_NAME + "~rename";
+    HttpPost putRequest = new HttpPost(canonicalWebUrl.get() + endPoint);
+
+    UsernamePasswordCredentials creds =
+        new UsernamePasswordCredentials(admin.username(), admin.httpPassword());
+    putRequest.addHeader(new BasicScheme().authenticate(creds, putRequest, null));
+    putRequest.setHeader("Accept", MediaType.ANY_TEXT_TYPE.toString());
+    putRequest.setHeader("Content-type", "application/json");
+    putRequest.setEntity(new StringEntity(body));
+    try {
+      executeRequest(putRequest);
+    } catch (RestApiException restApiException) {
+      return false;
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+    return true;
+  }
+
+  private void executeRequest(HttpRequestBase request) throws IOException, RestApiException {
+    try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
+      client.execute(request);
+    } catch (IOException e) {
+      e.printStackTrace();
+      throw e;
+    }
+  }
+
   private RestResponse renameProjectTo(String newName) throws Exception {
     requestScopeOperations.setApiUser(user.id());
     sender.clear();