Handle http <-> https redirects

Support all 301 and 302 redirect handling in bazel. Only support
absolute Location redirects (this is the spec, we can revisit if we
find a lot of servers are doing it wrong).

Fixes #959.

--
MOS_MIGRATED_REVID=115490327
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java
index dd19f83..3594767a 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/HttpDownloader.java
@@ -36,6 +36,7 @@
 import java.net.Authenticator;
 import java.net.HttpURLConnection;
 import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
 import java.net.PasswordAuthentication;
 import java.net.Proxy;
 import java.net.URL;
@@ -54,6 +55,7 @@
  * Helper class for downloading a file from a URL.
  */
 public class HttpDownloader {
+  private static final int MAX_REDIRECTS = 20;
   private static final int BUFFER_SIZE = 32 * 1024;
   private static final int KB = 1024;
   private static final String UNITS = " KMGTPEY";
@@ -238,16 +240,73 @@
     }
 
     public static HttpConnection createAndConnect(URL url) throws IOException {
-      HttpURLConnection connection = (HttpURLConnection) url.openConnection(
-          createProxyIfNeeded(url.getProtocol()));
-      connection.setInstanceFollowRedirects(true);
-      connection.connect();
-      if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
-        InputStream errorStream = connection.getErrorStream();
-        throw new IOException(connection.getResponseCode() + ": "
-            + new String(ByteStreams.toByteArray(errorStream), StandardCharsets.UTF_8));
+      int retries = MAX_REDIRECTS;
+      Proxy proxy = createProxyIfNeeded(url.getProtocol());
+      do {
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);
+        try {
+          connection.connect();
+        } catch (IllegalArgumentException e) {
+          throw new IOException("Failed to connect to " + url + " : " + e.getMessage(), e);
+        }
+
+        int statusCode = connection.getResponseCode();
+        switch (statusCode) {
+          case HttpURLConnection.HTTP_OK:
+            return new HttpConnection(connection.getInputStream(), parseContentLength(connection));
+          case HttpURLConnection.HTTP_MOVED_PERM:
+          case HttpURLConnection.HTTP_MOVED_TEMP:
+            url = tryGetLocation(statusCode, connection);
+            connection.disconnect();
+            break;
+          case -1:
+            throw new IOException("An HTTP error occured");
+          default:
+            throw new IOException(String.format("%s %s: %s",
+                connection.getResponseCode(),
+                connection.getResponseMessage(),
+                readBody(connection)));
+          }
+      } while (retries-- > 0);
+      throw new IOException("Maximum redirects (" + MAX_REDIRECTS + ") exceeded");
+    }
+
+    private static URL tryGetLocation(int statusCode, HttpURLConnection connection)
+        throws IOException {
+      String newLocation = connection.getHeaderField("Location");
+      if (newLocation == null) {
+        throw new IOException(
+            "Remote returned " + statusCode + " but did not return location header.");
       }
-      return new HttpConnection(connection.getInputStream(), parseContentLength(connection));
+
+      URL newUrl;
+      try {
+        newUrl = new URL(newLocation);
+      } catch (MalformedURLException e) {
+        throw new IOException("Remote returned invalid location header: " + newLocation);
+      }
+
+      String newProtocol = newUrl.getProtocol();
+      if (!("http".equals(newProtocol) || "https".equals(newProtocol))) {
+        throw new IOException(
+            "Remote returned invalid location header: " + newLocation);
+      }
+
+      return newUrl;
+    }
+
+    private static String readBody(HttpURLConnection connection) throws IOException {
+      InputStream errorStream = connection.getErrorStream();
+      if (errorStream != null) {
+        return new String(ByteStreams.toByteArray(errorStream), StandardCharsets.UTF_8);
+      }
+
+      InputStream responseStream = connection.getInputStream();
+      if (responseStream != null) {
+        return new String(ByteStreams.toByteArray(responseStream), StandardCharsets.UTF_8);
+      }
+
+      return null;
     }
   }
 
diff --git a/src/test/shell/bazel/external_integration_test.sh b/src/test/shell/bazel/external_integration_test.sh
index d65b35a..ce2980d 100755
--- a/src/test/shell/bazel/external_integration_test.sh
+++ b/src/test/shell/bazel/external_integration_test.sh
@@ -345,6 +345,31 @@
   expect_log "Tra-la!"
 }
 
+function test_http_to_https_redirect() {
+  http_response=$TEST_TMPDIR/http_response
+  cat > $http_response <<EOF
+HTTP/1.0 301 Moved Permantently
+Location: https://localhost:123456789/bad-port-shouldnt-work
+EOF
+  nc_port=$(pick_random_unused_tcp_port) || exit 1
+  nc_l $nc_port < $http_response &
+  nc_pid=$!
+
+  cd ${WORKSPACE_DIR}
+  cat > WORKSPACE <<EOF
+http_file(
+    name = 'toto',
+    url = 'http://localhost:$nc_port/toto',
+    sha256 = 'whatever'
+)
+EOF
+  bazel build @toto//file &> $TEST_log && fail "Expected run to fail"
+  kill_nc
+  # Observes that we tried to follow redirect, but failed due to ridiculous
+  # port.
+  expect_log "Failed to connect.*port out of range"
+}
+
 function test_http_404() {
   http_response=$TEST_TMPDIR/http_response
   cat > $http_response <<EOF
@@ -366,7 +391,7 @@
 EOF
   bazel build @toto//file &> $TEST_log && fail "Expected run to fail"
   kill_nc
-  expect_log "404: Help, I'm lost!"
+  expect_log "404 Not Found: Help, I'm lost!"
 }
 
 # Tests downloading a file and using it as a dependency.