Add support for Maven username & password authentication

Progress on issue #264.

RELNOTES: Maven servers that require username & password authentication are
now supported (see maven_server documentation).

--
MOS_MIGRATED_REVID=103583838
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
index db08bd9..a13a752 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Ascii;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
@@ -37,16 +38,21 @@
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
 
+import org.apache.maven.settings.Server;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactRequest;
 import org.eclipse.aether.resolution.ArtifactResolutionException;
 import org.eclipse.aether.resolution.ArtifactResult;
 
 import java.io.IOException;
+import java.util.Map;
 
 import javax.annotation.Nullable;
 
@@ -65,7 +71,7 @@
       return null;
     }
 
-    String url;
+    MavenServerValue serverValue;
     AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
     boolean hasRepository = mapper.has("repository", Type.STRING)
         && !mapper.get("repository", Type.STRING).isEmpty();
@@ -77,30 +83,28 @@
               + "'repository' and 'server', which are mutually exclusive options"),
           Transience.PERSISTENT);
     } else if (hasRepository) {
-      url = mapper.get("repository", Type.STRING);
+      serverValue = MavenServerValue.createFromUrl(mapper.get("repository", Type.STRING));
     } else {
       String serverName = DEFAULT_SERVER;
       if (mapper.has("server", Type.STRING) && !mapper.get("server", Type.STRING).isEmpty()) {
         serverName = mapper.get("server", Type.STRING);
       }
 
-      MavenServerValue mavenServerValue = (MavenServerValue) env.getValue(
-          MavenServerValue.key(serverName));
-      if (mavenServerValue == null) {
+      serverValue = (MavenServerValue) env.getValue(MavenServerValue.key(serverName));
+      if (serverValue == null) {
         return null;
       }
-      url = mavenServerValue.getUrl();
     }
 
-    MavenDownloader downloader = createMavenDownloader(mapper, url);
+    MavenDownloader downloader = createMavenDownloader(mapper, serverValue);
     return createOutputTree(downloader, env);
   }
 
   @VisibleForTesting
-  MavenDownloader createMavenDownloader(AttributeMap mapper, String url) {
+  MavenDownloader createMavenDownloader(AttributeMap mapper, MavenServerValue serverValue) {
     String name = mapper.getName();
     Path outputDirectory = getExternalRepositoryDirectory().getRelative(name);
-    return new MavenDownloader(name, mapper, outputDirectory, url);
+    return new MavenDownloader(name, mapper, outputDirectory, serverValue);
   }
 
   SkyValue createOutputTree(MavenDownloader downloader, Environment env)
@@ -159,9 +163,10 @@
     @Nullable
     private final String sha1;
     private final String url;
+    private final Server server;
 
     public MavenDownloader(
-        String name, AttributeMap mapper, Path outputDirectory, String url) {
+        String name, AttributeMap mapper, Path outputDirectory, MavenServerValue serverValue) {
       this.name = name;
       this.outputDirectory = outputDirectory;
 
@@ -173,7 +178,8 @@
             + mapper.get("version", Type.STRING);
       }
       this.sha1 = (mapper.has("sha1", Type.STRING)) ? mapper.get("sha1", Type.STRING) : null;
-      this.url = url;
+      this.url = serverValue.getUrl();
+      this.server = serverValue.getServer();
     }
 
     /**
@@ -198,7 +204,11 @@
       RepositorySystem system = connector.newRepositorySystem();
       RepositorySystemSession session = connector.newRepositorySystemSession(system);
 
-      RemoteRepository repository = new RemoteRepository.Builder(name, "default", url).build();
+
+      RemoteRepository repository = new RemoteRepository.Builder(
+          name, MavenServerValue.DEFAULT_ID, url)
+          .setAuthentication(new MavenAuthentication(server))
+          .build();
       ArtifactRequest artifactRequest = new ArtifactRequest();
       Artifact artifact;
       try {
@@ -230,4 +240,37 @@
     }
   }
 
+  private static class MavenAuthentication implements Authentication {
+
+    private final Map<String, String> authenticationInfo;
+
+    private MavenAuthentication(Server server) {
+      ImmutableMap.Builder builder = ImmutableMap.<String, String>builder();
+      // From https://maven.apache.org/settings.html: "If you use a private key to login to the
+      // server, make sure you omit the <password> element. Otherwise, the key will be ignored."
+      if (server.getPassword() != null) {
+        builder.put(AuthenticationContext.USERNAME, server.getUsername());
+        builder.put(AuthenticationContext.PASSWORD, server.getPassword());
+      } else if (server.getPrivateKey() != null) {
+        // getPrivateKey sounds like it returns the key, but it actually returns a path to it.
+        builder.put(AuthenticationContext.PRIVATE_KEY_PATH, server.getPrivateKey());
+        builder.put(AuthenticationContext.PRIVATE_KEY_PASSPHRASE, server.getPassphrase());
+      }
+      authenticationInfo = builder.build();
+    }
+
+    @Override
+    public void fill(
+        AuthenticationContext authenticationContext, String s, Map<String, String> map) {
+      for (Map.Entry<String, String> entry : authenticationInfo.entrySet()) {
+        authenticationContext.put(entry.getKey(), entry.getValue());
+      }
+    }
+
+    @Override
+    public void digest(AuthenticationDigest authenticationDigest) {
+      // No-op.
+    }
+  }
+
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java
index 6dcfc6c..89b8199 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.bazel.repository;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.bazel.rules.workspace.MavenServerRule;
@@ -37,9 +39,9 @@
 import org.apache.maven.settings.building.SettingsBuildingException;
 import org.apache.maven.settings.building.SettingsBuildingResult;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Map;
 
 import javax.annotation.Nullable;
 
@@ -49,6 +51,9 @@
 public class MavenServerFunction extends RepositoryFunction {
   public static final SkyFunctionName NAME = SkyFunctionName.create("MAVEN_SERVER_FUNCTION");
 
+  private static final String USER_KEY = "user";
+  private static final String SYSTEM_KEY = "system";
+
   public MavenServerFunction(BlazeDirectories directories) {
     setDirectories(directories);
   }
@@ -58,48 +63,75 @@
   public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctionException {
     String repository = skyKey.argument().toString();
     Package externalPackage = RepositoryFunction.getExternalPackage(env);
+    if (externalPackage == null) {
+      return null;
+    }
     Rule repositoryRule = externalPackage.getRule(repository);
 
+    String serverName;
+    String url;
+    Map<String, FileValue> settingsFiles;
     boolean foundRepoRule = repositoryRule != null
         && repositoryRule.getRuleClass().equals(MavenServerRule.NAME);
     if (!foundRepoRule) {
       if (repository.equals(MavenServerValue.DEFAULT_ID)) {
-        // The default repository is being used and the WORKSPACE is not overriding the default.
-        return new MavenServerValue();
+        settingsFiles = getDefaultSettingsFile(env);
+        serverName = MavenServerValue.DEFAULT_ID;
+        url = MavenConnector.getMavenCentralRemote().getUrl();
+      } else {
+        throw new RepositoryFunctionException(
+            new IOException("Could not find maven repository " + repository),
+            Transience.TRANSIENT);
       }
-      throw new RepositoryFunctionException(
-          new IOException("Could not find maven repository " + repository), Transience.TRANSIENT);
-    }
+    } else {
+      AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(repositoryRule);
+      serverName = repositoryRule.getName();
+      url = mapper.get("url", Type.STRING);
+      if (!mapper.has("settings_file", Type.STRING)
+          || mapper.get("settings_file", Type.STRING).isEmpty()) {
+        settingsFiles = getDefaultSettingsFile(env);
+      } else {
+        PathFragment settingsFilePath = new PathFragment(mapper.get("settings_file", Type.STRING));
+        RootedPath settingsPath = RootedPath.toRootedPath(
+            getWorkspace().getRelative(settingsFilePath), PathFragment.EMPTY_FRAGMENT);
+        FileValue fileValue = (FileValue) env.getValue(FileValue.key(settingsPath));
+        if (fileValue == null) {
+          return null;
+        }
 
-    AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(repositoryRule);
-    String serverName = repositoryRule.getName();
-    String url = mapper.get("url", Type.STRING);
-    if (!mapper.has("settings_file", Type.STRING)
-        || mapper.get("settings_file", Type.STRING).isEmpty()) {
-      return new MavenServerValue(serverName, url, new Server());
+        if (!fileValue.exists()) {
+          throw new RepositoryFunctionException(
+              new IOException("Could not find settings file " + settingsPath),
+              Transience.TRANSIENT);
+        }
+        settingsFiles = ImmutableMap.<String, FileValue>builder().put(
+            USER_KEY, fileValue).build();
+      }
     }
-    PathFragment settingsFilePath = new PathFragment(mapper.get("settings_file", Type.STRING));
-    RootedPath settingsPath = RootedPath.toRootedPath(
-        getWorkspace().getRelative(settingsFilePath), PathFragment.EMPTY_FRAGMENT);
-    FileValue settingsFile = (FileValue) env.getValue(FileValue.key(settingsPath));
-    if (settingsFile == null) {
+    if (settingsFiles == null) {
       return null;
     }
 
-    if (!settingsFile.exists()) {
-      throw new RepositoryFunctionException(
-          new IOException("Could not find settings file " + settingsPath), Transience.TRANSIENT);
+    if (settingsFiles.isEmpty()) {
+      return new MavenServerValue(serverName, url, new Server());
     }
 
     DefaultSettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
-    request.setUserSettingsFile(new File(settingsFile.realRootedPath().asPath().toString()));
+    if (settingsFiles.containsKey(SYSTEM_KEY)) {
+      request.setGlobalSettingsFile(
+          settingsFiles.get(SYSTEM_KEY).realRootedPath().asPath().getPathFile());
+    }
+    if (settingsFiles.containsKey(USER_KEY)) {
+      request.setUserSettingsFile(
+          settingsFiles.get(USER_KEY).realRootedPath().asPath().getPathFile());
+    }
     DefaultSettingsBuilder builder = (new DefaultSettingsBuilderFactory()).newInstance();
     SettingsBuildingResult result;
     try {
       result = builder.build(request);
     } catch (SettingsBuildingException e) {
       throw new RepositoryFunctionException(
-          new IOException("Error parsing settings file " + settingsFile + ": " + e.getMessage()),
+          new IOException("Error parsing settings files: " + e.getMessage()),
           Transience.TRANSIENT);
     }
     if (!result.getProblems().isEmpty()) {
@@ -108,11 +140,53 @@
               + Arrays.toString(result.getProblems().toArray())), Transience.PERSISTENT);
     }
     Settings settings = result.getEffectiveSettings();
-    Server server = settings.getServer(mapper.getName());
+    Server server = settings.getServer(serverName);
     server = server == null ? new Server() : server;
     return new MavenServerValue(serverName, url, server);
   }
 
+  private Map<String, FileValue> getDefaultSettingsFile(Environment env) {
+    // The system settings file is at $M2_HOME/conf/settings.xml.
+    String m2Home = System.getenv("M2_HOME");
+    ImmutableList.Builder<SkyKey> settingsFilesBuilder = ImmutableList.builder();
+    SkyKey systemKey = null;
+    if (m2Home != null) {
+      PathFragment mavenInstallSettings = new PathFragment(m2Home).getRelative("conf/settings.xml");
+      systemKey = FileValue.key(
+          RootedPath.toRootedPath(getWorkspace().getRelative(mavenInstallSettings),
+              PathFragment.EMPTY_FRAGMENT));
+      settingsFilesBuilder.add(systemKey);
+    }
+
+    // The user settings file is at $HOME/.m2/settings.xml.
+    String userHome = System.getenv("HOME");
+    SkyKey userKey = null;
+    if (userHome != null) {
+      PathFragment userSettings = new PathFragment(userHome).getRelative(".m2/settings.xml");
+      userKey = FileValue.key(RootedPath.toRootedPath(getWorkspace().getRelative(userSettings),
+          PathFragment.EMPTY_FRAGMENT));
+      settingsFilesBuilder.add(userKey);
+    }
+
+    ImmutableList settingsFiles = settingsFilesBuilder.build();
+    if (settingsFiles.isEmpty()) {
+      return ImmutableMap.of();
+    }
+    Map<SkyKey, SkyValue> values = env.getValues(settingsFilesBuilder.build());
+    ImmutableMap.Builder<String, FileValue> settingsBuilder = ImmutableMap.builder();
+    for (Map.Entry<SkyKey, SkyValue> entry : values.entrySet()) {
+      if (entry.getValue() == null) {
+        return null;
+      }
+      if (systemKey != null && systemKey.equals(entry.getKey())) {
+        settingsBuilder.put(SYSTEM_KEY, (FileValue) entry.getValue());
+      } else if (userKey != null && userKey.equals(entry.getKey())) {
+        settingsBuilder.put(USER_KEY, (FileValue) entry.getValue());
+      }
+    }
+    return settingsBuilder.build();
+  }
+
   @Override
   public SkyFunctionName getSkyFunctionName() {
     return NAME;
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java
index 78f2e26..22254b7 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java
@@ -35,6 +35,10 @@
     return new SkyKey(MavenServerFunction.NAME, serverName);
   }
 
+  public static MavenServerValue createFromUrl(String url) {
+    return new MavenServerValue(DEFAULT_ID, url, new Server());
+  }
+
   public MavenServerValue() {
     id = DEFAULT_ID;
     url = MavenConnector.getMavenCentralRemote().getUrl();
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java
index e710f9b..f08ec76 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java
@@ -42,7 +42,9 @@
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
         .add(attr("url", Type.STRING))
         /* <!-- #BLAZE_RULE(maven_server).ATTRIBUTE(settings_file) -->
-        A path to a settings.xml file.
+        A path to a settings.xml file.  Used for testing. If unspecified, this defaults to using
+        <code>$M2_HOME/conf/settings.xml</code> for the global settings and
+        <code>$HOME/.m2/settings.xml</code> for the user settings.
         ${SYNOPSIS}
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
         .add(attr("settings_file", Type.STRING))
@@ -71,4 +73,33 @@
 <p>This is a combination of a &lt;repository&gt; definition from a pom.xml file and a
 &lt;server&lt; definition from a settings.xml file.</p>
 
+<h4>Using <code>maven_server</code></h4>
+
+<p><code>maven_jar</code> rules can specify the name of a <code>maven_server</code> in their
+<code>server</code> field. For example, suppose we have the following WORKSPACE file:</p>
+
+<pre>
+maven_jar(
+    name = "junit",
+    artifact = "junit:junit-dep:4.10",
+    server = "my-server",
+)
+
+maven_server(
+    name = "my-server",
+    url = "http://intranet.mycorp.net",
+)
+</pre>
+
+This specifies that junit should be downloaded from http://intranet.mycorp.net using the
+authentication information found in ~/.m2/settings.xml (specifically, the settings
+for the server with the id <code>my-server</code>).
+
+<h4>Specifying a default server</h4>
+
+<p>If you create a <code>maven_server</code> with the <code>name</code> "default" it will be
+used for any <code>maven_jar</code> that does not specify a <code>server</code> nor
+<code>repository</code>. If there is no <code>maven_server</code> named default, the
+default will be fetching from Maven Central with no authentication enabled.</p>
+
 <!-- #END_BLAZE_RULE -->*/
diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD
index 4df03b7..7ce57f56 100644
--- a/src/test/shell/bazel/BUILD
+++ b/src/test/shell/bazel/BUILD
@@ -41,6 +41,7 @@
         "remote_helpers.sh",
         "test-setup.sh",
         "testenv.sh",
+        "testing_server.py",
         ":langtools-copy",
         ":objc-deps",
         "//examples:srcs",
diff --git a/src/test/shell/bazel/maven_test.sh b/src/test/shell/bazel/maven_test.sh
index 57f20d2..1490385 100755
--- a/src/test/shell/bazel/maven_test.sh
+++ b/src/test/shell/bazel/maven_test.sh
@@ -52,22 +52,19 @@
 
 function test_maven_jar() {
   setup_zoo
-  serve_jar
+  serve_artifact com.example.carnivore carnivore 1.23
 
   cat > WORKSPACE <<EOF
 maven_jar(
     name = 'endangered',
     artifact = "com.example.carnivore:carnivore:1.23",
-    repository = 'http://localhost:$nc_port/',
+    repository = 'http://localhost:$fileserver_port/',
     sha1 = '$sha1',
 )
 bind(name = 'mongoose', actual = '@endangered//jar')
 EOF
 
-  bazel fetch //zoo:ball-pit || fail "Fetch failed"
   bazel run //zoo:ball-pit >& $TEST_log || fail "Expected run to succeed"
-  kill_nc
-  assert_contains "GET /com/example/carnivore/carnivore/1.23/carnivore-1.23.jar" $nc_log
   expect_log "Tra-la!"
 }
 
@@ -219,4 +216,57 @@
   expect_log "no such package '@x//'"
 }
 
+function test_auth() {
+  startup_auth_server
+  create_artifact thing amabop 1.9
+  cat > WORKSPACE <<EOF
+maven_server(
+    name = "x",
+    url = "http://localhost:$fileserver_port/",
+    settings_file = "settings.xml",
+)
+maven_jar(
+    name = "good-auth",
+    artifact = "thing:amabop:1.9",
+    server = "x",
+)
+
+maven_server(
+    name = "y",
+    url = "http://localhost:$fileserver_port/",
+    settings_file = "settings.xml",
+)
+maven_jar(
+    name = "bad-auth",
+    artifact = "thing:amabop:1.9",
+    server = "y",
+)
+EOF
+
+  cat > settings.xml <<EOF
+<settings>
+  <servers>
+    <server>
+      <id>x</id>
+      <username>foo</username>
+      <password>bar</password>
+    </server>
+    <server>
+      <id>y</id>
+      <username>foo</username>
+      <password>baz</password>
+    </server>
+  </servers>
+</settings>
+EOF
+
+  bazel build @good-auth//jar &> $TEST_log \
+    || fail "Expected correct password to work"
+  expect_log "Target @good-auth//jar:jar up-to-date"
+
+  bazel build @bad-auth//jar &> $TEST_log \
+    && fail "Expected incorrect password to fail"
+  expect_log "Unauthorized (401)"
+}
+
 run_suite "maven tests"
diff --git a/src/test/shell/bazel/remote_helpers.sh b/src/test/shell/bazel/remote_helpers.sh
index 28d4b14..091a9b7 100755
--- a/src/test/shell/bazel/remote_helpers.sh
+++ b/src/test/shell/bazel/remote_helpers.sh
@@ -57,7 +57,7 @@
 function make_test_jar() {
   pkg_dir=$TEST_TMPDIR/carnivore
   rm -fr $pkg_dir
-  mkdir $pkg_dir
+  mkdir -p $pkg_dir
   cat > $pkg_dir/Mongoose.java <<EOF
 package carnivore;
 public class Mongoose {
@@ -108,28 +108,39 @@
 }
 
 
-function serve_artifact() {
-  startup_server $PWD
+function create_artifact() {
   local group_id=$1
   local artifact_id=$2
   local version=$3
   make_test_jar
-  maven_path=$fileserver_root/$(echo $group_id | sed 's/\./\//g')/$artifact_id/$version
+  maven_path=$PWD/$(echo $group_id | sed 's/\./\//g')/$artifact_id/$version
   mkdir -p $maven_path
   openssl sha1 $test_jar > $maven_path/$artifact_id-$version.jar.sha1
   mv $test_jar $maven_path/$artifact_id-$version.jar
 }
 
+function serve_artifact() {
+  startup_server $PWD
+  create_artifact $1 $2 $3
+}
+
 function startup_server() {
   fileserver_root=$1
   cd $fileserver_root
   fileserver_port=$(pick_random_unused_tcp_port) || exit 1
-  python -m SimpleHTTPServer $fileserver_port &
+  python $python_server --port=$fileserver_port &
   fileserver_pid=$!
   wait_for_server_startup
   cd -
 }
 
+function startup_auth_server() {
+  fileserver_port=$(pick_random_unused_tcp_port) || exit 1
+  python $python_server --port=$fileserver_port --auth=basic &
+  fileserver_pid=$!
+  wait_for_server_startup
+}
+
 function shutdown_server() {
   # Try to kill nc, otherwise the test will time out if Bazel has a bug and
   # didn't make a request to it.
diff --git a/src/test/shell/bazel/testenv.sh b/src/test/shell/bazel/testenv.sh
index 8a62fea..0bd4ac2 100755
--- a/src/test/shell/bazel/testenv.sh
+++ b/src/test/shell/bazel/testenv.sh
@@ -66,6 +66,7 @@
 
 # Test data
 testdata_path=${TEST_SRCDIR}/src/test/shell/bazel/testdata
+python_server="${TEST_SRCDIR}/src/test/shell/bazel/testing_server.py"
 
 # Third-party
 PLATFORM="$(uname -s | tr 'A-Z' 'a-z')"
diff --git a/src/test/shell/bazel/testing_server.py b/src/test/shell/bazel/testing_server.py
new file mode 100644
index 0000000..cd12657
--- /dev/null
+++ b/src/test/shell/bazel/testing_server.py
@@ -0,0 +1,87 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""An HTTP server to use for external repository integration tests."""
+
+import base64
+from BaseHTTPServer import BaseHTTPRequestHandler
+import getopt
+import os
+import SocketServer
+import sys
+
+auth = None
+
+
+class Handler(BaseHTTPRequestHandler):
+  """Handlers for testing HTTP server."""
+
+  def do_HEAD(self):  # pylint: disable=invalid-name
+    self.send_response(200)
+    self.send_header('Content-type', 'text/html')
+    self.end_headers()
+
+  def do_AUTHHEAD(self):  # pylint: disable=invalid-name
+    self.send_response(401)
+    self.send_header('WWW-Authenticate', 'Basic realm=\"Bazel\"')
+    self.send_header('Content-type', 'text/html')
+    self.end_headers()
+
+  def do_GET(self):  # pylint: disable=invalid-name
+    if auth is None:
+      self.do_HEAD()
+      self.serve_file()
+      return
+
+    foo_bar = base64.b64encode('foo:bar')
+
+    if self.headers.getheader('Authorization') is None:
+      self.do_AUTHHEAD()
+      self.wfile.write('Login required.')
+    elif self.headers.getheader('Authorization') == 'Basic %s' % foo_bar:
+      self.do_HEAD()
+      try:
+        self.wfile.write(self.headers.getheader('Authorization'))
+        self.serve_file()
+      except IOError:
+        self.wfile.write('Authorized.')
+    else:
+      self.do_AUTHHEAD()
+      self.wfile.write(self.headers.getheader('Authorization'))
+      self.wfile.write('not authenticated')
+
+  def serve_file(self):
+    file_to_serve = open(str(os.getcwd()) + self.path)
+    self.wfile.write(file_to_serve.read())
+
+
+if __name__ == '__main__':
+  try:
+    opts, args = getopt.getopt(sys.argv[1:], 'p:a:', ['port=', 'auth='])
+  except getopt.GetoptError:
+    print 'Error parsing args'
+    sys.exit(1)
+
+  port = 12345
+  for o, a in opts:
+    if o in ('-p', '--port'):
+      port = int(a)
+    if o in ('-a', '--auth'):
+      auth = a
+  httpd = SocketServer.TCPServer(('', port), Handler)
+  try:
+    print 'Serving forever on %d.' % port
+    httpd.serve_forever()
+  except:  # pylint: disable=bare-except
+    print 'Goodbye.'