shill: Add HTTP Request object

Add an object that requests an HTTP URL through a given connection.
This will be used by portal detection.

BUG=chromium-os:23318
TEST=New unit tests

Change-Id: I060c1ebf32c84df3f77b321277e5fc652b0366a8
Reviewed-on: https://gerrit.chromium.org/gerrit/14593
Reviewed-by: Paul Stewart <pstew@chromium.org>
Tested-by: Paul Stewart <pstew@chromium.org>
Commit-Ready: Paul Stewart <pstew@chromium.org>
diff --git a/Makefile b/Makefile
index cc08914..0c9ce4a 100644
--- a/Makefile
+++ b/Makefile
@@ -108,6 +108,7 @@
 	glib_io_ready_handler.o \
 	glib_io_input_handler.o \
 	http_proxy.o \
+	http_request.o \
 	http_url.o \
 	ip_address.o \
 	ipconfig.o \
@@ -181,6 +182,7 @@
 	dns_client_unittest.o \
 	error_unittest.o \
 	http_proxy_unittest.o \
+	http_request_unittest.o \
 	http_url_unittest.o \
 	ip_address_unittest.o \
 	ipconfig_unittest.o \
diff --git a/http_request.cc b/http_request.cc
new file mode 100644
index 0000000..6e65538
--- /dev/null
+++ b/http_request.cc
@@ -0,0 +1,270 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/http_request.h"
+
+#include <string>
+
+#include <base/logging.h>
+#include <base/string_number_conversions.h>
+#include <base/stringprintf.h>
+
+#include "shill/async_connection.h"
+#include "shill/connection.h"
+#include "shill/dns_client.h"
+#include "shill/event_dispatcher.h"
+#include "shill/http_url.h"
+#include "shill/ip_address.h"
+#include "shill/sockets.h"
+
+using base::StringPrintf;
+using std::string;
+
+namespace shill {
+
+const int HTTPRequest::kConnectTimeoutSeconds = 10;
+const int HTTPRequest::kDNSTimeoutSeconds = 5;
+const int HTTPRequest::kInputTimeoutSeconds = 10;
+
+const char HTTPRequest::kHTTPRequestTemplate[] =
+    "GET %s HTTP/1.1\r\n"
+    "Host: %s:%d\r\n"
+    "Connection: Close\r\n\r\n";
+
+HTTPRequest::HTTPRequest(ConnectionRefPtr connection,
+                         EventDispatcher *dispatcher,
+                         Sockets *sockets)
+    : connection_(connection),
+      dispatcher_(dispatcher),
+      sockets_(sockets),
+      connect_completion_callback_(
+          NewCallback(this, &HTTPRequest::OnConnectCompletion)),
+      dns_client_callback_(NewCallback(this, &HTTPRequest::GetDNSResult)),
+      read_server_callback_(NewCallback(this, &HTTPRequest::ReadFromServer)),
+      write_server_callback_(NewCallback(this, &HTTPRequest::WriteToServer)),
+      result_callback_(NULL),
+      read_event_callback_(NULL),
+      task_factory_(this),
+      idle_timeout_(NULL),
+      dns_client_(
+          new DNSClient(IPAddress::kFamilyIPv4,
+                        connection->interface_name(),
+                        connection->dns_servers(),
+                        kDNSTimeoutSeconds * 1000,
+                        dispatcher,
+                        dns_client_callback_.get())),
+      server_async_connection_(
+          new AsyncConnection(connection_->interface_name(),
+                              dispatcher_, sockets,
+                              connect_completion_callback_.get())),
+      server_port_(-1),
+      server_socket_(-1),
+      timeout_result_(kResultUnknown),
+      is_running_(false) { }
+
+HTTPRequest::~HTTPRequest() {
+  Stop();
+}
+
+HTTPRequest::Result HTTPRequest::Start(
+    const HTTPURL &url,
+    Callback1<int>::Type *read_event_callback,
+    Callback1<Result>::Type *result_callback) {
+  VLOG(3) << "In " << __func__;
+
+  DCHECK(!is_running_);
+
+  is_running_ = true;
+  request_data_ = ByteString(StringPrintf(kHTTPRequestTemplate,
+                                          url.path().c_str(),
+                                          url.host().c_str(),
+                                          url.port()), false);
+  server_hostname_ = url.host();
+  server_port_ = url.port();
+  connection_->RequestRouting();
+
+  IPAddress addr(IPAddress::kFamilyIPv4);
+  if (addr.SetAddressFromString(server_hostname_)) {
+    if (!ConnectServer(addr, server_port_)) {
+      LOG(ERROR) << "Connect to "
+                 << server_hostname_
+                 << " failed synchronously";
+      return kResultConnectionFailure;
+    }
+  } else {
+    VLOG(3) << "Looking up host: " << server_hostname_;
+    if (!dns_client_->Start(server_hostname_)) {
+      LOG(ERROR) << "Failed to start DNS client";
+      Stop();
+      return kResultDNSFailure;
+    }
+  }
+
+  // Only install callbacks after connection succeeds in starting.
+  read_event_callback_ = read_event_callback;
+  result_callback_ = result_callback;
+
+  return kResultInProgress;
+}
+
+void HTTPRequest::Stop() {
+  VLOG(3) << "In " << __func__ << "; running is " << is_running_;
+
+  if (!is_running_) {
+    return;
+  }
+
+  // Clear IO handlers first so that closing the socket doesn't cause
+  // events to fire.
+  write_server_handler_.reset();
+  read_server_handler_.reset();
+
+  connection_->ReleaseRouting();
+  dns_client_->Stop();
+  idle_timeout_ = NULL;
+  is_running_ = false;
+  result_callback_ = NULL;
+  read_event_callback_ = NULL;
+  request_data_.Clear();
+  response_data_.Clear();
+  server_async_connection_->Stop();
+  server_hostname_.clear();
+  server_port_ = -1;
+  if (server_socket_ != -1) {
+    sockets_->Close(server_socket_);
+    server_socket_ = -1;
+  }
+  task_factory_.RevokeAll();
+  timeout_result_ = kResultUnknown;
+}
+
+bool HTTPRequest::ConnectServer(const IPAddress &address, int port) {
+  VLOG(3) << "In " << __func__;
+  if (!server_async_connection_->Start(address, port)) {
+    string address_string("<unknown>");
+    address.ToString(&address_string);
+    LOG(ERROR) << "Could not create socket to connect to server at "
+               << address_string;
+    SendStatus(kResultConnectionFailure);
+    return false;
+  }
+  // Start a connection timeout only if we didn't synchronously connect.
+  if (server_socket_ == -1) {
+    StartIdleTimeout(kConnectTimeoutSeconds, kResultConnectionTimeout);
+  }
+  return true;
+}
+
+// DNSClient callback that fires when the DNS request completes.
+void HTTPRequest::GetDNSResult(bool result) {
+  VLOG(3) << "In " << __func__;
+  if (!result) {
+    const string &error = dns_client_->error();
+    LOG(ERROR) << "Could not resolve hostname "
+               << server_hostname_
+               << ": "
+               << error;
+    if (error == DNSClient::kErrorTimedOut) {
+      SendStatus(kResultDNSTimeout);
+    } else {
+      SendStatus(kResultDNSFailure);
+    }
+    return;
+  }
+  ConnectServer(dns_client_->address(), server_port_);
+}
+
+// AsyncConnection callback routine which fires when the asynchronous Connect()
+// to the remote server completes (or fails).
+void HTTPRequest::OnConnectCompletion(bool success, int fd) {
+  VLOG(3) << "In " << __func__;
+  if (!success) {
+    LOG(ERROR) << "Socket connection delayed failure to "
+               << server_hostname_
+               << ": "
+               << server_async_connection_->error();
+    SendStatus(kResultConnectionFailure);
+    return;
+  }
+  server_socket_ = fd;
+  write_server_handler_.reset(
+      dispatcher_->CreateReadyHandler(server_socket_,
+                                      IOHandler::kModeOutput,
+                                      write_server_callback_.get()));
+  StartIdleTimeout(kInputTimeoutSeconds, kResultRequestTimeout);
+}
+
+// IOInputHandler callback which fires when data has been read from the
+// server.
+void HTTPRequest::ReadFromServer(InputData *data) {
+  VLOG(3) << "In " << __func__ << " length " << data->len;
+  if (data->len == 0) {
+    SendStatus(kResultSuccess);
+    return;
+  }
+
+  response_data_.Append(ByteString(data->buf, data->len));
+  if (read_event_callback_) {
+    read_event_callback_->Run(data->len);
+  }
+  StartIdleTimeout(kInputTimeoutSeconds, kResultResponseTimeout);
+}
+
+void HTTPRequest::SendStatus(Result result) {
+  if (result_callback_) {
+    result_callback_->Run(result);
+  }
+  Stop();
+}
+
+// Start a timeout for "the next event".
+void HTTPRequest::StartIdleTimeout(int timeout_seconds, Result timeout_result) {
+  if (idle_timeout_) {
+    idle_timeout_->Cancel();
+  }
+  timeout_result_ = timeout_result;
+  idle_timeout_ = task_factory_.NewRunnableMethod(&HTTPRequest::TimeoutTask);
+  dispatcher_->PostDelayedTask(idle_timeout_, timeout_seconds * 1000);
+}
+
+void HTTPRequest::TimeoutTask() {
+  LOG(ERROR) << "Connection with "
+             << server_hostname_
+             << " timed out";
+  SendStatus(timeout_result_);
+}
+
+// Output ReadyHandler callback which fires when the server socket is
+// ready for data to be sent to it.
+void HTTPRequest::WriteToServer(int fd) {
+  CHECK_EQ(server_socket_, fd);
+  int ret = sockets_->Send(fd, request_data_.GetConstData(),
+                           request_data_.GetLength(), 0);
+  CHECK(static_cast<size_t>(ret) <= request_data_.GetLength());
+
+  VLOG(3) << "In " << __func__ << " wrote " << ret << " of " <<
+      request_data_.GetLength();
+
+  if (ret < 0) {
+    LOG(ERROR) << "Client write failed to "
+               << server_hostname_;
+    SendStatus(kResultRequestFailure);
+    return;
+  }
+
+  request_data_ = ByteString(request_data_.GetConstData() + ret,
+                             request_data_.GetLength() - ret);
+
+  if (request_data_.IsEmpty()) {
+    write_server_handler_->Stop();
+    read_server_handler_.reset(
+        dispatcher_->CreateInputHandler(server_socket_,
+                                        read_server_callback_.get()));
+    StartIdleTimeout(kInputTimeoutSeconds, kResultResponseTimeout);
+  } else {
+    StartIdleTimeout(kInputTimeoutSeconds, kResultRequestTimeout);
+  }
+}
+
+}  // namespace shill
diff --git a/http_request.h b/http_request.h
new file mode 100644
index 0000000..406a94d
--- /dev/null
+++ b/http_request.h
@@ -0,0 +1,129 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SHILL_HTTP_REQUEST_
+#define SHILL_HTTP_REQUEST_
+
+#include <string>
+#include <vector>
+
+#include <base/callback_old.h>
+#include <base/memory/ref_counted.h>
+#include <base/memory/scoped_ptr.h>
+#include <base/task.h>
+
+#include "shill/byte_string.h"
+#include "shill/refptr_types.h"
+#include "shill/shill_time.h"
+
+namespace shill {
+
+class AsyncConnection;
+class DNSClient;
+class EventDispatcher;
+class HTTPURL;
+class InputData;
+class IOHandler;
+class IPAddress;
+class Sockets;
+
+// The HTTPRequest class implements facilities for performing
+// a simple "GET" request and returning the contents via a
+// callback.
+class HTTPRequest {
+ public:
+  enum Result {
+    kResultUnknown,
+    kResultInProgress,
+    kResultDNSFailure,
+    kResultDNSTimeout,
+    kResultConnectionFailure,
+    kResultConnectionTimeout,
+    kResultRequestFailure,
+    kResultRequestTimeout,
+    kResultResponseFailure,
+    kResultResponseTimeout,
+    kResultSuccess
+  };
+
+  HTTPRequest(ConnectionRefPtr connection,
+              EventDispatcher *dispatcher,
+              Sockets *sockets);
+  virtual ~HTTPRequest();
+
+  // Start an http GET request to the URL |url|.  Whenever any data is
+  // read from the server, |read_event_callback| is called with the number
+  // of bytes read.  This callback could be called more than once as data
+  // arrives.  All callbacks can access the data as it is read in by
+  // using the response_data() getter below.
+  //
+  // When the transaction completes, |result_callback| will be called with
+  // the final status from the transaction.  |result_callback| will not be
+  // called if either the Start() call fails or if Stop() is called before
+  // the transaction completes.
+  //
+  // This (Start) function returns a failure result if the request
+  // failed during initialization, or kResultInProgress if the request
+  // has started successfully and is now in progress.
+  Result Start(const HTTPURL &url,
+               Callback1<int>::Type *read_event_callback,
+               Callback1<Result>::Type *result_callback);
+
+  // Stop the current HTTPRequest.  No callback is called as a side
+  // effect of this function.
+  void Stop();
+
+  const ByteString &response_data() const { return response_data_; }
+
+ private:
+  friend class HTTPRequestTest;
+
+  // Time to wait for connection to remote server.
+  static const int kConnectTimeoutSeconds;
+  // Time to wait for DNS server.
+  static const int kDNSTimeoutSeconds;
+  // Time to wait for any input from server.
+  static const int kInputTimeoutSeconds;
+
+  static const char kHTTPRequestTemplate[];
+
+  bool ConnectServer(const IPAddress &address, int port);
+  void GetDNSResult(bool result);
+  void OnConnectCompletion(bool success, int fd);
+  void ReadFromServer(InputData *data);
+  void SendStatus(Result result);
+  void StartIdleTimeout(int timeout_seconds, Result timeout_result);
+  void TimeoutTask();
+  void WriteToServer(int fd);
+
+  ConnectionRefPtr connection_;
+  EventDispatcher *dispatcher_;
+  Sockets *sockets_;
+
+  scoped_ptr<Callback2<bool, int>::Type> connect_completion_callback_;
+  scoped_ptr<Callback1<bool>::Type> dns_client_callback_;
+  scoped_ptr<Callback1<InputData *>::Type> read_server_callback_;
+  scoped_ptr<Callback1<int>::Type> write_server_callback_;
+  Callback1<Result>::Type *result_callback_;
+  Callback1<int>::Type *read_event_callback_;
+  ScopedRunnableMethodFactory<HTTPRequest> task_factory_;
+  CancelableTask *idle_timeout_;
+  scoped_ptr<IOHandler> read_server_handler_;
+  scoped_ptr<IOHandler> write_server_handler_;
+  scoped_ptr<DNSClient> dns_client_;
+  scoped_ptr<AsyncConnection> server_async_connection_;
+  std::string server_hostname_;
+  int server_port_;
+  int server_socket_;
+  Result timeout_result_;
+  ByteString request_data_;
+  ByteString response_data_;
+  bool is_running_;
+
+  DISALLOW_COPY_AND_ASSIGN(HTTPRequest);
+};
+
+}  // namespace shill
+
+#endif  // SHILL_HTTP_REQUEST_
diff --git a/http_request_unittest.cc b/http_request_unittest.cc
new file mode 100644
index 0000000..d29fcd6
--- /dev/null
+++ b/http_request_unittest.cc
@@ -0,0 +1,455 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/http_request.h"
+
+#include <netinet/in.h>
+
+#include <string>
+#include <vector>
+
+#include <base/stringprintf.h>
+#include <gtest/gtest.h>
+
+#include "shill/ip_address.h"
+#include "shill/http_url.h"
+#include "shill/mock_async_connection.h"
+#include "shill/mock_connection.h"
+#include "shill/mock_control.h"
+#include "shill/mock_device_info.h"
+#include "shill/mock_dns_client.h"
+#include "shill/mock_event_dispatcher.h"
+#include "shill/mock_sockets.h"
+
+using base::StringPrintf;
+using std::string;
+using std::vector;
+using ::testing::_;
+using ::testing::AtLeast;
+using ::testing::DoAll;
+using ::testing::Invoke;
+using ::testing::NiceMock;
+using ::testing::Return;
+using ::testing::ReturnArg;
+using ::testing::ReturnNew;
+using ::testing::ReturnRef;
+using ::testing::SetArgumentPointee;
+using ::testing::StrEq;
+using ::testing::StrictMock;
+using ::testing::Test;
+
+namespace shill {
+
+namespace {
+const char kTextSiteName[] = "www.chromium.org";
+const char kTextURL[] = "http://www.chromium.org/path/to/resource";
+const char kNumericURL[] = "http://10.1.1.1";
+const char kPath[] = "/path/to/resource";
+const char kBadURL[] = "xxx";
+const char kInterfaceName[] = "int0";
+const char kDNSServer0[] = "8.8.8.8";
+const char kDNSServer1[] = "8.8.4.4";
+const char *kDNSServers[] = { kDNSServer0, kDNSServer1 };
+const char kServerAddress[] = "10.1.1.1";
+const int kServerFD = 10203;
+const int kServerPort = 80;
+}  // namespace {}
+
+MATCHER_P(IsIPAddress, address, "") {
+  IPAddress ip_address(IPAddress::kFamilyIPv4);
+  EXPECT_TRUE(ip_address.SetAddressFromString(address));
+  return ip_address.Equals(arg);
+}
+
+class HTTPRequestTest : public Test {
+ public:
+  HTTPRequestTest()
+      : interface_name_(kInterfaceName),
+        server_async_connection_(new StrictMock<MockAsyncConnection>()),
+        dns_servers_(kDNSServers, kDNSServers + 2),
+        dns_client_(new StrictMock<MockDNSClient>()),
+        device_info_(new NiceMock<MockDeviceInfo>(
+            &control_,
+            reinterpret_cast<EventDispatcher*>(NULL),
+            reinterpret_cast<Metrics*>(NULL),
+            reinterpret_cast<Manager*>(NULL))),
+        connection_(new StrictMock<MockConnection>(device_info_.get())) { }
+ protected:
+  class CallbackTarget {
+   public:
+    CallbackTarget()
+        : read_event_callback_(
+              NewCallback(this, &CallbackTarget::ReadEventCallTarget)),
+          result_callback_(
+              NewCallback(this, &CallbackTarget::ResultCallTarget)) {}
+
+    MOCK_METHOD1(ReadEventCallTarget, void(int byte_count));
+    MOCK_METHOD1(ResultCallTarget, void(HTTPRequest::Result));
+    Callback1<int>::Type *read_event_callback() {
+      return read_event_callback_.get();
+    }
+    Callback1<HTTPRequest::Result>::Type *result_callback() {
+      return result_callback_.get();
+    }
+
+   private:
+    scoped_ptr<Callback1<int>::Type> read_event_callback_;
+    scoped_ptr<Callback1<HTTPRequest::Result>::Type> result_callback_;
+  };
+
+  virtual void SetUp() {
+    EXPECT_CALL(*connection_.get(), interface_name())
+        .WillRepeatedly(ReturnRef(interface_name_));
+    EXPECT_CALL(*connection_.get(), dns_servers())
+        .WillRepeatedly(ReturnRef(dns_servers_));
+
+    request_.reset(new HTTPRequest(connection_, &dispatcher_, &sockets_));
+    // Passes ownership.
+    request_->dns_client_.reset(dns_client_);
+    // Passes ownership.
+    request_->server_async_connection_.reset(server_async_connection_);
+  }
+  virtual void TearDown() {
+    if (request_->is_running_) {
+      ExpectStop();
+
+      // Subtle: Make sure the finalization of the request happens while our
+      // expectations are still active.
+      request_.reset();
+    }
+  }
+  size_t FindInRequestData(const string &find_string) {
+    string request_string(
+        reinterpret_cast<char *>(request_->request_data_.GetData()),
+        request_->request_data_.GetLength());
+    return request_string.find(find_string);
+  }
+  // Accessors
+  const ByteString &GetRequestData() {
+    return request_->request_data_;
+  }
+  HTTPRequest *request() { return request_.get(); }
+  MockSockets &sockets() { return sockets_; }
+
+  // Expectations
+  void ExpectReset() {
+    EXPECT_EQ(connection_.get(), request_->connection_.get());
+    EXPECT_EQ(&dispatcher_, request_->dispatcher_);
+    EXPECT_EQ(&sockets_, request_->sockets_);
+    EXPECT_FALSE(request_->result_callback_);
+    EXPECT_FALSE(request_->read_event_callback_);
+    EXPECT_TRUE(request_->task_factory_.empty());
+    EXPECT_FALSE(request_->idle_timeout_);
+    EXPECT_FALSE(request_->read_server_handler_.get());
+    EXPECT_FALSE(request_->write_server_handler_.get());
+    EXPECT_EQ(dns_client_, request_->dns_client_.get());
+    EXPECT_EQ(server_async_connection_,
+              request_->server_async_connection_.get());
+    EXPECT_TRUE(request_->server_hostname_.empty());
+    EXPECT_EQ(-1, request_->server_port_);
+    EXPECT_EQ(-1, request_->server_socket_);
+    EXPECT_EQ(HTTPRequest::kResultUnknown, request_->timeout_result_);
+    EXPECT_TRUE(request_->request_data_.IsEmpty());
+    EXPECT_TRUE(request_->response_data_.IsEmpty());
+    EXPECT_FALSE(request_->is_running_);
+  }
+  void ExpectStop() {
+    if (request_->server_socket_ != -1) {
+      EXPECT_CALL(sockets(), Close(kServerFD))
+          .WillOnce(Return(0));
+    }
+    EXPECT_CALL(*dns_client_, Stop())
+        .Times(AtLeast(1));
+    EXPECT_CALL(*server_async_connection_, Stop())
+        .Times(AtLeast(1));
+    EXPECT_CALL(*connection_.get(), ReleaseRouting());
+  }
+  void ExpectSetTimeout(int timeout) {
+    EXPECT_CALL(dispatcher_, PostDelayedTask(_, timeout * 1000))
+        .WillOnce(Return(true));
+  }
+  void ExpectSetConnectTimeout() {
+    ExpectSetTimeout(HTTPRequest::kConnectTimeoutSeconds);
+  }
+  void ExpectSetInputTimeout() {
+    ExpectSetTimeout(HTTPRequest::kInputTimeoutSeconds);
+  }
+  void ExpectInResponse(const string &expected_response_data) {
+    string response_string(
+        reinterpret_cast<char *>(request_->response_data_.GetData()),
+        request_->response_data_.GetLength());
+    EXPECT_NE(string::npos, response_string.find(expected_response_data));
+  }
+  void ExpectDNSRequest(const string &host, bool return_value) {
+    EXPECT_CALL(*dns_client_, Start(StrEq(host)))
+        .WillOnce(Return(return_value));
+  }
+  void ExpectDNSFailure(const string &error) {
+    EXPECT_CALL(*dns_client_, error())
+        .WillOnce(ReturnRef(error));
+  }
+  void ExpectAsyncConnect(const string &address, int port,
+                          bool return_value) {
+    EXPECT_CALL(*server_async_connection_, Start(IsIPAddress(address), port))
+        .WillOnce(Return(return_value));
+    if (return_value) {
+      ExpectSetConnectTimeout();
+    }
+  }
+  void  InvokeSyncConnect(const IPAddress &/*address*/, int /*port*/) {
+    CallConnectCompletion(true, kServerFD);
+  }
+  void CallConnectCompletion(bool success, int fd) {
+    request_->OnConnectCompletion(success, fd);
+  }
+  void ExpectSyncConnect(const string &address, int port) {
+    EXPECT_CALL(*server_async_connection_, Start(IsIPAddress(address), port))
+        .WillOnce(DoAll(Invoke(this, &HTTPRequestTest::InvokeSyncConnect),
+                        Return(true)));
+  }
+  void ExpectConnectFailure() {
+    EXPECT_CALL(*server_async_connection_, Start(_, _))
+        .WillOnce(Return(false));
+  }
+  void ExpectMonitorServerInput() {
+    EXPECT_CALL(dispatcher_,
+                CreateInputHandler(kServerFD,
+                                   request_->read_server_callback_.get()))
+        .WillOnce(ReturnNew<IOHandler>());
+    ExpectSetInputTimeout();
+  }
+  void ExpectMonitorServerOutput() {
+    EXPECT_CALL(dispatcher_,
+                CreateReadyHandler(kServerFD,
+                                   IOHandler::kModeOutput,
+                                   request_->write_server_callback_.get()))
+        .WillOnce(ReturnNew<IOHandler>());
+    ExpectSetInputTimeout();
+  }
+  void ExpectRouteRequest() {
+    EXPECT_CALL(*connection_.get(), RequestRouting());
+  }
+  void ExpectRouteRelease() {
+    EXPECT_CALL(*connection_.get(), ReleaseRouting());
+  }
+  void ExpectResultCallback(HTTPRequest::Result result) {
+    EXPECT_CALL(target_, ResultCallTarget(result));
+  }
+  void InvokeResultVerify(HTTPRequest::Result result) {
+    EXPECT_EQ(HTTPRequest::kResultSuccess, result);
+    EXPECT_TRUE(expected_response_.Equals(request_->response_data()));
+  }
+  void ExpectResultCallbackWithResponse(const string &response) {
+    expected_response_ = ByteString(response, false);
+    EXPECT_CALL(target_, ResultCallTarget(HTTPRequest::kResultSuccess))
+        .WillOnce(Invoke(this, &HTTPRequestTest::InvokeResultVerify));
+  }
+  void ExpectReadEventCallback(int byte_count) {
+    EXPECT_CALL(target_, ReadEventCallTarget(byte_count));
+  }
+  void GetDNSResult(bool result) {
+    request_->GetDNSResult(result);
+  }
+  void OnConnectCompletion(bool result, int sockfd) {
+    request_->OnConnectCompletion(result, sockfd);
+  }
+  void ReadFromServer(const string &data) {
+    const unsigned char *ptr =
+        reinterpret_cast<const unsigned char *>(data.c_str());
+    vector<unsigned char> data_writable(ptr, ptr + data.length());
+    InputData server_data(data_writable.data(), data_writable.size());
+    request_->ReadFromServer(&server_data);
+  }
+  void WriteToServer(int fd) {
+    request_->WriteToServer(fd);
+  }
+  HTTPRequest::Result StartRequest(const string &url) {
+    HTTPURL http_url;
+    EXPECT_TRUE(http_url.ParseFromString(url));
+    return request_->Start(http_url,
+                           target_.read_event_callback(),
+                           target_.result_callback());
+  }
+  void SetupConnectWithURL(const string &url, const string &expected_hostname) {
+    ExpectRouteRequest();
+    ExpectDNSRequest(expected_hostname, true);
+    EXPECT_EQ(HTTPRequest::kResultInProgress, StartRequest(url));
+    IPAddress addr(IPAddress::kFamilyIPv4);
+    EXPECT_TRUE(addr.SetAddressFromString(kServerAddress));
+    EXPECT_CALL(*dns_client_, address())
+        .WillOnce(ReturnRef(addr));;
+    GetDNSResult(true);
+  }
+  void SetupConnect() {
+    SetupConnectWithURL(kTextURL, kTextSiteName);
+  }
+  void SetupConnectAsync() {
+    ExpectAsyncConnect(kServerAddress, kServerPort, true);
+    SetupConnect();
+  }
+  void SetupConnectComplete() {
+    SetupConnectAsync();
+    ExpectMonitorServerOutput();
+    OnConnectCompletion(true, kServerFD);
+  }
+  void CallTimeoutTask() {
+    request_->TimeoutTask();
+  }
+
+ private:
+  const string interface_name_;
+  // Owned by the HTTPRequest, but tracked here for EXPECT().
+  StrictMock<MockAsyncConnection> *server_async_connection_;
+  vector<string> dns_servers_;
+  // Owned by the HTTPRequest, but tracked here for EXPECT().
+  StrictMock<MockDNSClient> *dns_client_;
+  StrictMock<MockEventDispatcher> dispatcher_;
+  MockControl control_;
+  scoped_ptr<MockDeviceInfo> device_info_;
+  scoped_refptr<MockConnection> connection_;
+  scoped_ptr<HTTPRequest> request_;
+  StrictMock<MockSockets> sockets_;
+  StrictMock<CallbackTarget> target_;
+  ByteString expected_response_;
+};
+
+TEST_F(HTTPRequestTest, Constructor) {
+  ExpectReset();
+}
+
+
+TEST_F(HTTPRequestTest, FailConnectNumericSynchronous) {
+  ExpectRouteRequest();
+  ExpectConnectFailure();
+  ExpectStop();
+  EXPECT_EQ(HTTPRequest::kResultConnectionFailure, StartRequest(kNumericURL));
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, FailConnectNumericAsynchronous) {
+  ExpectRouteRequest();
+  ExpectAsyncConnect(kServerAddress, HTTPURL::kDefaultHTTPPort, true);
+  EXPECT_EQ(HTTPRequest::kResultInProgress, StartRequest(kNumericURL));
+  ExpectResultCallback(HTTPRequest::kResultConnectionFailure);
+  ExpectStop();
+  CallConnectCompletion(false, -1);
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, FailConnectNumericTimeout) {
+  ExpectRouteRequest();
+  ExpectAsyncConnect(kServerAddress, HTTPURL::kDefaultHTTPPort, true);
+  EXPECT_EQ(HTTPRequest::kResultInProgress, StartRequest(kNumericURL));
+  ExpectResultCallback(HTTPRequest::kResultConnectionTimeout);
+  ExpectStop();
+  CallTimeoutTask();
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, SyncConnectNumeric) {
+  ExpectRouteRequest();
+  ExpectSyncConnect(kServerAddress, HTTPURL::kDefaultHTTPPort);
+  ExpectMonitorServerOutput();
+  EXPECT_EQ(HTTPRequest::kResultInProgress, StartRequest(kNumericURL));
+}
+
+TEST_F(HTTPRequestTest, FailDNSStart) {
+  ExpectRouteRequest();
+  ExpectDNSRequest(kTextSiteName, false);
+  ExpectStop();
+  EXPECT_EQ(HTTPRequest::kResultDNSFailure, StartRequest(kTextURL));
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, FailDNSFailure) {
+  ExpectRouteRequest();
+  ExpectDNSRequest(kTextSiteName, true);
+  EXPECT_EQ(HTTPRequest::kResultInProgress, StartRequest(kTextURL));
+  const string error(DNSClient::kErrorNoData);
+  ExpectDNSFailure(error);
+  ExpectResultCallback(HTTPRequest::kResultDNSFailure);
+  ExpectStop();
+  GetDNSResult(false);
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, FailDNSTimeout) {
+  ExpectRouteRequest();
+  ExpectDNSRequest(kTextSiteName, true);
+  EXPECT_EQ(HTTPRequest::kResultInProgress, StartRequest(kTextURL));
+  const string error(DNSClient::kErrorTimedOut);
+  ExpectDNSFailure(error);
+  ExpectResultCallback(HTTPRequest::kResultDNSTimeout);
+  ExpectStop();
+  GetDNSResult(false);
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, FailConnectText) {
+  ExpectConnectFailure();
+  ExpectResultCallback(HTTPRequest::kResultConnectionFailure);
+  ExpectStop();
+  SetupConnect();
+  ExpectReset();
+}
+
+TEST_F(HTTPRequestTest, ConnectComplete) {
+  SetupConnectComplete();
+}
+
+TEST_F(HTTPRequestTest, RequestTimeout) {
+  SetupConnectComplete();
+  ExpectResultCallback(HTTPRequest::kResultRequestTimeout);
+  ExpectStop();
+  CallTimeoutTask();
+}
+
+TEST_F(HTTPRequestTest, RequestData) {
+  SetupConnectComplete();
+  EXPECT_EQ(0, FindInRequestData(string("GET ") + kPath));
+  EXPECT_NE(string::npos,
+            FindInRequestData(string("\r\nHost: ") + kTextSiteName));
+  ByteString request_data = GetRequestData();
+  EXPECT_CALL(sockets(), Send(kServerFD, _, request_data.GetLength(), 0))
+      .WillOnce(Return(request_data.GetLength() - 1));
+  ExpectSetInputTimeout();
+  WriteToServer(kServerFD);
+  EXPECT_CALL(sockets(), Send(kServerFD, _, 1, 0))
+      .WillOnce(Return(1));
+  ExpectMonitorServerInput();
+  WriteToServer(kServerFD);
+}
+
+TEST_F(HTTPRequestTest, ResponseTimeout) {
+  SetupConnectComplete();
+  ByteString request_data = GetRequestData();
+  EXPECT_CALL(sockets(), Send(kServerFD, _, request_data.GetLength(), 0))
+      .WillOnce(Return(request_data.GetLength()));
+  ExpectMonitorServerInput();
+  WriteToServer(kServerFD);
+  ExpectResultCallback(HTTPRequest::kResultResponseTimeout);
+  ExpectStop();
+  CallTimeoutTask();
+}
+
+TEST_F(HTTPRequestTest, ResponseData) {
+  SetupConnectComplete();
+  const string response0("hello");
+  ExpectReadEventCallback(response0.length());
+  ExpectSetInputTimeout();
+  ReadFromServer(response0);
+  ExpectInResponse(response0);
+
+  const string response1(" to you");
+  ExpectReadEventCallback(response1.length());
+  ExpectSetInputTimeout();
+  ReadFromServer(response1);
+  ExpectInResponse(response1);
+
+  ExpectResultCallbackWithResponse(response0 + response1);
+  ExpectStop();
+  ReadFromServer("");
+}
+
+}  // namespace shill
diff --git a/ip_address.cc b/ip_address.cc
index d1334d0..e71f574 100644
--- a/ip_address.cc
+++ b/ip_address.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -11,6 +11,8 @@
 
 #include "shill/byte_string.h"
 
+using std::string;
+
 namespace shill {
 
 // static
@@ -49,7 +51,7 @@
   }
 }
 
-bool IPAddress::SetAddressFromString(const std::string &address_string) {
+bool IPAddress::SetAddressFromString(const string &address_string) {
   int address_length = GetAddressLength(family_);
 
   if (!address_length) {
@@ -68,4 +70,15 @@
   address_ = ByteString(GetAddressLength(family_));
 }
 
+bool IPAddress::ToString(string *address_string) const {
+  // Noting that INET6_ADDRSTRLEN > INET_ADDRSTRLEN
+  char address_buf[INET6_ADDRSTRLEN];
+  if (GetLength() != GetAddressLength(family_) ||
+      !inet_ntop(family_, GetConstData(), address_buf, sizeof(address_buf))) {
+    return false;
+  }
+  *address_string = address_buf;
+  return true;
+}
+
 }  // namespace shill
diff --git a/ip_address.h b/ip_address.h
index d6f0139..0d65fa6 100644
--- a/ip_address.h
+++ b/ip_address.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -57,6 +57,11 @@
   // An uninitialized IPAddress is empty and invalid when constructed.
   // Use SetAddressToDefault() to set it to the default or "all-zeroes" address.
   void SetAddressToDefault();
+  // Return the string equivalent of the address.  Returns true if the
+  // conversion succeeds in which case |address_string| is set to the
+  // result.  Otherwise the function returns false and |address_string|
+  // is left unmodified.
+  bool ToString(std::string *address_string) const;
 
   bool Equals(const IPAddress &b) const {
     return family_ == b.family_ && address_.Equals(b.address_) &&
diff --git a/ip_address_unittest.cc b/ip_address_unittest.cc
index d105903..47d027a 100644
--- a/ip_address_unittest.cc
+++ b/ip_address_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -9,6 +9,7 @@
 #include "shill/byte_string.h"
 #include "shill/ip_address.h"
 
+using std::string;
 using testing::Test;
 
 namespace shill {
@@ -33,9 +34,9 @@
 class IPAddressTest : public Test {
  protected:
   void TestAddress(IPAddress::Family family,
-                   const std::string &good_string,
+                   const string &good_string,
                    const ByteString &good_bytes,
-                   const std::string &bad_string,
+                   const string &bad_string,
                    const ByteString &bad_bytes) {
     IPAddress good_addr(family);
 
@@ -46,6 +47,9 @@
     EXPECT_EQ(0, memcmp(good_addr.GetConstData(), good_bytes.GetConstData(),
                         good_bytes.GetLength()));
     EXPECT_TRUE(good_addr.address().Equals(good_bytes));
+    string address_string;
+    EXPECT_TRUE(good_addr.ToString(&address_string));
+    EXPECT_EQ(good_string, address_string);
 
     IPAddress good_addr_from_bytes(family, good_bytes);
     EXPECT_TRUE(good_addr.Equals(good_addr_from_bytes));
@@ -61,6 +65,7 @@
     EXPECT_FALSE(bad_addr_from_bytes.IsValid());
 
     EXPECT_FALSE(bad_addr.Equals(bad_addr_from_bytes));
+    EXPECT_FALSE(bad_addr.ToString(&address_string));
   }
 };