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.