You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
326 lines
12 KiB
326 lines
12 KiB
// 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 <utility>
|
|
|
|
#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
|