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