| // Copyright 2014 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 <brillo/http/http_connection_curl.h> |
| |
| #include <algorithm> |
| #include <set> |
| |
| #include <base/callback.h> |
| #include <brillo/http/http_request.h> |
| #include <brillo/http/http_transport.h> |
| #include <brillo/http/mock_curl_api.h> |
| #include <brillo/http/mock_transport.h> |
| #include <brillo/streams/memory_stream.h> |
| #include <brillo/streams/mock_stream.h> |
| #include <brillo/strings/string_utils.h> |
| #include <brillo/mime_utils.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| using testing::DoAll; |
| using testing::Invoke; |
| using testing::Return; |
| using testing::SetArgPointee; |
| using testing::_; |
| |
| namespace brillo { |
| namespace http { |
| namespace curl { |
| |
| namespace { |
| |
| using ReadWriteCallback = |
| size_t(char* ptr, size_t size, size_t num, void* data); |
| |
| // A helper class to simulate curl_easy_perform action. It invokes the |
| // read callbacks to obtain the request data from the Connection and then |
| // calls the header and write callbacks to "send" the response header and body. |
| class CurlPerformer { |
| public: |
| // During the tests, use the address of |this| as the CURL* handle. |
| // This allows the static Perform() method to obtain the instance pointer |
| // having only CURL*. |
| CURL* GetCurlHandle() { return reinterpret_cast<CURL*>(this); } |
| |
| // Callback to be invoked when mocking out curl_easy_perform() method. |
| static CURLcode Perform(CURL* curl) { |
| CurlPerformer* me = reinterpret_cast<CurlPerformer*>(curl); |
| return me->DoPerform(); |
| } |
| |
| // CURL callback functions and |connection| pointer needed to invoke the |
| // callbacks from the Connection class. |
| Connection* connection{nullptr}; |
| ReadWriteCallback* write_callback{nullptr}; |
| ReadWriteCallback* read_callback{nullptr}; |
| ReadWriteCallback* header_callback{nullptr}; |
| |
| // Request body read from the connection. |
| std::string request_body; |
| |
| // Response data to be sent back to connection. |
| std::string status_line; |
| HeaderList response_headers; |
| std::string response_body; |
| |
| private: |
| // The actual implementation of curl_easy_perform() fake. |
| CURLcode DoPerform() { |
| // Read request body. |
| char buffer[1024]; |
| for (;;) { |
| size_t size_read = read_callback(buffer, sizeof(buffer), 1, connection); |
| if (size_read == CURL_READFUNC_ABORT) |
| return CURLE_ABORTED_BY_CALLBACK; |
| if (size_read == CURL_READFUNC_PAUSE) |
| return CURLE_READ_ERROR; // Shouldn't happen. |
| if (size_read == 0) |
| break; |
| request_body.append(buffer, size_read); |
| } |
| |
| // Send the response headers. |
| std::vector<std::string> header_lines; |
| header_lines.push_back(status_line + "\r\n"); |
| for (const auto& pair : response_headers) { |
| header_lines.push_back(string_utils::Join(": ", pair.first, pair.second) + |
| "\r\n"); |
| } |
| |
| for (const std::string& line : header_lines) { |
| CURLcode code = WriteString(header_callback, line); |
| if (code != CURLE_OK) |
| return code; |
| } |
| |
| // Send response body. |
| return WriteString(write_callback, response_body); |
| } |
| |
| // Helper method to send a string to a write callback. Keeps calling |
| // the callback until all the data is written. |
| CURLcode WriteString(ReadWriteCallback* callback, const std::string& str) { |
| size_t pos = 0; |
| size_t size_remaining = str.size(); |
| while (size_remaining) { |
| size_t size_written = callback( |
| const_cast<char*>(str.data() + pos), size_remaining, 1, connection); |
| if (size_written == CURL_WRITEFUNC_PAUSE) |
| return CURLE_WRITE_ERROR; // Shouldn't happen. |
| CHECK(size_written <= size_remaining) << "Unexpected size returned"; |
| size_remaining -= size_written; |
| pos += size_written; |
| } |
| return CURLE_OK; |
| } |
| }; |
| |
| // Custom matcher to validate the parameter of CURLOPT_HTTPHEADER CURL option |
| // which contains the request headers as curl_slist* chain. |
| MATCHER_P(HeadersMatch, headers, "") { |
| std::multiset<std::string> test_headers; |
| for (const auto& pair : headers) |
| test_headers.insert(string_utils::Join(": ", pair.first, pair.second)); |
| |
| std::multiset<std::string> src_headers; |
| const curl_slist* head = static_cast<const curl_slist*>(arg); |
| while (head) { |
| src_headers.insert(head->data); |
| head = head->next; |
| } |
| |
| std::vector<std::string> difference; |
| std::set_symmetric_difference(src_headers.begin(), src_headers.end(), |
| test_headers.begin(), test_headers.end(), |
| std::back_inserter(difference)); |
| return difference.empty(); |
| } |
| |
| // Custom action to save a CURL callback pointer into a member of CurlPerformer. |
| ACTION_TEMPLATE(SaveCallback, |
| HAS_1_TEMPLATE_PARAMS(int, k), |
| AND_2_VALUE_PARAMS(performer, mem_ptr)) { |
| performer->*mem_ptr = reinterpret_cast<ReadWriteCallback*>(std::get<k>(args)); |
| } |
| |
| } // anonymous namespace |
| |
| class HttpCurlConnectionTest : public testing::Test { |
| public: |
| void SetUp() override { |
| curl_api_ = std::make_shared<MockCurlInterface>(); |
| transport_ = std::make_shared<MockTransport>(); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_PRIVATE, _)) |
| .WillOnce(Return(CURLE_OK)); |
| connection_ = std::make_shared<Connection>( |
| handle_, request_type::kPost, curl_api_, transport_); |
| performer_.connection = connection_.get(); |
| } |
| |
| void TearDown() override { |
| EXPECT_CALL(*curl_api_, EasyCleanup(handle_)).Times(1); |
| connection_.reset(); |
| transport_.reset(); |
| curl_api_.reset(); |
| } |
| |
| protected: |
| std::shared_ptr<MockCurlInterface> curl_api_; |
| std::shared_ptr<MockTransport> transport_; |
| CurlPerformer performer_; |
| CURL* handle_{performer_.GetCurlHandle()}; |
| std::shared_ptr<Connection> connection_; |
| }; |
| |
| TEST_F(HttpCurlConnectionTest, FinishRequestAsync) { |
| std::string request_data{"Foo Bar Baz"}; |
| StreamPtr stream = MemoryStream::OpenRef(request_data, nullptr); |
| EXPECT_TRUE(connection_->SetRequestData(std::move(stream), nullptr)); |
| EXPECT_TRUE(connection_->SendHeaders({{"X-Foo", "bar"}}, nullptr)); |
| |
| if (VLOG_IS_ON(3)) { |
| EXPECT_CALL(*curl_api_, |
| EasySetOptCallback(handle_, CURLOPT_DEBUGFUNCTION, _)) |
| .WillOnce(Return(CURLE_OK)); |
| EXPECT_CALL(*curl_api_, EasySetOptInt(handle_, CURLOPT_VERBOSE, 1)) |
| .WillOnce(Return(CURLE_OK)); |
| } |
| |
| EXPECT_CALL( |
| *curl_api_, |
| EasySetOptOffT(handle_, CURLOPT_POSTFIELDSIZE_LARGE, request_data.size())) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, EasySetOptCallback(handle_, CURLOPT_READFUNCTION, _)) |
| .WillOnce(Return(CURLE_OK)); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_READDATA, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_HTTPHEADER, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, EasySetOptCallback(handle_, CURLOPT_WRITEFUNCTION, _)) |
| .WillOnce(Return(CURLE_OK)); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_WRITEDATA, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, |
| EasySetOptCallback(handle_, CURLOPT_HEADERFUNCTION, _)) |
| .WillOnce(Return(CURLE_OK)); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_HEADERDATA, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*transport_, StartAsyncTransfer(connection_.get(), _, _)) |
| .Times(1); |
| connection_->FinishRequestAsync({}, {}); |
| } |
| |
| MATCHER_P(MatchStringBuffer, data, "") { |
| return data.compare(static_cast<const char*>(arg)) == 0; |
| } |
| |
| TEST_F(HttpCurlConnectionTest, FinishRequest) { |
| std::string request_data{"Foo Bar Baz"}; |
| std::string response_data{"<html><body>OK</body></html>"}; |
| StreamPtr stream = MemoryStream::OpenRef(request_data, nullptr); |
| HeaderList headers{ |
| {request_header::kAccept, "*/*"}, |
| {request_header::kContentType, mime::text::kPlain}, |
| {request_header::kContentLength, std::to_string(request_data.size())}, |
| {"X-Foo", "bar"}, |
| }; |
| std::unique_ptr<MockStream> response_stream(new MockStream); |
| EXPECT_CALL(*response_stream, |
| WriteAllBlocking(MatchStringBuffer(response_data), |
| response_data.size(), _)) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(*response_stream, CanSeek()) |
| .WillOnce(Return(false)); |
| connection_->SetResponseData(std::move(response_stream)); |
| EXPECT_TRUE(connection_->SetRequestData(std::move(stream), nullptr)); |
| EXPECT_TRUE(connection_->SendHeaders(headers, nullptr)); |
| |
| // Expectations for Connection::FinishRequest() call. |
| if (VLOG_IS_ON(3)) { |
| EXPECT_CALL(*curl_api_, |
| EasySetOptCallback(handle_, CURLOPT_DEBUGFUNCTION, _)) |
| .WillOnce(Return(CURLE_OK)); |
| EXPECT_CALL(*curl_api_, EasySetOptInt(handle_, CURLOPT_VERBOSE, 1)) |
| .WillOnce(Return(CURLE_OK)); |
| } |
| |
| EXPECT_CALL( |
| *curl_api_, |
| EasySetOptOffT(handle_, CURLOPT_POSTFIELDSIZE_LARGE, request_data.size())) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, EasySetOptCallback(handle_, CURLOPT_READFUNCTION, _)) |
| .WillOnce( |
| DoAll(SaveCallback<2>(&performer_, &CurlPerformer::read_callback), |
| Return(CURLE_OK))); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_READDATA, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, |
| EasySetOptPtr(handle_, CURLOPT_HTTPHEADER, HeadersMatch(headers))) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, EasySetOptCallback(handle_, CURLOPT_WRITEFUNCTION, _)) |
| .WillOnce( |
| DoAll(SaveCallback<2>(&performer_, &CurlPerformer::write_callback), |
| Return(CURLE_OK))); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_WRITEDATA, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, |
| EasySetOptCallback(handle_, CURLOPT_HEADERFUNCTION, _)) |
| .WillOnce( |
| DoAll(SaveCallback<2>(&performer_, &CurlPerformer::header_callback), |
| Return(CURLE_OK))); |
| EXPECT_CALL(*curl_api_, EasySetOptPtr(handle_, CURLOPT_HEADERDATA, _)) |
| .WillOnce(Return(CURLE_OK)); |
| |
| EXPECT_CALL(*curl_api_, EasyPerform(handle_)) |
| .WillOnce(Invoke(&CurlPerformer::Perform)); |
| |
| EXPECT_CALL(*curl_api_, EasyGetInfoInt(handle_, CURLINFO_RESPONSE_CODE, _)) |
| .WillOnce(DoAll(SetArgPointee<2>(status_code::Ok), Return(CURLE_OK))); |
| |
| // Set up the CurlPerformer with the response data expected to be received. |
| HeaderList response_headers{ |
| {response_header::kContentLength, std::to_string(response_data.size())}, |
| {response_header::kContentType, mime::text::kHtml}, |
| {"X-Foo", "baz"}, |
| }; |
| performer_.status_line = "HTTP/1.1 200 OK"; |
| performer_.response_body = response_data; |
| performer_.response_headers = response_headers; |
| |
| // Perform the request. |
| EXPECT_TRUE(connection_->FinishRequest(nullptr)); |
| |
| // Make sure we sent out the request body correctly. |
| EXPECT_EQ(request_data, performer_.request_body); |
| |
| // Validate the parsed response data. |
| EXPECT_CALL(*curl_api_, EasyGetInfoInt(handle_, CURLINFO_RESPONSE_CODE, _)) |
| .WillOnce(DoAll(SetArgPointee<2>(status_code::Ok), Return(CURLE_OK))); |
| EXPECT_EQ(status_code::Ok, connection_->GetResponseStatusCode()); |
| EXPECT_EQ("HTTP/1.1", connection_->GetProtocolVersion()); |
| EXPECT_EQ("OK", connection_->GetResponseStatusText()); |
| EXPECT_EQ(std::to_string(response_data.size()), |
| connection_->GetResponseHeader(response_header::kContentLength)); |
| EXPECT_EQ(mime::text::kHtml, |
| connection_->GetResponseHeader(response_header::kContentType)); |
| EXPECT_EQ("baz", connection_->GetResponseHeader("X-Foo")); |
| auto data_stream = connection_->ExtractDataStream(nullptr); |
| ASSERT_NE(nullptr, data_stream.get()); |
| } |
| |
| } // namespace curl |
| } // namespace http |
| } // namespace brillo |