buffet: reworked http transport to prepare for unit testing

Changed the way HTTP transport classes are implemented. Now
the Transport class is a very simple factory class that just
creates an appropriate instance of http::Connection object.
http::Connection is a thin layer wrapper around underlying
transport library, such as libcurl.

Also, the Transport class is now stateless and can be used
to initiate multiple HTTP connections.

Majority of HTTP processing is done in http::Request and
http::Response classes which are not dependent on the underlying
transport.

The HTTP utility functions now take the Transport class as
a parameter to facilitate unit tesing.

Also added a stub http_utils_unittest.cc to be populated
with actual tests when the fake HTTP transport is implemented.

BUG=chromium:364733
TEST=Unit tests pass.

Change-Id: If506854d274f725bbc2d6f765f19344d8697a239
Reviewed-on: https://chromium-review.googlesource.com/196153
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/http_transport_curl.cc b/buffet/http_transport_curl.cc
index be204d9..8c4b946 100644
--- a/buffet/http_transport_curl.cc
+++ b/buffet/http_transport_curl.cc
@@ -4,317 +4,73 @@
 
 #include "buffet/http_transport_curl.h"
 
-#define __STDC_FORMAT_MACROS
-#include <inttypes.h>
-#include <string.h>
 #include <base/logging.h>
 
-#include "buffet/mime_utils.h"
-#include "buffet/string_utils.h"
-#include "buffet/map_utils.h"
+#include "buffet/http_connection_curl.h"
+#include "buffet/http_request.h"
 
 using namespace chromeos;
 using namespace chromeos::http::curl;
 
-#define VERBOSE_CURL 0  // Set to 1 to log advanced debugging info for CURL
-
-#if VERBOSE_CURL
-static int curl_trace(CURL *handle, curl_infotype type,
-                      char *data, size_t size, void *userp) {
-  std::string msg(data, size);
-
-  switch (type) {
-  case CURLINFO_TEXT:
-    LOG(INFO) << "== Info: " << msg;
-    break;
-  case CURLINFO_HEADER_OUT:
-    LOG(INFO) << "=> Send headers:\n" << msg;
-    break;
-  case CURLINFO_DATA_OUT:
-    LOG(INFO) << "=> Send data:\n" << msg;
-    break;
-  case CURLINFO_SSL_DATA_OUT:
-    LOG(INFO) << "=> Send SSL data" << msg;
-    break;
-  case CURLINFO_HEADER_IN:
-    LOG(INFO) << "<= Recv header: " << msg;
-    break;
-  case CURLINFO_DATA_IN:
-    LOG(INFO) << "<= Recv data:\n" << msg;
-    break;
-  case CURLINFO_SSL_DATA_IN:
-    LOG(INFO) << "<= Recv SSL data" << msg;
-    break;
-  default:
-    break;
-  }
-  return 0;
-}
-#endif
-
-Transport::Transport(const std::string& url, const char* method) :
-    request_url_(url),
-    method_(method ? method : request_type::kGet) {
-  stage_ = Stage::initialized;
+Transport::Transport() {
+  VLOG(1) << "curl::Transport created";
 }
 
 Transport::~Transport() {
-  Close();
+  VLOG(1) << "curl::Transport destroyed";
 }
 
-void Transport::AddRange(int64_t bytes) {
-  if (bytes < 0) {
-    ranges_.emplace_back(Transport::range_value_omitted, -bytes);
-  } else {
-    ranges_.emplace_back(bytes, Transport::range_value_omitted);
-  }
-}
-
-void Transport::AddRange(uint64_t fromByte, uint64_t toByte) {
-  ranges_.emplace_back(fromByte, toByte);
-}
-
-std::string Transport::GetAccept() const {
-  return accept_;
-}
-
-chromeos::http::HeaderList Transport::GetHeaders() const {
-  chromeos::http::HeaderList headers = MapToVector(headers_);
-  std::vector<std::string> ranges;
-  if (method_ != request_type::kHead) {
-    ranges.reserve(ranges_.size());
-    for (auto p : ranges_) {
-      if (p.first != range_value_omitted || p.second != range_value_omitted) {
-        std::string range;
-        if (p.first != range_value_omitted) {
-          range = std::to_string(p.first);
-        }
-        range += '-';
-        if (p.second != range_value_omitted) {
-          range += std::to_string(p.second);
-        }
-        ranges.push_back(range);
-      }
-    }
-  }
-  if (!ranges.empty())
-    headers.emplace_back(request_header::kRange,
-                         "bytes=" + string_utils::Join(',', ranges));
-
-  headers.emplace_back(request_header::kAccept, GetAccept());
-
-  return headers;
-}
-
-void Transport::AddHeader(const char* header, const char* value) {
-  headers_[header] = value;
-}
-
-void Transport::RemoveHeader(const char* header) {
-  AddHeader(header, "");
-}
-
-bool Transport::AddRequestBody(const void* data, size_t size) {
-  if (size == 0)
-    return true;
-
-  if (data == nullptr) {
-    LOG(ERROR) << "Invalid request body data pointer";
-    return false;
-  }
-
-  const unsigned char* data_ptr = reinterpret_cast<const unsigned char*>(data);
-  request_data_.insert(request_data_.end(), data_ptr, data_ptr + size);
-  return true;
-}
-
-bool Transport::Perform() {
-  if (stage_ != Stage::initialized) {
-    LOG(ERROR) << "Cannot call Perform() on unintialized transport object";
-    return false;
-  }
-
-  curl_handle_ = curl_easy_init();
-  if (!curl_handle_) {
+std::unique_ptr<http::Connection> Transport::CreateConnection(
+    std::shared_ptr<http::Transport> transport,
+    const std::string& url,
+    const std::string& method,
+    const HeaderList& headers,
+    const std::string& user_agent,
+    const std::string& referer,
+    std::string* error_msg) {
+  CURL* curl_handle = curl_easy_init();
+  if (!curl_handle) {
     LOG(ERROR) << "Failed to initialize CURL";
-    return false;
+    if (error_msg)
+      *error_msg = "Failed to initialize CURL";
+    return std::unique_ptr<http::Connection>();
   }
 
-  LOG(INFO) << "Sending a " << method_ << " request to " << request_url_;
-  curl_easy_setopt(curl_handle_, CURLOPT_URL, request_url_.c_str());
+  LOG(INFO) << "Sending a " << method << " request to " << url;
+  curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
 
-#if VERBOSE_CURL
-  curl_easy_setopt(curl_handle_, CURLOPT_DEBUGFUNCTION, curl_trace);
-  curl_easy_setopt(curl_handle_, CURLOPT_VERBOSE, 1L);
-#endif
-
-  if (!user_agent_.empty()) {
-    curl_easy_setopt(curl_handle_,
-                     CURLOPT_USERAGENT, user_agent_.c_str());
+  if (!user_agent.empty()) {
+    curl_easy_setopt(curl_handle,
+                     CURLOPT_USERAGENT, user_agent.c_str());
   }
 
-  if (!referer_.empty()) {
-    curl_easy_setopt(curl_handle_,
-                     CURLOPT_REFERER, referer_.c_str());
+  if (!referer.empty()) {
+    curl_easy_setopt(curl_handle,
+                     CURLOPT_REFERER, referer.c_str());
   }
 
   // Setup HTTP request method and optional request body.
-  if (method_ == request_type::kGet) {
-    curl_easy_setopt(curl_handle_, CURLOPT_HTTPGET, 1L);
-  } else if (method_ == request_type::kHead) {
-    curl_easy_setopt(curl_handle_, CURLOPT_NOBODY, 1L);
-  } else if (method_ == request_type::kPut) {
-    curl_easy_setopt(curl_handle_, CURLOPT_UPLOAD, 1L);
-    curl_easy_setopt(curl_handle_, CURLOPT_INFILESIZE_LARGE,
-                     curl_off_t(request_data_.size()));
-    curl_easy_setopt(curl_handle_,
-                     CURLOPT_READFUNCTION, &Transport::read_callback);
-    curl_easy_setopt(curl_handle_, CURLOPT_READDATA, this);
+  if (method ==  request_type::kGet) {
+    curl_easy_setopt(curl_handle, CURLOPT_HTTPGET, 1L);
+  } else if (method == request_type::kHead) {
+    curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 1L);
+  } else if (method == request_type::kPut) {
+    curl_easy_setopt(curl_handle, CURLOPT_UPLOAD, 1L);
   } else {
     // POST and custom request methods
-    curl_easy_setopt(curl_handle_, CURLOPT_POST, 1L);
-    curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDS, nullptr);
-    if (!request_data_.empty()) {
-      curl_easy_setopt(curl_handle_,
-                       CURLOPT_READFUNCTION, &Transport::read_callback);
-      curl_easy_setopt(curl_handle_, CURLOPT_READDATA, this);
-    }
-    curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDSIZE_LARGE,
-                     curl_off_t(request_data_.size()));
-    if (method_ != request_type::kPost)
-      curl_easy_setopt(curl_handle_, CURLOPT_CUSTOMREQUEST, method_.c_str());
+    curl_easy_setopt(curl_handle, CURLOPT_POST, 1L);
+    curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, nullptr);
+    if (method != request_type::kPost)
+      curl_easy_setopt(curl_handle, CURLOPT_CUSTOMREQUEST, method.c_str());
   }
 
-  LOG(INFO) << "Request data (" << request_data_.size() << ")";
-  VLOG_IF(2, !request_data_.empty()) << "Raw request data: "
-      << std::string(reinterpret_cast<const char*>(request_data_.data()),
-                     request_data_.size());
-
-  // Setup HTTP response data.
-  if (method_ != request_type::kHead) {
-    curl_easy_setopt(curl_handle_,
-                     CURLOPT_WRITEFUNCTION, &Transport::write_callback);
-    curl_easy_setopt(curl_handle_, CURLOPT_WRITEDATA, this);
+  std::unique_ptr<http::Connection> connection(
+      new http::curl::Connection(curl_handle, method, transport));
+  CHECK(connection) << "Unable to create Connection object";
+  if (!connection->SendHeaders(headers)) {
+    connection.reset();
+    if (error_msg)
+      *error_msg = "Failed to send request headers";
   }
-
-  // HTTP request headers
-  auto headers = GetHeaders();
-  if (method_ != request_type::kGet && method_ != request_type::kHead) {
-    if (!content_type_.empty())
-      headers.emplace_back(request_header::kContentType, content_type_);
-  }
-
-  curl_slist* header_list = nullptr;
-  if (!headers.empty()) {
-    for (auto pair : headers) {
-      std::string header = string_utils::Join(": ", pair.first, pair.second);
-      VLOG(2) << "Request header: " << header;
-      header_list = curl_slist_append(header_list, header.c_str());
-    }
-    curl_easy_setopt(curl_handle_, CURLOPT_HTTPHEADER, header_list);
-  }
-
-  headers.clear();
-
-  // HTTP response headers
-  curl_easy_setopt(curl_handle_,
-                   CURLOPT_HEADERFUNCTION, &Transport::header_callback);
-  curl_easy_setopt(curl_handle_, CURLOPT_HEADERDATA, this);
-
-  CURLcode ret = curl_easy_perform(curl_handle_);
-  if (ret != CURLE_OK) {
-    error_ = curl_easy_strerror(ret);
-    stage_ = Stage::failed;
-    LOG(ERROR) << "CURL request failed: " << error_;
-  } else {
-    stage_ = Stage::response_received;
-  }
-  curl_slist_free_all(header_list);
-  if (stage_ == Stage::response_received) {
-    LOG(INFO) << "Response: " << GetResponseStatusCode() << " ("
-              << GetResponseStatusText() << ")";
-    VLOG(2) << "Response data (" << response_data_.size() << "): "
-        << std::string(reinterpret_cast<const char*>(response_data_.data()),
-                       response_data_.size());
-  }
-  return (ret == CURLE_OK);
-}
-
-int Transport::GetResponseStatusCode() const {
-  if (stage_ != Stage::response_received)
-    return 0;
-  long status_code = 0;
-  curl_easy_getinfo(curl_handle_, CURLINFO_RESPONSE_CODE, &status_code);
-  return status_code;
-}
-
-std::string Transport::GetResponseHeader(const char* headerName) const {
-  auto p = headers_.find(headerName);
-  return p != headers_.end() ? p->second : std::string();
-}
-
-const std::vector<unsigned char>& Transport::GetResponseData() const {
-  return response_data_;
-}
-
-void Transport::Close() {
-  if (curl_handle_) {
-    curl_easy_cleanup(curl_handle_);
-    curl_handle_ = nullptr;
-  }
-  stage_ = Stage::closed;
-}
-
-size_t Transport::write_callback(char* ptr, size_t size,
-                                 size_t num, void* data) {
-  Transport* me = reinterpret_cast<Transport*>(data);
-  size_t data_len = size * num;
-  me->response_data_.insert(me->response_data_.end(), ptr, ptr + data_len);
-  return data_len;
-}
-
-size_t Transport::read_callback(char* ptr, size_t size,
-                                size_t num, void* data) {
-  Transport* me = reinterpret_cast<Transport*>(data);
-  size_t data_len = size * num;
-
-  if (me->request_data_ptr_ >= me->request_data_.size())
-    return 0;
-
-  if (me->request_data_ptr_ + data_len > me->request_data_.size())
-    data_len = me->request_data_.size() - me->request_data_ptr_;
-
-  memcpy(ptr, me->request_data_.data() + me->request_data_ptr_, data_len);
-  me->request_data_ptr_ += data_len;
-
-  return data_len;
-}
-
-size_t Transport::header_callback(char* ptr, size_t size,
-                                  size_t num, void* data) {
-  Transport* me = reinterpret_cast<Transport*>(data);
-  size_t hdr_len = size * num;
-  std::string header(ptr, int(hdr_len));
-  // Remove newlines at the end of header line.
-  while (!header.empty() && (header.back() == '\r' || header.back() == '\n')) {
-    header.pop_back();
-  }
-
-  VLOG(2) << "Response header: " << header;
-
-  if (!me->status_text_set_) {
-    // First header - response code as "HTTP/1.1 200 OK".
-    // Need to extract the OK part
-    size_t pos = header.find(' ');
-    if(pos != std::string::npos)
-      pos = header.find(' ', pos + 1);
-    if (pos != std::string::npos)
-      me->status_text_ = header.substr(pos + 1);
-    me->status_text_set_ = true;
-  } else {
-    auto pair = string_utils::SplitAtFirst(header, ':');
-    if (!pair.second.empty())
-      me->headers_.insert(pair);
-  }
-  return hdr_len;
+  return connection;
 }