AU: Support redirects.

BUG=5692
TEST=unit tests, gmerged on device, making sure update engine still works

Change-Id: If593f6efbd39452aa98b453f4d5489ce7d7d7fb9

Review URL: http://codereview.chromium.org/3161041
diff --git a/http_fetcher_unittest.cc b/http_fetcher_unittest.cc
index 17e359e..f7dad8e 100644
--- a/http_fetcher_unittest.cc
+++ b/http_fetcher_unittest.cc
@@ -3,12 +3,15 @@
 // found in the LICENSE file.
 
 #include <unistd.h>
+
 #include <string>
 #include <vector>
-#include <base/scoped_ptr.h>
-#include <glib.h>
-#include <gtest/gtest.h>
+
 #include "base/logging.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "glib.h"
+#include "gtest/gtest.h"
 #include "update_engine/libcurl_http_fetcher.h"
 #include "update_engine/mock_http_fetcher.h"
 
@@ -450,4 +453,91 @@
   g_main_loop_unref(loop);
 }
 
+namespace {
+const int kRedirectCodes[] = { 301, 302, 303, 307 };
+
+class RedirectHttpFetcherTestDelegate : public HttpFetcherDelegate {
+ public:
+  RedirectHttpFetcherTestDelegate(bool expected_successful)
+      : expected_successful_(expected_successful) {}
+  virtual void ReceivedBytes(HttpFetcher* fetcher,
+                             const char* bytes, int length) {
+    data.append(bytes, length);
+  }
+  virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
+    EXPECT_EQ(expected_successful_, successful);
+    g_main_loop_quit(loop_);
+  }
+  bool expected_successful_;
+  string data;
+  GMainLoop* loop_;
+};
+
+// RedirectTest takes ownership of |http_fetcher|.
+void RedirectTest(bool expected_successful,
+                  const string& url,
+                  HttpFetcher* http_fetcher) {
+  GMainLoop* loop = g_main_loop_new(g_main_context_default(), FALSE);
+  RedirectHttpFetcherTestDelegate delegate(expected_successful);
+  delegate.loop_ = loop;
+  scoped_ptr<HttpFetcher> fetcher(http_fetcher);
+  fetcher->set_delegate(&delegate);
+
+  StartTransferArgs start_xfer_args =
+      { fetcher.get(), LocalServerUrlForPath(url) };
+
+  g_timeout_add(0, StartTransfer, &start_xfer_args);
+  g_main_loop_run(loop);
+  if (expected_successful) {
+    // verify the data we get back
+    ASSERT_EQ(1000, delegate.data.size());
+    for (int i = 0; i < 1000; i += 10) {
+      // Assert so that we don't flood the screen w/ EXPECT errors on failure.
+      ASSERT_EQ(delegate.data.substr(i, 10), "abcdefghij");
+    }
+  }
+  g_main_loop_unref(loop);
+}
+}  // namespace {}
+
+TYPED_TEST(HttpFetcherTest, SimpleRedirectTest) {
+  if (this->IsMock())
+    return;
+  typename TestFixture::HttpServer server;
+  ASSERT_TRUE(server.started_);
+  for (size_t c = 0; c < arraysize(kRedirectCodes); ++c) {
+    const string url = base::StringPrintf("/redirect/%d/medium",
+                                          kRedirectCodes[c]);
+    RedirectTest(true, url, this->NewLargeFetcher());
+  }
+}
+
+TYPED_TEST(HttpFetcherTest, MaxRedirectTest) {
+  if (this->IsMock())
+    return;
+  typename TestFixture::HttpServer server;
+  ASSERT_TRUE(server.started_);
+  string url;
+  for (int r = 0; r < LibcurlHttpFetcher::kMaxRedirects; r++) {
+    url += base::StringPrintf("/redirect/%d",
+                              kRedirectCodes[r % arraysize(kRedirectCodes)]);
+  }
+  url += "/medium";
+  RedirectTest(true, url, this->NewLargeFetcher());
+}
+
+TYPED_TEST(HttpFetcherTest, BeyondMaxRedirectTest) {
+  if (this->IsMock())
+    return;
+  typename TestFixture::HttpServer server;
+  ASSERT_TRUE(server.started_);
+  string url;
+  for (int r = 0; r < LibcurlHttpFetcher::kMaxRedirects + 1; r++) {
+    url += base::StringPrintf("/redirect/%d",
+                              kRedirectCodes[r % arraysize(kRedirectCodes)]);
+  }
+  url += "/medium";
+  RedirectTest(false, url, this->NewLargeFetcher());
+}
+
 }  // namespace chromeos_update_engine
diff --git a/libcurl_http_fetcher.cc b/libcurl_http_fetcher.cc
index f636764..a07a825 100644
--- a/libcurl_http_fetcher.cc
+++ b/libcurl_http_fetcher.cc
@@ -61,6 +61,13 @@
   CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_LOW_SPEED_TIME, 3 * 60),
            CURLE_OK);
 
+  // By default, libcurl doesn't follow redirections. Allow up to
+  // |kMaxRedirects| redirections.
+  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_FOLLOWLOCATION, 1),
+           CURLE_OK);
+  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_MAXREDIRS, kMaxRedirects),
+           CURLE_OK);
+
   CHECK_EQ(curl_multi_add_handle(curl_multi_handle_, curl_handle_), CURLM_OK);
   transfer_in_progress_ = true;
 }
diff --git a/libcurl_http_fetcher.h b/libcurl_http_fetcher.h
index a4ed52f..8908638 100644
--- a/libcurl_http_fetcher.h
+++ b/libcurl_http_fetcher.h
@@ -20,6 +20,8 @@
 
 class LibcurlHttpFetcher : public HttpFetcher {
  public:
+  static const int kMaxRedirects = 10;
+
   LibcurlHttpFetcher()
       : curl_multi_handle_(NULL),
         curl_handle_(NULL),
diff --git a/test_http_server.cc b/test_http_server.cc
index 3c2ee54..9573c31 100644
--- a/test_http_server.cc
+++ b/test_http_server.cc
@@ -32,6 +32,7 @@
 
 struct HttpRequest {
   HttpRequest() : offset(0), return_code(200) {}
+  string host;
   string url;
   off_t offset;
   int return_code;
@@ -40,6 +41,7 @@
 namespace {
 const int kPort = 8080;  // hardcoded to 8080 for now
 const int kBigLength = 100000;
+const int kMediumLength = 1000;
 }
 
 bool ParseRequest(int fd, HttpRequest* request) {
@@ -65,6 +67,7 @@
   CHECK_NE(string::npos, url_end);
   string url = headers.substr(url_start, url_end - url_start);
   LOG(INFO) << "URL: " << url;
+  request->url = url;
 
   string::size_type range_start, range_end;
   if (headers.find("\r\nRange: ") == string::npos) {
@@ -81,7 +84,20 @@
     request->return_code = 206;  // Success for Range: request
     LOG(INFO) << "Offset: " << request->offset;
   }
-  request->url = url;
+
+  if (headers.find("\r\nHost: ") == string::npos) {
+    request->host = "";
+  } else {
+    string::size_type host_start =
+        headers.find("\r\nHost: ") + strlen("\r\nHost: ");
+    string::size_type host_end = headers.find('\r', host_start);
+    CHECK_NE(string::npos, host_end);
+    string host = headers.substr(host_start, host_end - host_start);
+
+    LOG(INFO) << "Host: " << host;
+    request->host = host;
+  }
+
   return true;
 }
 
@@ -128,8 +144,8 @@
   exit(0);
 }
 
-void HandleBig(int fd, const HttpRequest& request) {
-  const off_t full_length = kBigLength;
+void HandleBig(int fd, const HttpRequest& request, int big_length) {
+  const off_t full_length = big_length;
   WriteHeaders(fd, true, full_length, request.offset, request.return_code);
   const off_t content_length = full_length - request.offset;
   int i = request.offset;
@@ -173,6 +189,36 @@
   }
 }
 
+// Handles /redirect/<code>/<url> requests by returning the specified
+// redirect <code> with a location pointing to /<url>.
+void HandleRedirect(int fd, const HttpRequest& request) {
+  LOG(INFO) << "Redirecting...";
+  string url = request.url;
+  CHECK_EQ(0, url.find("/redirect/"));
+  url.erase(0, strlen("/redirect/"));
+  string::size_type url_start = url.find('/');
+  CHECK_NE(url_start, string::npos);
+  string code = url.substr(0, url_start);
+  url.erase(0, url_start);
+  url = "http://" + request.host + url;
+  string status;
+  if (code == "301") {
+    status = "Moved Permanently";
+  } else if (code == "302") {
+    status = "Found";
+  } else if (code == "303") {
+    status = "See Other";
+  } else if (code == "307") {
+    status = "Temporary Redirect";
+  } else {
+    CHECK(false) << "Unrecognized redirection code: " << code;
+  }
+  LOG(INFO) << "Code: " << code << " " << status;
+  LOG(INFO) << "New URL: " << url;
+  WriteString(fd, "HTTP/1.1 " + code + " " + status + "\r\n");
+  WriteString(fd, "Location: " + url + "\r\n");
+}
+
 void HandleDefault(int fd, const HttpRequest& request) {
   const string data("unhandled path");
   WriteHeaders(fd, true, data.size(), request.offset, request.return_code);
@@ -188,9 +234,13 @@
   if (request.url == "/quitquitquit")
     HandleQuitQuitQuit(fd);
   else if (request.url == "/big")
-    HandleBig(fd, request);
+    HandleBig(fd, request, kBigLength);
+  else if (request.url == "/medium")
+    HandleBig(fd, request, kMediumLength);
   else if (request.url == "/flaky")
     HandleFlaky(fd, request);
+  else if (request.url.find("/redirect/") == 0)
+    HandleRedirect(fd, request);
   else
     HandleDefault(fd, request);