| // Copyright 2019 The Fuchsia 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 "src/developer/debug/shared/curl.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include "src/developer/debug/shared/fd_watcher.h" |
| #include "src/developer/debug/shared/message_loop.h" |
| |
| namespace debug_ipc { |
| |
| namespace { |
| |
| bool global_initialized = false; |
| |
| template <typename T> |
| void curl_easy_setopt_CHECK(CURL* handle, CURLoption option, T t) { |
| auto result = curl_easy_setopt(handle, option, t); |
| FX_DCHECK(result == CURLE_OK); |
| } |
| |
| } // namespace |
| |
| // All Curl instances share one Curl::Impl instance. RefCountedThreadSafe is used to destroy the |
| // Impl after the last Curl instance is destructed. |
| class Curl::Impl final : public debug_ipc::FDWatcher, public fxl::RefCountedThreadSafe<Curl::Impl> { |
| public: |
| static fxl::RefPtr<Curl::Impl> GetInstance(); |
| CURLM* multi_handle() { return multi_handle_; } |
| void OnFDReady(int fd, bool read, bool write, bool err) override; |
| |
| private: |
| // Callback given to CURL which it uses to inform us it would like to do IO on a socket and that |
| // we should add it to our polling in the event loop. |
| static int SocketCallback(CURL* easy, curl_socket_t s, int what, void* userp, void* socketp); |
| |
| // Callback given to CURL which it uses to inform us it would like to receive a timer notification |
| // at a given time in the future. If the callback is called twice before the timer expires it is |
| // expected to re-schedule the existing timer, not make a second timer. A timeout of -1 means to |
| // cancel the outstanding timer. |
| static int TimerCallback(CURLM* multi, long timeout_ms, void* userp); |
| |
| // Impl() will check and set the pointer, and ~Impl() will check and reset it to make sure there's |
| // at most 1 instance per thread at a time. |
| static thread_local Impl* instance_; |
| |
| Impl(); |
| ~Impl(); |
| |
| CURLM* multi_handle_; |
| std::map<curl_socket_t, debug_ipc::MessageLoop::WatchHandle> watches_; |
| // Indicates whether we already have a task posted to read the messages from multi_handler_. |
| bool cleanup_pending_ = false; |
| // Used in TimerCallback to avoid scheduling 2 timers and invalidate timers after destruction. |
| std::shared_ptr<bool> last_timer_valid_ = std::make_shared<bool>(false); |
| |
| FRIEND_REF_COUNTED_THREAD_SAFE(Impl); |
| FRIEND_MAKE_REF_COUNTED(Impl); |
| }; |
| |
| thread_local Curl::Impl* Curl::Impl::instance_ = nullptr; |
| |
| fxl::RefPtr<Curl::Impl> Curl::Impl::GetInstance() { |
| if (instance_) { |
| return fxl::RefPtr<Curl::Impl>(instance_); |
| } |
| return fxl::MakeRefCounted<Curl::Impl>(); |
| } |
| |
| void Curl::Impl::OnFDReady(int fd, bool read, bool write, bool err) { |
| int action = 0; |
| |
| if (read) |
| action |= CURL_CSELECT_IN; |
| if (write) |
| action |= CURL_CSELECT_OUT; |
| if (err) |
| action |= CURL_CSELECT_ERR; |
| |
| int _ignore; |
| auto result = curl_multi_socket_action(multi_handle_, fd, action, &_ignore); |
| FX_DCHECK(result == CURLM_OK); |
| |
| if (cleanup_pending_) { |
| return; |
| } |
| |
| cleanup_pending_ = true; |
| debug_ipc::MessageLoop::Current()->PostTask(FROM_HERE, [self = fxl::RefPtr<Impl>(this)]() { |
| self->cleanup_pending_ = false; |
| |
| int _ignore; |
| while (auto info = curl_multi_info_read(self->multi_handle_, &_ignore)) { |
| if (info->msg != CURLMSG_DONE) { |
| // CURLMSG_DONE is the only value for msg, documented or otherwise, so this is mostly |
| // future-proofing at writing. |
| continue; |
| } |
| |
| Curl* curl; |
| auto result = curl_easy_getinfo(info->easy_handle, CURLINFO_PRIVATE, &curl); |
| FX_DCHECK(result == CURLE_OK); |
| |
| auto cb = std::move(curl->multi_cb_); |
| curl->multi_cb_ = nullptr; |
| curl->FreeSList(); |
| auto rem_result = curl_multi_remove_handle(self->multi_handle_, info->easy_handle); |
| FX_DCHECK(rem_result == CURLM_OK); |
| |
| auto ref = curl->self_ref_; |
| curl->self_ref_ = nullptr; |
| |
| cb(ref.get(), Curl::Error(info->data.result)); |
| } |
| }); |
| } |
| |
| int Curl::Impl::SocketCallback(CURL* /*easy*/, curl_socket_t s, int what, void* /*userp*/, |
| void* /*socketp*/) { |
| FX_DCHECK(instance_); |
| |
| if (what == CURL_POLL_REMOVE || what == CURL_POLL_NONE) { |
| instance_->watches_.erase(s); |
| } else { |
| debug_ipc::MessageLoop::WatchMode mode; |
| |
| switch (what) { |
| case CURL_POLL_IN: |
| mode = debug_ipc::MessageLoop::WatchMode::kRead; |
| break; |
| case CURL_POLL_OUT: |
| mode = debug_ipc::MessageLoop::WatchMode::kWrite; |
| break; |
| case CURL_POLL_INOUT: |
| mode = debug_ipc::MessageLoop::WatchMode::kReadWrite; |
| break; |
| default: |
| FX_NOTREACHED(); |
| return -1; |
| } |
| |
| instance_->watches_[s] = debug_ipc::MessageLoop::Current()->WatchFD(mode, s, instance_); |
| } |
| |
| return 0; |
| } |
| |
| int Curl::Impl::TimerCallback(CURLM* multi, long timeout_ms, void* /*userp*/) { |
| FX_DCHECK(instance_); |
| |
| *instance_->last_timer_valid_ = false; |
| // A timeout_ms value of -1 passed to this callback means you should delete the timer. |
| if (timeout_ms < 0) { |
| return 0; |
| } |
| |
| instance_->last_timer_valid_ = std::make_shared<bool>(true); |
| debug_ipc::MessageLoop::Current()->PostTimer( |
| FROM_HERE, timeout_ms, [multi, valid = instance_->last_timer_valid_]() { |
| if (!*valid) { |
| return; |
| } |
| |
| int _ignore; |
| auto result = curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &_ignore); |
| FX_DCHECK(result == CURLM_OK); |
| }); |
| |
| return 0; |
| } |
| |
| Curl::Impl::Impl() { |
| FX_DCHECK(instance_ == nullptr); |
| instance_ = this; |
| |
| FX_DCHECK(global_initialized); |
| |
| multi_handle_ = curl_multi_init(); |
| FX_DCHECK(multi_handle_); |
| |
| auto result = curl_multi_setopt(multi_handle_, CURLMOPT_SOCKETFUNCTION, SocketCallback); |
| FX_DCHECK(result == CURLM_OK); |
| result = curl_multi_setopt(multi_handle_, CURLMOPT_TIMERFUNCTION, TimerCallback); |
| FX_DCHECK(result == CURLM_OK); |
| } |
| |
| Curl::Impl::~Impl() { |
| *last_timer_valid_ = false; |
| |
| auto result = curl_multi_cleanup(multi_handle_); |
| FX_DCHECK(result == CURLM_OK); |
| |
| FX_DCHECK(instance_ == this); |
| instance_ = nullptr; |
| } |
| |
| void Curl::GlobalInit() { |
| FX_DCHECK(!global_initialized); |
| auto res = curl_global_init(CURL_GLOBAL_SSL); |
| FX_DCHECK(!res); |
| global_initialized = true; |
| } |
| |
| void Curl::GlobalCleanup() { |
| FX_DCHECK(global_initialized); |
| curl_global_cleanup(); |
| global_initialized = false; |
| } |
| |
| Curl::Curl() { |
| impl_ = Impl::GetInstance(); |
| curl_ = curl_easy_init(); |
| FX_DCHECK(curl_); |
| |
| // The curl handle has a private pointer which we can stash the address of our wrapper class in. |
| // Then anywhere the curl handle appears in the API we can grab our wrapper. |
| curl_easy_setopt_CHECK(curl_, CURLOPT_PRIVATE, this); |
| } |
| |
| Curl::~Curl() { |
| FX_DCHECK(!multi_cb_); |
| curl_easy_cleanup(curl_); |
| } |
| |
| void Curl::set_post_data(const std::map<std::string, std::string>& items) { |
| std::string encoded; |
| |
| for (const auto& item : items) { |
| if (!encoded.empty()) { |
| encoded += "&"; |
| } |
| |
| encoded += Escape(item.first); |
| encoded += "="; |
| encoded += Escape(item.second); |
| } |
| |
| set_post_data(encoded); |
| } |
| |
| std::string Curl::Escape(const std::string& input) { |
| // It's legal to pass a null Curl_easy to curl_easy_escape (actually Curl_convert_to_network). |
| auto escaped = curl_easy_escape(nullptr, input.c_str(), static_cast<int>(input.size())); |
| // std::string(nullptr) is an UB. |
| if (!escaped) |
| return ""; |
| std::string ret(escaped); |
| curl_free(escaped); |
| return ret; |
| } |
| |
| void Curl::PrepareToPerform() { |
| FX_DCHECK(!multi_cb_); |
| |
| // It's critical to convert the lambda into function pointer, because the lambda is only valid in |
| // this scope but the function pointer is always valid even without "static". |
| using FunctionType = size_t (*)(char* data, size_t size, size_t nitems, void* curl); |
| FunctionType DoHeaderCallback = [](char* data, size_t size, size_t nitems, void* curl) { |
| return reinterpret_cast<Curl*>(curl)->header_callback_(std::string(data, size * nitems)); |
| }; |
| FunctionType DoDataCallback = [](char* data, size_t size, size_t nitems, void* curl) { |
| return reinterpret_cast<Curl*>(curl)->data_callback_(std::string(data, size * nitems)); |
| }; |
| |
| curl_easy_setopt_CHECK(curl_, CURLOPT_HEADERFUNCTION, DoHeaderCallback); |
| curl_easy_setopt_CHECK(curl_, CURLOPT_HEADERDATA, this); |
| curl_easy_setopt_CHECK(curl_, CURLOPT_WRITEFUNCTION, DoDataCallback); |
| curl_easy_setopt_CHECK(curl_, CURLOPT_WRITEDATA, this); |
| |
| // API documentation specifies "A long value of 1" enables this option, so we convert very |
| // specifically. Why take chances on sensible behavior? |
| curl_easy_setopt_CHECK(curl_, CURLOPT_NOBODY, get_body_ ? 0L : 1L); |
| |
| if (post_data_.empty()) { |
| curl_easy_setopt_CHECK(curl_, CURLOPT_POST, 0); |
| } else { |
| curl_easy_setopt_CHECK(curl_, CURLOPT_POSTFIELDS, post_data_.data()); |
| curl_easy_setopt_CHECK(curl_, CURLOPT_POSTFIELDSIZE, post_data_.size()); |
| } |
| |
| FX_DCHECK(!slist_); |
| for (const auto& header : headers_) { |
| slist_ = curl_slist_append(slist_, header.c_str()); |
| } |
| |
| curl_easy_setopt_CHECK(curl_, CURLOPT_HTTPHEADER, slist_); |
| } |
| |
| void Curl::FreeSList() { |
| if (slist_) |
| curl_slist_free_all(slist_); |
| slist_ = nullptr; |
| } |
| |
| Curl::Error Curl::Perform() { |
| PrepareToPerform(); |
| auto ret = Error(curl_easy_perform(curl_)); |
| FreeSList(); |
| return ret; |
| } |
| |
| void Curl::Perform(fit::callback<void(Curl*, Curl::Error)> cb) { |
| self_ref_ = fxl::RefPtr<Curl>(this); |
| |
| PrepareToPerform(); |
| auto result = curl_multi_add_handle(impl_->multi_handle(), curl_); |
| FX_DCHECK(result == CURLM_OK); |
| |
| multi_cb_ = std::move(cb); |
| |
| int _ignore; |
| result = curl_multi_socket_action(impl_->multi_handle(), CURL_SOCKET_TIMEOUT, 0, &_ignore); |
| FX_DCHECK(result == CURLM_OK); |
| } |
| |
| long Curl::ResponseCode() { |
| long ret; |
| |
| auto result = curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &ret); |
| FX_DCHECK(result == CURLE_OK); |
| return ret; |
| } |
| |
| } // namespace debug_ipc |