remote: Add support for HTTP Basic Auth

Closes #4609.

PiperOrigin-RevId: 185032751
diff --git a/site/docs/remote-caching.md b/site/docs/remote-caching.md
index bfdf4e7..0d193dd 100644
--- a/site/docs/remote-caching.md
+++ b/site/docs/remote-caching.md
@@ -19,6 +19,7 @@
     * [Bazel Remote Cache](#bazel-remote-cache)
     * [Google Cloud Storage](#google-cloud-storage)
     * [Other servers](#other-servers)
+* [Authentication](#authentication)
 * [HTTP Caching Protocol](#http-caching-protocol)
 * [Run Bazel using the remote cache](#run-bazel-using-the-remote-cache)
     * [Read from and write to the remote cache](#read-from-and-write-to-the-remote-cache)
@@ -174,6 +175,14 @@
 backend. Users have reported success with caching backends such as [Hazelcast],
 [Apache httpd], and [AWS S3].
 
+## Authentication
+
+As of version 0.11.0 support for HTTP Basic Authentication was added to Bazel.
+You can pass a username and password to Bazel via the remote cache URL. The
+syntax is `https://username:password@hostname.com:port/path`. Please note that
+HTTP Basic Authentication transmits username and password in plaintext over the
+network and it's thus critical to always use it with HTTPS.
+
 ## HTTP Caching Protocol
 
 Bazel supports remote caching via HTTP/1.1. The protocol is conceptually simple:
diff --git a/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandler.java b/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandler.java
index 5b7df8a..fc8c14a 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandler.java
@@ -14,10 +14,13 @@
 package com.google.devtools.build.lib.remote.blobstore.http;
 
 import com.google.auth.Credentials;
+import com.google.common.base.Charsets;
+import com.google.common.io.BaseEncoding;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelOutboundHandler;
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpObject;
 import io.netty.handler.codec.http.HttpRequest;
 import java.io.IOException;
@@ -53,6 +56,12 @@
   }
 
   protected void addCredentialHeaders(HttpRequest request, URI uri) throws IOException {
+    String userInfo = uri.getUserInfo();
+    if (userInfo != null) {
+      String value = BaseEncoding.base64Url().encode(userInfo.getBytes(Charsets.UTF_8));
+      request.headers().set(HttpHeaderNames.AUTHORIZATION, "Basic " + value);
+      return;
+    }
     if (credentials == null || !credentials.hasRequestMetadata()) {
       return;
     }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandlerTest.java b/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandlerTest.java
new file mode 100644
index 0000000..bb4e226
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/AbstractHttpHandlerTest.java
@@ -0,0 +1,60 @@
+// Copyright 2018 The Bazel Authors. 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.
+
+package com.google.devtools.build.lib.remote.blobstore.http;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpRequest;
+import java.io.ByteArrayOutputStream;
+import java.net.URI;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Tests for {@link AbstractHttpHandlerTest}. */
+@RunWith(JUnit4.class)
+public abstract class AbstractHttpHandlerTest {
+
+  @Test
+  public void basicAuthShouldWork() throws Exception {
+    URI uri = new URI("http://user:password@does.not.exist/foo");
+    EmbeddedChannel ch = new EmbeddedChannel(new HttpDownloadHandler(null));
+    ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream());
+    DownloadCommand cmd = new DownloadCommand(uri, true, "abcdef", new ByteArrayOutputStream());
+    ChannelPromise writePromise = ch.newPromise();
+    ch.writeOneOutbound(cmd, writePromise);
+
+    HttpRequest request = ch.readOutbound();
+    assertThat(request.headers().get(HttpHeaderNames.AUTHORIZATION))
+        .isEqualTo("Basic dXNlcjpwYXNzd29yZA==");
+  }
+
+  @Test
+  public void basicAuthShouldNotEnabled() throws Exception {
+    URI uri = new URI("http://does.not.exist/foo");
+    EmbeddedChannel ch = new EmbeddedChannel(new HttpDownloadHandler(null));
+    ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream());
+    DownloadCommand cmd = new DownloadCommand(uri, true, "abcdef", new ByteArrayOutputStream());
+    ChannelPromise writePromise = ch.newPromise();
+    ch.writeOneOutbound(cmd, writePromise);
+
+    HttpRequest request = ch.readOutbound();
+    assertThat(request.headers().contains(HttpHeaderNames.AUTHORIZATION)).isFalse();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpDownloadHandlerTest.java b/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpDownloadHandlerTest.java
index da16587..ca544ee 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpDownloadHandlerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpDownloadHandlerTest.java
@@ -40,7 +40,7 @@
 
 /** Tests for {@link HttpDownloadHandler}. */
 @RunWith(JUnit4.class)
-public class HttpDownloadHandlerTest {
+public class HttpDownloadHandlerTest extends AbstractHttpHandlerTest {
 
   private static final URI CACHE_URI = URI.create("http://storage.googleapis.com:80/cache-bucket");
 
diff --git a/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpUploadHandlerTest.java b/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpUploadHandlerTest.java
index 9ab5650..27914da 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpUploadHandlerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/blobstore/http/HttpUploadHandlerTest.java
@@ -37,7 +37,7 @@
 
 /** Tests for {@link HttpUploadHandler}. */
 @RunWith(JUnit4.class)
-public class HttpUploadHandlerTest {
+public class HttpUploadHandlerTest extends AbstractHttpHandlerTest {
 
   private static final URI CACHE_URI = URI.create("http://storage.googleapis.com:80/cache-bucket");