| // Copyright (c) 2013 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 "p2p/http_server/connection_delegate.h" |
| |
| #include <arpa/inet.h> |
| #include <fcntl.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| |
| #include <map> |
| #include <string> |
| #include <vector> |
| |
| #include <base/command_line.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/threading/simple_thread.h> |
| #include <gtest/gtest.h> |
| |
| #include "p2p/common/constants.h" |
| #include "p2p/common/fake_clock.h" |
| #include "p2p/common/testutil.h" |
| #include "p2p/common/util.h" |
| #include "p2p/http_server/mock_server.h" |
| |
| using std::map; |
| using std::string; |
| using std::tuple; |
| using std::vector; |
| |
| using base::FilePath; |
| using base::WriteFile; |
| |
| using p2p::common::FakeClock; |
| using p2p::testutil::SetExpectedFileSize; |
| using p2p::testutil::SetupTestDir; |
| using p2p::testutil::TeardownTestDir; |
| |
| using testing::_; |
| |
| namespace { |
| // DefaultDownloadRate used for the tests in bytes per seconds (5MB/s). |
| static const int kDefaultDownloadRate = 5 * 1000 * 1000; |
| } // namespace |
| |
| namespace p2p { |
| |
| namespace http_server { |
| |
| class ConnectionDelegateTest : public ::testing::Test { |
| public: |
| ConnectionDelegateTest() |
| : testdir_fd_(-1), |
| server_fd_(-1), |
| client_fd_(-1), |
| thread_(NULL), |
| delegate_(NULL) { |
| ON_CALL(mock_server_, Clock()).WillByDefault(testing::Return(&clock_)); |
| EXPECT_CALL(mock_server_, Clock()).Times(testing::AtLeast(0)); |
| } |
| |
| protected: |
| void SetupDelegate() { |
| testdir_path_ = SetupTestDir("connection-delegate"); |
| testdir_fd_ = open(testdir_path_.value().c_str(), O_DIRECTORY); |
| if (testdir_fd_ == -1) |
| PLOG(ERROR) << "Opening delegate-test temp directory"; |
| ASSERT_NE(testdir_fd_, -1); |
| |
| // Create a TCP server in any port and connect to it. This provides the |
| // very same kind of socket ConnectionDelegate will use in the real |
| // implementation. |
| int servsock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, |
| IPPROTO_TCP); |
| if (servsock == -1) |
| PLOG(ERROR) << "Creating a server socket()"; |
| ASSERT_NE(servsock, -1); |
| |
| client_fd_ = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP); |
| if (client_fd_ == -1) |
| PLOG(ERROR) << "Creating a client socket()"; |
| ASSERT_NE(client_fd_, -1); |
| |
| // Set the socket to listen to a random port. |
| struct sockaddr_in server_addr; |
| socklen_t server_len = sizeof(server_addr); |
| memset(reinterpret_cast<char*>(&server_addr), 0, sizeof(server_addr)); |
| server_addr.sin_family = AF_INET; |
| server_addr.sin_port = htons(0); // any port |
| server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); |
| ASSERT_NE(-1, |
| bind(servsock, reinterpret_cast<struct sockaddr*>(&server_addr), |
| sizeof(server_addr))); |
| // Read back the selected address and port. |
| ASSERT_NE(-1, getsockname(servsock, |
| reinterpret_cast<struct sockaddr*>(&server_addr), |
| &server_len)); |
| |
| ASSERT_NE(listen(servsock, 1), -1); |
| |
| // At this point, the server socked will accept the connection and we will |
| // get the server side socket once we call accept(). |
| struct sockaddr_in client_addr; |
| socklen_t client_len = sizeof(client_addr); |
| ASSERT_NE(-1, connect(client_fd_, |
| reinterpret_cast<struct sockaddr*>(&server_addr), |
| sizeof(server_addr))); |
| |
| server_fd_ = |
| accept(servsock, reinterpret_cast<struct sockaddr*>(&client_addr), |
| &client_len); |
| if (server_fd_ == -1) |
| PLOG(ERROR) << "Accepting the client connection."; |
| ASSERT_NE(server_fd_, -1); |
| |
| // Tear down the listening server (but keep the server_fd_ socket). |
| ASSERT_EQ(close(servsock), 0); |
| |
| // Create the Server |
| delegate_ = new ConnectionDelegate(testdir_fd_, server_fd_, "[addr]", |
| &mock_server_, kDefaultDownloadRate); |
| |
| thread_ = new base::DelegateSimpleThread(delegate_, "delegate"); |
| } |
| |
| void TearDown() override { |
| if (thread_) |
| delete thread_; |
| // The ConnectionDelegate deletes itself when Run() finishes. |
| |
| if (client_fd_ != -1) |
| EXPECT_EQ(0, close(client_fd_)); |
| // The ConnectionDelegate should close the provided file descriptor, thus |
| // this close should fail. |
| if (server_fd_ != -1) |
| EXPECT_EQ(-1, close(server_fd_)); |
| if (testdir_fd_ != -1) |
| EXPECT_EQ(0, close(testdir_fd_)); |
| if (!testdir_path_.empty()) |
| TeardownTestDir(testdir_path_); |
| } |
| |
| // A randomly generated temporary testing directory to put the shared files. |
| FilePath testdir_path_; |
| |
| // File descriptor to the testdir_path_ directory. |
| int testdir_fd_; |
| |
| // The server-side fd in the connection (passed to ConnectionDelegate). |
| int server_fd_; |
| |
| // The client-side fd that a client is using to request the content from the |
| // ConnectionDelegate. |
| int client_fd_; |
| |
| // Thread pool needed to run the ConnectionDelegate. |
| base::DelegateSimpleThread* thread_; |
| |
| testing::StrictMock<MockServer> mock_server_; |
| ConnectionDelegate* delegate_; |
| FakeClock clock_; |
| }; |
| |
| // A class to help building a HTTP Request. |
| class HTTPRequest { |
| public: |
| HTTPRequest() |
| : method_("GET"), http_version_("1.1"), uri_("/"), host_("127.0.0.1") { |
| headers_["User-Agent"] = "HTTPRequest/1.0 (unittester)"; |
| } |
| |
| string ToString() const { |
| string res = method_ + " " + uri_ + " HTTP/" + http_version_ + "\r\n" + |
| "Host: " + host_ + "\r\n"; |
| for (const auto& it : headers_) |
| res += it.first + ": " + it.second + "\r\n"; |
| res += "\r\n" + post_data_; |
| return res; |
| } |
| |
| bool Send(int fd) const { |
| string req = ToString(); |
| size_t to_write = req.size(); |
| const char* p = req.c_str(); |
| int write_result; |
| while (to_write > 0) { |
| write_result = write(fd, p, to_write); |
| if (write_result < 0 && errno == EAGAIN) |
| continue; |
| if (write_result <= 0) { |
| PLOG(ERROR) << "Error writing to fd " << fd; |
| return false; |
| } |
| to_write -= write_result; |
| p += write_result; |
| } |
| return true; |
| } |
| |
| string method_; |
| string http_version_; // The HTTP protocol version. |
| string uri_; |
| string host_; |
| map<string, string> headers_; |
| string post_data_; |
| }; |
| |
| class HTTPResponse { |
| public: |
| explicit HTTPResponse(const string& response) |
| : response_(response), valid_(false) { |
| const char* p = response_.c_str(); |
| int len = response_.size(); |
| if (len == 0) |
| return; |
| |
| // Parse the header lines until the empty line is reached. |
| const char* line = p; |
| int line_len = len; |
| while (line_len > 0) { |
| int i; |
| for (i = 0; i < line_len; ++i) { |
| if (line[i] == '\n') { |
| if (i > 0 && line[i - 1] == '\r') { |
| // Found \r\n ending line. |
| line_len = i + 1; |
| break; |
| } |
| LOG(ERROR) << "Header line doesn't end in \\r\\n."; |
| return; |
| } |
| } |
| // Check if the end of the response was reached while parsing the headers. |
| if (i == line_len) |
| return; |
| |
| raw_headers_.push_back(string(line, line_len - 2)); |
| line += line_len; |
| line_len = len - (line - p); |
| // Check if the empty header line was found. |
| if (line_len >= 2 && line[0] == '\r' && line[1] == '\n') |
| break; |
| } |
| // Check if the header is \r\n |
| if (!line_len) { |
| LOG(ERROR) << "Missing \\r\\n after the headers."; |
| return; |
| } |
| |
| line += 2; |
| if (line < p + len) |
| content_ = string(line, len - (line - p)); |
| |
| // Parse the first response line. |
| string first_line = raw_headers_[0]; |
| if (first_line.substr(0, 5) == "HTTP/") { |
| size_t sp_pos = first_line.find(' '); |
| if (sp_pos != std::string::npos) { |
| http_version_ = first_line.substr(5, sp_pos - 5); |
| first_line = first_line.substr(sp_pos + 1); |
| } |
| } |
| http_code_ = atoi(first_line.c_str()); |
| |
| // Parse the raw_headers. |
| for (size_t i = 1; i < raw_headers_.size(); ++i) { |
| const string& header = raw_headers_[i]; |
| const char* header_p = header.c_str(); |
| const char* sep = |
| reinterpret_cast<const char*>(memchr(header_p, ':', header.size())); |
| if (!sep || sep[1] != ' ') { |
| LOG(ERROR) << "Invalid header: \"" << header << "\"."; |
| return; |
| } |
| string key(header_p, sep - header_p); |
| string value(sep + 2, header.size() - (sep + 2 - header_p)); |
| |
| if (headers_.find(key) != headers_.end()) { |
| LOG(ERROR) << "The header \"" << key << "\" appears twice in the " |
| << "response."; |
| return; |
| } |
| // TODO(deymo): Support header continuation. See http://crbug.com/246326. |
| headers_[key] = value; |
| } |
| valid_ = true; |
| } |
| |
| // The original response string. |
| string response_; |
| |
| // Tells wether the response has a valid (and supported) format. |
| bool valid_; |
| |
| // The HTTP response code. |
| int http_code_; |
| |
| string http_version_; |
| |
| map<string, string> headers_; |
| |
| // The content body. |
| string content_; |
| |
| private: |
| vector<string> raw_headers_; |
| }; |
| |
| // Read or continue reading a HTTP response from the file descriptor until the |
| // connection is closed or the "content" part of the HTTP response is at least |
| // |min_content_size|. If more than |min_content_size| bytes are available to |
| // read on the file descriptor those will be included in the |response| as well. |
| // A value of -1 in |min_content_size| will block until the connection is |
| // closed. |
| // Returns wether the response was successfully read from the file descriptor |
| // and the socked was properly close from the other end or the |
| // |min_content_size| reached. |
| // The bytes read from the file descriptor are appended to the |response| |
| // string, allowing the caller to do a partial read of a HTTP response and then |
| // continue it. For example: |
| // |
| // string resp; |
| // // This will at least read the headers: |
| // ReadHTTPResponse(some_sock, &resp, 0); |
| // // ... do some checking of the headers, read the Content-Length if present. |
| // ReadHTTPResponse(some_sock, &resp, expect_content_size); |
| static bool ReadHTTPResponse(int fd, |
| string* response, |
| int min_content_size = -1) { |
| char buf[16 * 1024]; // 16KiB is a reasonable buffer for recv(). |
| int res; |
| do { |
| if (min_content_size >= 0) { |
| res = recv(fd, buf, sizeof(buf), MSG_DONTWAIT); |
| if (res == -1 && errno == EWOULDBLOCK) { |
| HTTPResponse ret(*response); |
| if (ret.valid_ && |
| ret.content_.size() >= static_cast<size_t>(min_content_size)) |
| return true; |
| // Re-read but block this time. |
| res = recv(fd, buf, sizeof(buf), 0); |
| } |
| } else { |
| res = recv(fd, buf, sizeof(buf), 0); |
| } |
| if (res == -1 && errno == EAGAIN) |
| continue; |
| if (res == -1) { |
| PLOG(ERROR) << "Reading HTTPResponse from fd " << fd; |
| return false; |
| } |
| if (res == 0) |
| break; |
| response->append(buf, res); |
| } while (true); |
| return true; |
| } |
| |
| // Generates undefined but deterministic printable data. |
| // Printable data is considered all the ASCII-7 char excluding the first 32 |
| // codes (control characters) and the 128. Thus, this function generates |
| // ASCII chars in the range 32-127 inclusive. |
| static void GeneratePrintableData(size_t size, string* output) { |
| output->clear(); |
| output->resize(size); |
| for (size_t i = 0; i < size; ++i) |
| output->at(i) = static_cast<char>(32 + (45 + i * 131 + i * i * 17) % 95); |
| } |
| |
| TEST_F(ConnectionDelegateTest, NoRequestAndClose) { |
| SetupDelegate(); |
| |
| p2p::testutil::TimeBombAbort bomb(60, "Test NoRequestAndClose timeout\n"); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultMalformed)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| EXPECT_EQ(0, close(client_fd_)); |
| client_fd_ = -1; |
| thread_->Join(); |
| } |
| |
| TEST_F(ConnectionDelegateTest, RequestUnsupportedMode) { |
| SetupDelegate(); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultMalformed)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| // Send a HEAD request. |
| HTTPRequest req; |
| req.method_ = "HEAD"; |
| req.uri_ = "/non-existent"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(501, resp.http_code_); // Not Implemented. |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetExistentFile) { |
| SetupDelegate(); |
| |
| const string content = "Hello World!"; |
| WriteFile(testdir_path_.Append("hello.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 0)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/hello"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(200, resp.http_code_); |
| EXPECT_EQ(content, resp.content_); |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ(base::IntToString(content.size()), resp.headers_["Content-Length"]); |
| } |
| |
| TEST_F(ConnectionDelegateTest, PostExistentFile) { |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(9 * 1000 * 1000 - 1, &content); |
| WriteFile(testdir_path_.Append("a.foo.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerServedSuccessfullyMB, |
| 8)); // Almost 9 MB served. |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.method_ = "POST"; |
| req.uri_ = "/a.foo"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| ASSERT_EQ(200, resp.http_code_); |
| EXPECT_EQ(content, resp.content_); |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ(base::IntToString(content.size()), resp.headers_["Content-Length"]); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetEmptyFile) { |
| SetupDelegate(); |
| |
| WriteFile(testdir_path_.Append("empty.p2p"), "", 0); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| // The reported download speed should be 0 in this case, and no |
| // kP2PServerServedSuccessfullyMB is reported. |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, 0)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/empty"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(200, resp.http_code_); |
| EXPECT_EQ("", resp.content_); |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ("0", resp.headers_["Content-Length"]); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetRangeOfFile) { |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(60 * 1000, &content); |
| WriteFile(testdir_path_.Append("data.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 0)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 25)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/data"; |
| req.headers_["User-Agent"] = "The Unit Tester"; |
| req.headers_["Range"] = "bytes=15000-17000"; |
| // An extra header shouldn't affect. |
| req.headers_["X-Hello"] = "World"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| ASSERT_EQ(206, resp.http_code_); // Partial content. |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ("2001", resp.headers_["Content-Length"]); |
| EXPECT_EQ(content.substr(15000, 2001), resp.content_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetInvalidRangeOfFile) { |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(64 * 1000, &content); |
| WriteFile(testdir_path_.Append("data.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultMalformed)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/data"; |
| // The range starts in the file but the end is after the end of the file. |
| req.headers_["Range"] = "bytes=60000-70000"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(416, resp.http_code_); // Requested Range Not Satisfiable. |
| EXPECT_EQ("", resp.content_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetLastPartOfFile) { |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(5 * 1000, &content); |
| WriteFile(testdir_path_.Append("data.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 0)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 80)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/data"; |
| // HTTP headers aren't case sensitive and we should send only one. This tests |
| // the parser behaviour. |
| req.headers_["User-Agent"] = "The Unit Tester I"; |
| req.headers_["user-agent"] = "The Unit Tester II"; |
| req.headers_["UsEr-AgEnT"] = "The Unit Tester III"; |
| req.headers_["raNGE"] = "bytes=4000-"; |
| req.headers_["RangE"] = "bytes=4000-"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| ASSERT_EQ(206, resp.http_code_); // Partial content. |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ("1000", resp.headers_["Content-Length"]); |
| EXPECT_EQ(content.substr(4000, 1000), resp.content_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetIncompleteFile) { |
| if (!util::IsXAttrSupported(FilePath("/tmp"))) { |
| LOG(WARNING) << "Skipping test because /tmp does not support xattr. " |
| << "Please update your system to support this feature."; |
| return; |
| } |
| |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(50 * 1000, &content); |
| WriteFile(testdir_path_.Append("data.p2p"), content.c_str(), content.size()); |
| ASSERT_TRUE( |
| SetExpectedFileSize(testdir_path_.Append("data.p2p"), 100 * 1000)); |
| |
| EXPECT_CALL( |
| mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseInterrupted)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerServedInterruptedMB, 0)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/data"; |
| req.Send(client_fd_); |
| // Request the whole file (100KB), but try to read only the first 50KB. |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp, 50 * 1000)); |
| // Disconnect. |
| close(client_fd_); |
| client_fd_ = -1; |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| ASSERT_EQ(200, resp.http_code_); |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ("100000", resp.headers_["Content-Length"]); |
| EXPECT_EQ(content, resp.content_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetPartOfIncompleteFile) { |
| if (!util::IsXAttrSupported(FilePath("/tmp"))) { |
| LOG(WARNING) << "Skipping test because /tmp does not support xattr. " |
| << "Please update your system to support this feature."; |
| return; |
| } |
| |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(5 * 1000, &content); |
| WriteFile(testdir_path_.Append("data.p2p"), content.c_str(), content.size()); |
| ASSERT_TRUE(SetExpectedFileSize(testdir_path_.Append("data.p2p"), 10 * 1000)); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 0)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| // Total file size is 10KB, but actual file size is 5KB. A range starting |
| // at 2KB should be reported as 20%. |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 20)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/data"; |
| req.headers_["Range"] = "bytes=2000-4999"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| // Disconnect. |
| close(client_fd_); |
| client_fd_ = -1; |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| ASSERT_EQ(206, resp.http_code_); |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ("3000", resp.headers_["Content-Length"]); |
| EXPECT_EQ(content.substr(2000, 3000), resp.content_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, GetNonExistentFile) { |
| SetupDelegate(); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultNotFound)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/hello"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(404, resp.http_code_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, URIArgumentsNotParsed) { |
| SetupDelegate(); |
| |
| const string content = "Hello World!"; |
| WriteFile(testdir_path_.Append("hello.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultNotFound)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/hello?world"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(404, resp.http_code_); |
| } |
| |
| TEST_F(ConnectionDelegateTest, URIUsesRelativePath) { |
| SetupDelegate(); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultMalformed)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "//etc/passwd"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(400, resp.http_code_); // Bad request. |
| } |
| |
| TEST_F(ConnectionDelegateTest, MalformedInversedRange) { |
| SetupDelegate(); |
| |
| const string content = "Hello World!"; |
| WriteFile(testdir_path_.Append("hello.p2p"), content.c_str(), content.size()); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultMalformed)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/hello"; |
| // Request an inverted range. |
| req.headers_["Range"] = "bytes=9-5"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| EXPECT_EQ(400, resp.http_code_); // Bad request. |
| } |
| |
| // Tests that the ConnectionDelegate properly blocks when the end of the file |
| // is missing and continues serving the file when more data is available. |
| TEST_F(ConnectionDelegateTest, Waiting) { |
| if (!util::IsXAttrSupported(FilePath("/tmp"))) { |
| LOG(WARNING) << "Skipping test because /tmp does not support xattr. " |
| << "Please update your system to support this feature."; |
| return; |
| } |
| |
| SetupDelegate(); |
| |
| // The file starts with 50kB on disk, but expected total size of 100kB. The |
| // file is then extended to 75kB and finally to 100kB. |
| string content; |
| GeneratePrintableData(100 * 1000, &content); |
| WriteFile(testdir_path_.Append("wait.p2p"), content.c_str(), 50 * 1000); |
| ASSERT_TRUE( |
| SetExpectedFileSize(testdir_path_.Append("wait.p2p"), 100 * 1000)); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 0)); |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, _)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/wait"; |
| req.Send(client_fd_); |
| string text_resp; |
| // Expect to read the first 50kB with a valid HTTP response for the |
| // total size of 100kB. |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp, 50 * 1000)); |
| |
| HTTPResponse resp(text_resp); |
| ASSERT_TRUE(resp.valid_); |
| ASSERT_EQ(200, resp.http_code_); |
| EXPECT_EQ("application/octet-stream", resp.headers_["Content-Type"]); |
| EXPECT_EQ("100000", resp.headers_["Content-Length"]); |
| |
| // Extend the file to 75kB. |
| int fd = open(testdir_path_.Append("wait.p2p").value().c_str(), |
| O_WRONLY | O_APPEND); |
| EXPECT_NE(fd, -1); |
| EXPECT_EQ(25 * 1000, write(fd, content.c_str() + 50 * 1000, 25 * 1000)); |
| |
| // Expect to reach 75kB on the reader side. |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp, 75 * 1000)); |
| |
| HTTPResponse middle_resp(text_resp); |
| ASSERT_TRUE(middle_resp.valid_); |
| EXPECT_EQ(middle_resp.content_.size(), 75 * 1000); |
| |
| // Extend the file to its total expected size and expect the server to close |
| // the connection right after serving the total size. |
| EXPECT_EQ(25 * 1000, write(fd, content.c_str() + 75 * 1000, 25 * 1000)); |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| |
| EXPECT_EQ(0, close(fd)); |
| thread_->Join(); |
| |
| HTTPResponse full_resp(text_resp); |
| ASSERT_TRUE(full_resp.valid_); |
| EXPECT_EQ(full_resp.content_.size(), 100 * 1000); |
| EXPECT_EQ(full_resp.content_, content); |
| } |
| |
| TEST_F(ConnectionDelegateTest, LimitDownloadSpeed) { |
| if (!util::IsXAttrSupported(FilePath("/tmp"))) { |
| LOG(WARNING) << "Skipping test because /tmp does not support xattr. " |
| << "Please update your system to support this feature."; |
| return; |
| } |
| |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(50 * 1000 * 1000, &content); |
| WriteFile(testdir_path_.Append("50mb.p2p"), content.c_str(), content.size()); |
| ASSERT_TRUE( |
| SetExpectedFileSize(testdir_path_.Append("50mb.p2p"), content.size())); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 50)); |
| // The reported download speed should be the maximum default speed used in |
| // this test (kDefaultDownloadRate). |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerDownloadSpeedKBps, 5000)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/50mb"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| thread_->Join(); |
| |
| // Don't need to parse the response. Just expect it to have the 50MB plus the |
| // header size. |
| EXPECT_GE(text_resp.size(), 50 * 1000 * 1000); |
| |
| // Since the file was already complete at the begining of the test, the |
| // sleeping time should be only 10s (50MB / 5 MB/s). A minimum tolerance is |
| // added to avoid floating-point errors. |
| EXPECT_GE(clock_.GetSleptTime().InSecondsF(), 9.999); |
| EXPECT_LE(clock_.GetSleptTime().InSecondsF(), 10.001); |
| } |
| |
| TEST_F(ConnectionDelegateTest, DisregardTimeWaitingFromTransferBudget) { |
| if (!util::IsXAttrSupported(FilePath("/tmp"))) { |
| LOG(WARNING) << "Skipping test because /tmp does not support xattr. " |
| << "Please update your system to support this feature."; |
| return; |
| } |
| |
| SetupDelegate(); |
| |
| string content; |
| GeneratePrintableData(25 * 1000 * 1000, &content); |
| WriteFile(testdir_path_.Append("50mb.p2p"), content.c_str(), content.size()); |
| ASSERT_TRUE( |
| SetExpectedFileSize(testdir_path_.Append("50mb.p2p"), 50 * 1000 * 1000)); |
| |
| EXPECT_CALL(mock_server_, |
| ReportServerMessage(p2p::util::kP2PServerRequestResult, |
| p2p::util::kP2PRequestResultResponseSent)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerServedSuccessfullyMB, 50)); |
| // The reported download speed should be the maximum default speed used in |
| // this test (kDefaultDownloadRate). |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerDownloadSpeedKBps, 5000)); |
| EXPECT_CALL(mock_server_, ReportServerMessage( |
| p2p::util::kP2PServerRangeBeginPercentage, 0)); |
| EXPECT_CALL(mock_server_, ConnectionTerminated(delegate_)); |
| |
| thread_->Start(); |
| HTTPRequest req; |
| req.uri_ = "/50mb"; |
| req.Send(client_fd_); |
| string text_resp; |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp, 25 * 1000 * 1000)); |
| |
| // At this point, the ConnectionDelegate is waiting on data to be read from |
| // the file, but EOF is reached so it will try to call clock->Sleep() to |
| // sleep for a second waiting for data to be ready. After the last byte is |
| // sent to the socket, the ConnectionDelegate waits to reach the right |
| // speed and once EOF on the input is reached it will wait again until some |
| // data is ready to read from there. To ensure we saw at least one Sleep() |
| // call because of the later condition, block twice until Sleep() is called. |
| clock_.BlockUntilSleepIsCalled(); |
| clock_.BlockUntilSleepIsCalled(); |
| |
| // Extend the file to its total expected size and expect the server to close |
| // the connection right after serving the total size. |
| int fd = open(testdir_path_.Append("50mb.p2p").value().c_str(), |
| O_WRONLY | O_APPEND); |
| EXPECT_NE(fd, -1); |
| EXPECT_EQ(content.size(), write(fd, content.c_str(), content.size())); |
| EXPECT_EQ(0, close(fd)); |
| |
| EXPECT_TRUE(ReadHTTPResponse(client_fd_, &text_resp)); |
| |
| thread_->Join(); |
| |
| // Don't need to parse the response. Just expect it to have the 50MB plus the |
| // header size. |
| EXPECT_GE(text_resp.size(), 50 * 1000 * 1000); |
| } |
| |
| } // namespace http_server |
| |
| } // namespace p2p |