// 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_transport_curl.h>
#include <limits>
#include <base/bind.h>
#include <base/logging.h>
#include <base/message_loop/message_loop.h>
#include <brillo/http/http_connection_curl.h>
#include <brillo/http/http_request.h>
#include <brillo/strings/string_utils.h>
namespace {
const char kCACertificatePath[] =
#ifdef __ANDROID__
"/system/etc/security/cacerts_google";
#else
"/usr/share/brillo-ca-certificates";
#endif
} // namespace
namespace brillo {
namespace http {
namespace curl {
// This is a class that stores connection data on particular CURL socket
// and provides file descriptor watcher to monitor read and/or write operations
// on the socket's file descriptor.
class Transport::SocketPollData : public base::MessageLoopForIO::Watcher {
public:
SocketPollData(const std::shared_ptr<CurlInterface>& curl_interface,
CURLM* curl_multi_handle,
Transport* transport,
curl_socket_t socket_fd)
: curl_interface_(curl_interface),
curl_multi_handle_(curl_multi_handle),
transport_(transport),
socket_fd_(socket_fd) {}
// Returns the pointer for the socket-specific file descriptor watcher.
base::MessageLoopForIO::FileDescriptorWatcher* GetWatcher() {
return &file_descriptor_watcher_;
}
private:
// Overrides from base::MessageLoopForIO::Watcher.
void OnFileCanReadWithoutBlocking(int fd) override {
OnSocketReady(fd, CURL_CSELECT_IN);
}
void OnFileCanWriteWithoutBlocking(int fd) override {
OnSocketReady(fd, CURL_CSELECT_OUT);
}
// Data on the socket is available to be read from or written to.
// Notify CURL of the action it needs to take on the socket file descriptor.
void OnSocketReady(int fd, int action) {
CHECK_EQ(socket_fd_, fd) << "Unexpected socket file descriptor";
int still_running_count = 0;
CURLMcode code = curl_interface_->MultiSocketAction(
curl_multi_handle_, socket_fd_, action, &still_running_count);
CHECK_NE(CURLM_CALL_MULTI_PERFORM, code)
<< "CURL should no longer return CURLM_CALL_MULTI_PERFORM here";
if (code == CURLM_OK)
transport_->ProcessAsyncCurlMessages();
}
// The CURL interface to use.
std::shared_ptr<CurlInterface> curl_interface_;
// CURL multi-handle associated with the transport.
CURLM* curl_multi_handle_;
// Transport object itself.
Transport* transport_;
// The socket file descriptor for the connection.
curl_socket_t socket_fd_;
// File descriptor watcher to notify us of asynchronous I/O on the FD.
base::MessageLoopForIO::FileDescriptorWatcher file_descriptor_watcher_;
DISALLOW_COPY_AND_ASSIGN(SocketPollData);
};
// The request data associated with an asynchronous operation on a particular
// connection.
struct Transport::AsyncRequestData {
// Success/error callbacks to be invoked at the end of the request.
SuccessCallback success_callback;
ErrorCallback error_callback;
// We store a connection here to make sure the object is alive for
// as long as asynchronous operation is running.
std::shared_ptr<Connection> connection;
// The ID of this request.
RequestID request_id;
};
Transport::Transport(const std::shared_ptr<CurlInterface>& curl_interface)
: curl_interface_{curl_interface} {
VLOG(2) << "curl::Transport created";
}
Transport::Transport(const std::shared_ptr<CurlInterface>& curl_interface,
const std::string& proxy)
: curl_interface_{curl_interface}, proxy_{proxy} {
VLOG(2) << "curl::Transport created with proxy " << proxy;
}
Transport::~Transport() {
ShutDownAsyncCurl();
VLOG(2) << "curl::Transport destroyed";
}
std::shared_ptr<http::Connection> Transport::CreateConnection(
const std::string& url,
const std::string& method,
const HeaderList& headers,
const std::string& user_agent,
const std::string& referer,
brillo::ErrorPtr* error) {
std::shared_ptr<http::Connection> connection;
CURL* curl_handle = curl_interface_->EasyInit();
if (!curl_handle) {
LOG(ERROR) << "Failed to initialize CURL";
brillo::Error::AddTo(error, FROM_HERE, http::kErrorDomain,
"curl_init_failed", "Failed to initialize CURL");
return connection;
}
LOG(INFO) << "Sending a " << method << " request to " << url;
CURLcode code = curl_interface_->EasySetOptStr(curl_handle, CURLOPT_URL, url);
if (code == CURLE_OK) {
code = curl_interface_->EasySetOptStr(curl_handle, CURLOPT_CAPATH,
kCACertificatePath);
}
if (code == CURLE_OK) {
code =
curl_interface_->EasySetOptInt(curl_handle, CURLOPT_SSL_VERIFYPEER, 1);
}
if (code == CURLE_OK) {
code =
curl_interface_->EasySetOptInt(curl_handle, CURLOPT_SSL_VERIFYHOST, 2);
}
if (code == CURLE_OK && !user_agent.empty()) {
code = curl_interface_->EasySetOptStr(
curl_handle, CURLOPT_USERAGENT, user_agent);
}
if (code == CURLE_OK && !referer.empty()) {
code =
curl_interface_->EasySetOptStr(curl_handle, CURLOPT_REFERER, referer);
}
if (code == CURLE_OK && !proxy_.empty()) {
code = curl_interface_->EasySetOptStr(curl_handle, CURLOPT_PROXY, proxy_);
}
if (code == CURLE_OK) {
int64_t timeout_ms = connection_timeout_.InMillisecondsRoundedUp();
if (timeout_ms > 0 && timeout_ms <= std::numeric_limits<int>::max()) {
code = curl_interface_->EasySetOptInt(
curl_handle, CURLOPT_TIMEOUT_MS,
static_cast<int>(timeout_ms));
}
}
// Setup HTTP request method and optional request body.
if (code == CURLE_OK) {
if (method == request_type::kGet) {
code = curl_interface_->EasySetOptInt(curl_handle, CURLOPT_HTTPGET, 1);
} else if (method == request_type::kHead) {
code = curl_interface_->EasySetOptInt(curl_handle, CURLOPT_NOBODY, 1);
} else if (method == request_type::kPut) {
code = curl_interface_->EasySetOptInt(curl_handle, CURLOPT_UPLOAD, 1);
} else {
// POST and custom request methods
code = curl_interface_->EasySetOptInt(curl_handle, CURLOPT_POST, 1);
if (code == CURLE_OK) {
code = curl_interface_->EasySetOptPtr(
curl_handle, CURLOPT_POSTFIELDS, nullptr);
}
if (code == CURLE_OK && method != request_type::kPost) {
code = curl_interface_->EasySetOptStr(
curl_handle, CURLOPT_CUSTOMREQUEST, method);
}
}
}
if (code != CURLE_OK) {
AddEasyCurlError(error, FROM_HERE, code, curl_interface_.get());
curl_interface_->EasyCleanup(curl_handle);
return connection;
}
connection = std::make_shared<http::curl::Connection>(
curl_handle, method, curl_interface_, shared_from_this());
if (!connection->SendHeaders(headers, error)) {
connection.reset();
}
return connection;
}
void Transport::RunCallbackAsync(const tracked_objects::Location& from_here,
const base::Closure& callback) {
base::MessageLoopForIO::current()->PostTask(from_here, callback);
}
RequestID Transport::StartAsyncTransfer(http::Connection* connection,
const SuccessCallback& success_callback,
const ErrorCallback& error_callback) {
brillo::ErrorPtr error;
if (!SetupAsyncCurl(&error)) {
RunCallbackAsync(
FROM_HERE, base::Bind(error_callback, 0, base::Owned(error.release())));
return 0;
}
RequestID request_id = ++last_request_id_;
auto curl_connection = static_cast<http::curl::Connection*>(connection);
std::unique_ptr<AsyncRequestData> request_data{new AsyncRequestData};
// Add the request data to |async_requests_| before adding the CURL handle
// in case CURL feels like calling the socket callback synchronously which
// will need the data to be in |async_requests_| map already.
request_data->success_callback = success_callback;
request_data->error_callback = error_callback;
request_data->connection =
std::static_pointer_cast<Connection>(curl_connection->shared_from_this());
request_data->request_id = request_id;
async_requests_.emplace(curl_connection, std::move(request_data));
request_id_map_.emplace(request_id, curl_connection);
// Add the connection's CURL handle to the multi-handle.
CURLMcode code = curl_interface_->MultiAddHandle(
curl_multi_handle_, curl_connection->curl_handle_);
if (code != CURLM_OK) {
brillo::ErrorPtr error;
AddMultiCurlError(&error, FROM_HERE, code, curl_interface_.get());
RunCallbackAsync(
FROM_HERE, base::Bind(error_callback, 0, base::Owned(error.release())));
async_requests_.erase(curl_connection);
request_id_map_.erase(request_id);
return 0;
}
LOG(INFO) << "Started asynchronous HTTP request with ID " << request_id;
return request_id;
}
bool Transport::CancelRequest(RequestID request_id) {
auto p = request_id_map_.find(request_id);
if (p == request_id_map_.end()) {
// The request must have been completed already...
// This is not necessarily an error condition, so fail gracefully.
LOG(WARNING) << "HTTP request #" << request_id << " not found";
return false;
}
LOG(INFO) << "Canceling HTTP request #" << request_id;
CleanAsyncConnection(p->second);
return true;
}
void Transport::SetDefaultTimeout(base::TimeDelta timeout) {
connection_timeout_ = timeout;
}
void Transport::AddEasyCurlError(brillo::ErrorPtr* error,
const tracked_objects::Location& location,
CURLcode code,
CurlInterface* curl_interface) {
brillo::Error::AddTo(error, location, "curl_easy_error",
brillo::string_utils::ToString(code),
curl_interface->EasyStrError(code));
}
void Transport::AddMultiCurlError(brillo::ErrorPtr* error,
const tracked_objects::Location& location,
CURLMcode code,
CurlInterface* curl_interface) {
brillo::Error::AddTo(error, location, "curl_multi_error",
brillo::string_utils::ToString(code),
curl_interface->MultiStrError(code));
}
bool Transport::SetupAsyncCurl(brillo::ErrorPtr* error) {
if (curl_multi_handle_)
return true;
curl_multi_handle_ = curl_interface_->MultiInit();
if (!curl_multi_handle_) {
LOG(ERROR) << "Failed to initialize CURL";
brillo::Error::AddTo(error, FROM_HERE, http::kErrorDomain,
"curl_init_failed", "Failed to initialize CURL");
return false;
}
CURLMcode code = curl_interface_->MultiSetSocketCallback(
curl_multi_handle_, &Transport::MultiSocketCallback, this);
if (code == CURLM_OK) {
code = curl_interface_->MultiSetTimerCallback(
curl_multi_handle_, &Transport::MultiTimerCallback, this);
}
if (code != CURLM_OK) {
AddMultiCurlError(error, FROM_HERE, code, curl_interface_.get());
return false;
}
return true;
}
void Transport::ShutDownAsyncCurl() {
if (!curl_multi_handle_)
return;
LOG_IF(WARNING, !poll_data_map_.empty())
<< "There are pending requests at the time of transport's shutdown";
// Make sure we are not leaking any memory here.
for (const auto& pair : poll_data_map_)
delete pair.second;
poll_data_map_.clear();
curl_interface_->MultiCleanup(curl_multi_handle_);
curl_multi_handle_ = nullptr;
}
int Transport::MultiSocketCallback(CURL* easy,
curl_socket_t s,
int what,
void* userp,
void* socketp) {
auto transport = static_cast<Transport*>(userp);
CHECK(transport) << "Transport must be set for this callback";
auto poll_data = static_cast<SocketPollData*>(socketp);
if (!poll_data) {
// We haven't attached polling data to this socket yet. Let's do this now.
poll_data = new SocketPollData{transport->curl_interface_,
transport->curl_multi_handle_,
transport,
s};
transport->poll_data_map_.emplace(std::make_pair(easy, s), poll_data);
transport->curl_interface_->MultiAssign(
transport->curl_multi_handle_, s, poll_data);
}
if (what == CURL_POLL_NONE) {
return 0;
} else if (what == CURL_POLL_REMOVE) {
// Remove the attached data from the socket.
transport->curl_interface_->MultiAssign(
transport->curl_multi_handle_, s, nullptr);
transport->poll_data_map_.erase(std::make_pair(easy, s));
// Make sure we stop watching the socket file descriptor now, before
// we schedule the SocketPollData for deletion.
poll_data->GetWatcher()->StopWatchingFileDescriptor();
// This method can be called indirectly from SocketPollData::OnSocketReady,
// so delay destruction of SocketPollData object till the next loop cycle.
base::MessageLoopForIO::current()->DeleteSoon(FROM_HERE, poll_data);
return 0;
}
base::MessageLoopForIO::Mode watch_mode = base::MessageLoopForIO::WATCH_READ;
switch (what) {
case CURL_POLL_IN:
watch_mode = base::MessageLoopForIO::WATCH_READ;
break;
case CURL_POLL_OUT:
watch_mode = base::MessageLoopForIO::WATCH_WRITE;
break;
case CURL_POLL_INOUT:
watch_mode = base::MessageLoopForIO::WATCH_READ_WRITE;
break;
default:
LOG(FATAL) << "Unknown CURL socket action: " << what;
break;
}
// WatchFileDescriptor() can be called with the same controller object
// (watcher) to amend the watch mode, however this has cumulative effect.
// For example, if we were watching a file descriptor for READ operations
// and now call it to watch for WRITE, it will end up watching for both
// READ and WRITE. This is not what we want here, so stop watching the
// file descriptor on previous controller before starting with a different
// mode.
if (!poll_data->GetWatcher()->StopWatchingFileDescriptor())
LOG(WARNING) << "Failed to stop watching the previous socket descriptor";
CHECK(base::MessageLoopForIO::current()->WatchFileDescriptor(
s, true, watch_mode, poll_data->GetWatcher(), poll_data))
<< "Failed to watch the CURL socket.";
return 0;
}
// CURL actually uses "long" types in callback signatures, so we must comply.
int Transport::MultiTimerCallback(CURLM* /* multi */,
long timeout_ms, // NOLINT(runtime/int)
void* userp) {
auto transport = static_cast<Transport*>(userp);
// Cancel any previous timer callbacks.
transport->weak_ptr_factory_for_timer_.InvalidateWeakPtrs();
if (timeout_ms >= 0) {
base::MessageLoopForIO::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&Transport::OnTimer,
transport->weak_ptr_factory_for_timer_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(timeout_ms));
}
return 0;
}
void Transport::OnTimer() {
if (curl_multi_handle_) {
int still_running_count = 0;
curl_interface_->MultiSocketAction(
curl_multi_handle_, CURL_SOCKET_TIMEOUT, 0, &still_running_count);
ProcessAsyncCurlMessages();
}
}
void Transport::ProcessAsyncCurlMessages() {
CURLMsg* msg = nullptr;
int msgs_left = 0;
while ((msg = curl_interface_->MultiInfoRead(curl_multi_handle_,
&msgs_left))) {
if (msg->msg == CURLMSG_DONE) {
// Async I/O complete for a connection. Invoke the user callbacks.
Connection* connection = nullptr;
CHECK_EQ(CURLE_OK,
curl_interface_->EasyGetInfoPtr(
msg->easy_handle,
CURLINFO_PRIVATE,
reinterpret_cast<void**>(&connection)));
CHECK(connection != nullptr);
OnTransferComplete(connection, msg->data.result);
}
}
}
void Transport::OnTransferComplete(Connection* connection, CURLcode code) {
auto p = async_requests_.find(connection);
CHECK(p != async_requests_.end()) << "Unknown connection";
AsyncRequestData* request_data = p->second.get();
LOG(INFO) << "HTTP request # " << request_data->request_id
<< " has completed "
<< (code == CURLE_OK ? "successfully" : "with an error");
if (code != CURLE_OK) {
brillo::ErrorPtr error;
AddEasyCurlError(&error, FROM_HERE, code, curl_interface_.get());
RunCallbackAsync(FROM_HERE,
base::Bind(request_data->error_callback,
p->second->request_id,
base::Owned(error.release())));
} else {
LOG(INFO) << "Response: " << connection->GetResponseStatusCode() << " ("
<< connection->GetResponseStatusText() << ")";
brillo::ErrorPtr error;
// Rewind the response data stream to the beginning so the clients can
// read the data back.
const auto& stream = request_data->connection->response_data_stream_;
if (stream && stream->CanSeek() && !stream->SetPosition(0, &error)) {
RunCallbackAsync(FROM_HERE,
base::Bind(request_data->error_callback,
p->second->request_id,
base::Owned(error.release())));
} else {
std::unique_ptr<Response> resp{new Response{request_data->connection}};
RunCallbackAsync(FROM_HERE,
base::Bind(request_data->success_callback,
p->second->request_id,
base::Passed(&resp)));
}
}
// In case of an error on CURL side, we would have dispatched the error
// callback and we need to clean up the current connection, however the
// error callback has no reference to the connection itself and
// |async_requests_| is the only reference to the shared pointer that
// maintains the lifetime of |connection| and possibly even this Transport
// object instance. As a result, if we call CleanAsyncConnection() directly,
// there is a chance that this object might be deleted.
// Instead, schedule an asynchronous task to clean up the connection.
RunCallbackAsync(FROM_HERE,
base::Bind(&Transport::CleanAsyncConnection,
weak_ptr_factory_.GetWeakPtr(),
connection));
}
void Transport::CleanAsyncConnection(Connection* connection) {
auto p = async_requests_.find(connection);
CHECK(p != async_requests_.end()) << "Unknown connection";
// Remove the request data from the map first, since this might be the only
// reference to the Connection class and even possibly to this Transport.
auto request_data = std::move(p->second);
// Remove associated request ID.
request_id_map_.erase(request_data->request_id);
// Remove the connection's CURL handle from multi-handle.
curl_interface_->MultiRemoveHandle(curl_multi_handle_,
connection->curl_handle_);
// Remove all the socket data associated with this connection.
auto iter = poll_data_map_.begin();
while (iter != poll_data_map_.end()) {
if (iter->first.first == connection->curl_handle_)
iter = poll_data_map_.erase(iter);
else
++iter;
}
// Remove pending asynchronous request data.
// This must be last since there is a chance of this object being
// destroyed as the result. See the comment in Transport::OnTransferComplete.
async_requests_.erase(p);
}
} // namespace curl
} // namespace http
} // namespace brillo