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/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