Support HTTP authentication for replication

Gerrit now supports HTTP Basic and Digest authentication when
a remote http:// URL is configured for replication.  The password
can be supplied by secure.config.

Change-Id: Id1277ba8d9a8653f1b2f94d8b5fe94323623c5c5
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 84a4310..35fc3da 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1945,6 +1945,9 @@
 
 [sendemail]
   smtpPass = sp@m
+
+[remote "bar"]
+  password = s3kr3t
 ----
 
 File `etc/replication.config`
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
index 81fda98..53c69a5 100644
--- a/Documentation/config-replication.txt
+++ b/Documentation/config-replication.txt
@@ -194,6 +194,24 @@
 everything to all remotes.
 
 
+[[secure_config]]File `secure.config`
+-----------------------------------------------
+
+The optional file `'$site_path'/secure.config` is a Git-style config
+file that provides secure values that should not be world-readable,
+such as passwords. Passwords for HTTP remotes can be obtained from
+this file.
+
+[[remote.name.username]]remote.<name>.username::
++
+Username to use for HTTP authentication on this remote, if not given
+in the URL.
+
+[[remote.name.password]]remote.<name>.password::
++
+Password to use for HTTP authentication on this remote.
+
+
 [[ssh_config]]File `~/.ssh/config`
 ----------------------------------
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index df10c27..5426f34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.lifecycle.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AuthType;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.AnonymousUser;
@@ -35,12 +34,11 @@
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupCacheImpl;
-import com.google.gerrit.server.account.GroupInfoCacheFactory;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
+import com.google.gerrit.server.account.GroupInfoCacheFactory;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.git.ChangeMergeQueue;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -50,6 +48,7 @@
 import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.git.SecureCredentialsProvider;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.EmailSender;
@@ -68,16 +67,13 @@
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
-import com.google.inject.TypeLiteral;
 
 import org.apache.velocity.app.Velocity;
 import org.apache.velocity.runtime.RuntimeConstants;
-
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
 import java.util.Properties;
-import java.util.Set;
 
 
 /** Starts global state with standard dependencies. */
@@ -173,6 +169,7 @@
     bind(TransferConfig.class);
 
     bind(ReplicationQueue.class).to(PushReplication.class).in(SINGLETON);
+    factory(SecureCredentialsProvider.Factory.class);
     factory(PushAllProjectsOp.Factory.class);
 
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
index 049e0e0..a24d416 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.Project.NameKey;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.client.OrmException;
@@ -34,6 +34,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.FetchConnection;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
@@ -70,6 +71,7 @@
   private final SchemaFactory<ReviewDb> schema;
   private final PushReplication.ReplicationConfig pool;
   private final RemoteConfig config;
+  private final CredentialsProvider credentialsProvider;
 
   private final Set<String> delta = new HashSet<String>();
   private final Project.NameKey projectName;
@@ -88,11 +90,13 @@
   @Inject
   PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
       final PushReplication.ReplicationConfig p, final RemoteConfig c,
+      final SecureCredentialsProvider.Factory cpFactory,
       @Assisted final Project.NameKey d, @Assisted final URIish u) {
     repoManager = grm;
     schema = s;
     pool = p;
     config = c;
+    credentialsProvider = cpFactory.create(c.getName());
     projectName = d;
     uri = u;
   }
@@ -249,6 +253,7 @@
   private PushResult pushVia(final Transport tn) throws IOException,
       NotSupportedException, TransportException {
     tn.applyConfig(config);
+    tn.setCredentialsProvider(credentialsProvider);
 
     final List<RemoteRefUpdate> todo = generateUpdates(tn);
     if (todo.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
new file mode 100644
index 0000000..d51936a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2011 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.server.git;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+
+/** Looks up a remote's password in secure.config. */
+public class SecureCredentialsProvider extends CredentialsProvider {
+  public interface Factory {
+    SecureCredentialsProvider create(String remoteName);
+  }
+
+  private final String cfgUser;
+  private final String cfgPass;
+
+  @Inject
+  SecureCredentialsProvider(@GerritServerConfig Config cfg,
+      @Assisted String remoteName) {
+    cfgUser = cfg.getString("remote", remoteName, "username");
+    cfgPass = cfg.getString("remote", remoteName, "password");
+  }
+
+  @Override
+  public boolean isInteractive() {
+    return false;
+  }
+
+  @Override
+  public boolean supports(CredentialItem... items) {
+    for (CredentialItem i : items) {
+      if (i instanceof CredentialItem.Username) {
+        continue;
+      } else if (i instanceof CredentialItem.Password) {
+        continue;
+      } else {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean get(URIish uri, CredentialItem... items)
+      throws UnsupportedCredentialItem {
+    String username = uri.getUser();
+    if (username == null) {
+      username = cfgUser;
+    }
+    if (username == null) {
+      return false;
+    }
+
+    String password = uri.getPass();
+    if (password == null) {
+      password = cfgPass;
+    }
+    if (password == null) {
+      return false;
+    }
+
+    for (CredentialItem i : items) {
+      if (i instanceof CredentialItem.Username) {
+        ((CredentialItem.Username) i).setValue(username);
+      } else if (i instanceof CredentialItem.Password) {
+        ((CredentialItem.Password) i).setValue(password.toCharArray());
+      } else {
+        throw new UnsupportedCredentialItem(uri, i.getPromptText());
+      }
+    }
+    return true;
+  }
+}