| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <curl/curl.h> |
| #include <curl/system.h> |
| #include <dlfcn.h> |
| |
| #include <array> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include "base/compiler_specific.h" |
| #include "base/containers/flat_map.h" |
| #include "base/containers/span.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/numerics/checked_math.h" |
| #include "base/sequence_checker.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/sequence_bound.h" |
| #include "chrome/updater/event_logger.h" |
| #include "chrome/updater/net/network.h" |
| #include "chrome/updater/policy/service.h" |
| #include "chrome/updater/util/util.h" |
| #include "components/update_client/network.h" |
| #include "url/gurl.h" |
| |
| namespace updater { |
| namespace { |
| |
| struct CurlDeleter { |
| void operator()(CURL* curl) { curl_easy_cleanup(curl); } |
| }; |
| using CurlUniquePtr = std::unique_ptr<CURL, CurlDeleter>; |
| |
| class LibcurlNetworkFetcherImpl { |
| public: |
| using ResponseStartedCallback = |
| ::update_client::NetworkFetcher::ResponseStartedCallback; |
| using ProgressCallback = ::update_client::NetworkFetcher::ProgressCallback; |
| using PostRequestCompleteCallback = |
| ::update_client::NetworkFetcher::PostRequestCompleteCallback; |
| using DownloadToFileCompleteCallback = |
| ::update_client::NetworkFetcher::DownloadToFileCompleteCallback; |
| |
| LibcurlNetworkFetcherImpl() = delete; |
| LibcurlNetworkFetcherImpl(const LibcurlNetworkFetcherImpl&) = delete; |
| LibcurlNetworkFetcherImpl& operator=(const LibcurlNetworkFetcherImpl&) = |
| delete; |
| ~LibcurlNetworkFetcherImpl(); |
| |
| LibcurlNetworkFetcherImpl( |
| CurlUniquePtr curl, |
| scoped_refptr<base::SequencedTaskRunner> callback_sequence_); |
| |
| void PostRequest( |
| const GURL& url, |
| const std::string& post_data, |
| const std::string& content_type, |
| const base::flat_map<std::string, std::string>& post_additional_headers, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| PostRequestCompleteCallback post_request_complete_callback); |
| |
| void DownloadToFile( |
| const GURL& url, |
| const base::FilePath& file_path, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| DownloadToFileCompleteCallback download_to_file_complete_callback); |
| |
| private: |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| // Static callbacks for libcurl. |
| static size_t CurlWriteStringCallback(void* data, |
| size_t size, |
| size_t nmemb, |
| void* userp); |
| static size_t CurlHeaderCallback(char* data, |
| size_t size, |
| size_t nmemb, |
| void* userp); |
| static size_t CurlWriteFileCallback(void* data, |
| size_t size, |
| size_t nmemb, |
| void* userp); |
| static int CurlTransferCallback(void* userp, |
| curl_off_t dltotal, |
| curl_off_t dlnow, |
| curl_off_t ultotal, |
| curl_off_t ulnow); |
| |
| void OnTransferInfo(curl_off_t total, curl_off_t current); |
| |
| // Helper function to find the value of a header or return an empty string. |
| static std::string GetHeaderValue( |
| const base::flat_map<std::string, std::string>& response_headers, |
| const std::string& header) { |
| const std::string lower = base::ToLowerASCII(header); |
| return response_headers.contains(lower) ? response_headers.at(lower) : ""; |
| } |
| |
| CurlUniquePtr curl_; |
| std::array<char, CURL_ERROR_SIZE> curl_error_buf_; |
| |
| // Sequence to post callbacks to. |
| scoped_refptr<base::SequencedTaskRunner> callback_sequence_; |
| |
| ResponseStartedCallback response_started_callback_; |
| ProgressCallback progress_callback_; |
| |
| base::WeakPtrFactory<LibcurlNetworkFetcherImpl> weak_factory_{this}; |
| }; |
| |
| LibcurlNetworkFetcherImpl::LibcurlNetworkFetcherImpl( |
| CurlUniquePtr curl, |
| scoped_refptr<base::SequencedTaskRunner> callback_sequence) |
| : curl_(std::move(curl)), callback_sequence_(callback_sequence) {} |
| LibcurlNetworkFetcherImpl::~LibcurlNetworkFetcherImpl() = default; |
| |
| void LibcurlNetworkFetcherImpl::PostRequest( |
| const GURL& url, |
| const std::string& post_data, |
| const std::string& content_type, |
| const base::flat_map<std::string, std::string>& post_additional_headers, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| PostRequestCompleteCallback post_request_complete_callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| VLOG(2) << __func__; |
| |
| curl_easy_reset(curl_.get()); |
| |
| struct curl_slist* headers = nullptr; |
| |
| headers = |
| curl_slist_append(headers, ("Content-Type: " + content_type).c_str()); |
| for (const auto& [key, value] : post_additional_headers) { |
| headers = curl_slist_append(headers, (key + ": " + value).c_str()); |
| } |
| |
| base::flat_map<std::string, std::string> response_headers; |
| std::optional<std::string> response_body = std::string(); |
| |
| base::WeakPtr<LibcurlNetworkFetcherImpl> weak_ptr = |
| weak_factory_.GetWeakPtr(); |
| if (curl_easy_setopt(curl_.get(), CURLOPT_URL, url.spec().c_str()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_HTTPPOST, 1L) || |
| curl_easy_setopt(curl_.get(), CURLOPT_USERAGENT, |
| GetUpdaterUserAgent().c_str()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_HTTPHEADER, headers) || |
| curl_easy_setopt(curl_.get(), CURLOPT_POSTFIELDSIZE, post_data.size()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_POSTFIELDS, post_data.c_str()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_HEADERFUNCTION, |
| &LibcurlNetworkFetcherImpl::CurlHeaderCallback) || |
| curl_easy_setopt(curl_.get(), CURLOPT_HEADERDATA, &response_headers) || |
| curl_easy_setopt(curl_.get(), CURLOPT_WRITEFUNCTION, |
| &LibcurlNetworkFetcherImpl::CurlWriteStringCallback) || |
| curl_easy_setopt(curl_.get(), CURLOPT_WRITEDATA, |
| &response_body.value()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_NOPROGRESS, 0) || |
| curl_easy_setopt(curl_.get(), CURLOPT_XFERINFOFUNCTION, |
| &LibcurlNetworkFetcherImpl::CurlTransferCallback) || |
| curl_easy_setopt(curl_.get(), CURLOPT_XFERINFODATA, &weak_ptr) || |
| curl_easy_setopt(curl_.get(), CURLOPT_ERRORBUFFER, |
| curl_error_buf_.data())) { |
| VLOG(1) << "Failed to set curl options for HTTP POST."; |
| curl_slist_free_all(headers); |
| return; |
| } |
| |
| response_started_callback_ = std::move(response_started_callback); |
| progress_callback_ = std::move(progress_callback); |
| |
| CURLcode result = curl_easy_perform(curl_.get()); |
| if (result != CURLE_OK) { |
| VLOG(1) << "Failed to perform HTTP POST. " |
| << (curl_error_buf_[0] ? curl_error_buf_.data() : "") |
| << " (CURLcode " << result << ")"; |
| } |
| |
| int x_retry_after; |
| if (!base::StringToInt( |
| GetHeaderValue(response_headers, |
| update_client::NetworkFetcher::kHeaderXRetryAfter), |
| &x_retry_after)) { |
| x_retry_after = -1; |
| } |
| |
| callback_sequence_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| std::move(post_request_complete_callback), std::move(response_body), |
| CURLE_OK, |
| GetHeaderValue(response_headers, |
| update_client::NetworkFetcher::kHeaderEtag), |
| GetHeaderValue(response_headers, |
| update_client::NetworkFetcher::kHeaderXCupServerProof), |
| GetHeaderValue(response_headers, |
| update_client::NetworkFetcher::kHeaderSetCookie), |
| x_retry_after)); |
| |
| curl_slist_free_all(headers); |
| } |
| |
| void LibcurlNetworkFetcherImpl::DownloadToFile( |
| const GURL& url, |
| const base::FilePath& file_path, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| DownloadToFileCompleteCallback download_to_file_complete_callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| VLOG(2) << __func__; |
| |
| base::File file; |
| file.Initialize(file_path, base::File::Flags::FLAG_CREATE_ALWAYS | |
| base::File::Flags::FLAG_WRITE); |
| if (!file.IsValid()) { |
| VLOG(1) << "LibcurlNetworkFetcherImpl cannot open file for download."; |
| callback_sequence_->PostTask( |
| FROM_HERE, base::BindOnce(std::move(download_to_file_complete_callback), |
| CURLE_WRITE_ERROR, 0)); |
| return; |
| } |
| |
| curl_easy_reset(curl_.get()); |
| |
| base::WeakPtr<LibcurlNetworkFetcherImpl> weak_ptr = |
| weak_factory_.GetWeakPtr(); |
| if (curl_easy_setopt(curl_.get(), CURLOPT_URL, url.spec().c_str()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_HTTPGET, 1L) || |
| curl_easy_setopt(curl_.get(), CURLOPT_USERAGENT, |
| GetUpdaterUserAgent().c_str()) || |
| curl_easy_setopt(curl_.get(), CURLOPT_WRITEFUNCTION, |
| &LibcurlNetworkFetcherImpl::CurlWriteFileCallback) || |
| curl_easy_setopt(curl_.get(), CURLOPT_WRITEDATA, &file) || |
| curl_easy_setopt(curl_.get(), CURLOPT_NOPROGRESS, 0) || |
| curl_easy_setopt(curl_.get(), CURLOPT_XFERINFOFUNCTION, |
| &LibcurlNetworkFetcherImpl::CurlTransferCallback) || |
| curl_easy_setopt(curl_.get(), CURLOPT_XFERINFODATA, &weak_ptr) || |
| curl_easy_setopt(curl_.get(), CURLOPT_ERRORBUFFER, |
| curl_error_buf_.data())) { |
| VLOG(1) << "Failed to set curl options for HTTP GET."; |
| return; |
| } |
| |
| response_started_callback_ = std::move(response_started_callback); |
| progress_callback_ = std::move(progress_callback); |
| |
| curl_off_t downloaded_bytes = 0; |
| curl_error_buf_[0] = '\0'; |
| CURLcode result = curl_easy_perform(curl_.get()); |
| if (result != CURLE_OK) { |
| VLOG(1) << "Failed to perform HTTP GET. " |
| << (curl_error_buf_[0] ? curl_error_buf_.data() : "") |
| << " (CURLcode " << result << ")"; |
| } else if (curl_easy_getinfo(curl_.get(), CURLINFO_SIZE_DOWNLOAD_T, |
| &downloaded_bytes) != CURLE_OK) { |
| VLOG(1) << "Cannot retrieve downloaded bytes for finished transfer"; |
| downloaded_bytes = 0; |
| } |
| |
| file.Close(); |
| callback_sequence_->PostTask( |
| FROM_HERE, base::BindOnce(std::move(download_to_file_complete_callback), |
| result, downloaded_bytes)); |
| } |
| |
| void LibcurlNetworkFetcherImpl::OnTransferInfo(curl_off_t total, |
| curl_off_t current) { |
| if (response_started_callback_ && total) { |
| // Query for an HTTP response code. If one has not been sent yet, the |
| // transfer has not started. |
| long response_code = 0; |
| if (curl_easy_getinfo(curl_.get(), CURLINFO_RESPONSE_CODE, |
| &response_code) != CURLE_OK) { |
| VLOG(1) << "Cannot retrieve HTTP response code for ongoing transfer."; |
| return; |
| } else if (response_code) { |
| callback_sequence_->PostTask( |
| FROM_HERE, base::BindOnce(std::move(response_started_callback_), |
| response_code, total)); |
| } |
| } |
| |
| if (progress_callback_ && current) { |
| callback_sequence_->PostTask(FROM_HERE, |
| base::BindOnce(progress_callback_, current)); |
| } |
| } |
| |
| size_t LibcurlNetworkFetcherImpl::CurlWriteStringCallback(void* data, |
| size_t member_size, |
| size_t num_members, |
| void* userp) { |
| base::CheckedNumeric<size_t> write_size = |
| base::CheckedNumeric<size_t>(member_size) * |
| base::CheckedNumeric<size_t>(num_members); |
| static_cast<std::string*>(userp)->append(static_cast<const char*>(data), |
| write_size.ValueOrDefault(0)); |
| return write_size.ValueOrDefault(0); |
| } |
| |
| size_t LibcurlNetworkFetcherImpl::CurlHeaderCallback(char* data, |
| size_t member_size, |
| size_t num_members, |
| void* userp) { |
| auto* headers = static_cast<base::flat_map<std::string, std::string>*>(userp); |
| |
| base::CheckedNumeric<size_t> buf_size = |
| base::CheckedNumeric<size_t>(member_size) * |
| base::CheckedNumeric<size_t>(num_members); |
| |
| std::string line(data, buf_size.ValueOrDefault(0)); |
| // Reject any headers that aren't compliant with RFC 5987. |
| // Returning 0 will abort the transfer. |
| if (!base::IsStringASCII(line)) { |
| return 0; |
| } |
| |
| size_t delim_pos = line.find(":"); |
| if (delim_pos != std::string::npos) { |
| std::string key = line.substr(0, delim_pos); |
| std::string value = line.substr(delim_pos + 1); |
| base::TrimWhitespaceASCII(key, base::TRIM_ALL, &key); |
| base::TrimWhitespaceASCII(value, base::TRIM_ALL, &value); |
| |
| // HTTP headers are case insensitive. For simplicity, always use lower-case. |
| if (!key.empty() && !value.empty()) { |
| headers->insert_or_assign(base::ToLowerASCII(key), value); |
| } |
| } |
| return buf_size.ValueOrDefault(0); |
| } |
| |
| size_t LibcurlNetworkFetcherImpl::CurlWriteFileCallback(void* data, |
| size_t member_size, |
| size_t num_members, |
| void* userp) { |
| // SAFETY: libcurl guarantees that `member_size` and `num_members` describe a |
| // valid readable portion of `data`. `userp` is a pointer to a stack |
| // allocated `base::File` guaranteed to be valid throughout the libcurl |
| // operation by `DownloadToFile`. |
| const base::CheckedNumeric<size_t> write_size = |
| base::CheckedNumeric<size_t>(member_size) * |
| base::CheckedNumeric<size_t>(num_members); |
| CHECK_LE(size_t{write_size.ValueOrDie()}, size_t{CURL_MAX_WRITE_SIZE}); |
| CHECK(data); |
| const auto data_span = UNSAFE_BUFFERS(base::span<uint8_t>( |
| static_cast<uint8_t*>(data), write_size.ValueOrDie())); |
| auto* file = static_cast<base::File*>(userp); |
| const std::optional<int> bytes_written = file->WriteAtCurrentPos(data_span); |
| return bytes_written.value_or(0); |
| } |
| |
| int LibcurlNetworkFetcherImpl::CurlTransferCallback(void* userp, |
| curl_off_t dltotal, |
| curl_off_t dlnow, |
| curl_off_t ultotal, |
| curl_off_t ulnow) { |
| if (!dltotal && !dlnow && !ultotal && !ulnow) { |
| return 0; |
| } |
| |
| base::WeakPtr<LibcurlNetworkFetcherImpl> wrapper = |
| *static_cast<base::WeakPtr<LibcurlNetworkFetcherImpl>*>(userp); |
| if (wrapper) { |
| if (dltotal || dlnow) { |
| wrapper->OnTransferInfo(dltotal, dlnow); |
| } else { |
| wrapper->OnTransferInfo(ultotal, ulnow); |
| } |
| } |
| |
| return 0; |
| } |
| |
| // LibcurlNetworkFetcher wraps a |LibcurlNetworkFetcherImpl| in a |
| // |base::SequenceBound|. This allows the wrapped fetcher to run solely in a |
| // dedicated io sequence. |
| class LibcurlNetworkFetcher : public update_client::NetworkFetcher { |
| public: |
| using ResponseStartedCallback = |
| ::update_client::NetworkFetcher::ResponseStartedCallback; |
| using ProgressCallback = ::update_client::NetworkFetcher::ProgressCallback; |
| using PostRequestCompleteCallback = |
| ::update_client::NetworkFetcher::PostRequestCompleteCallback; |
| using DownloadToFileCompleteCallback = |
| ::update_client::NetworkFetcher::DownloadToFileCompleteCallback; |
| |
| LibcurlNetworkFetcher() = delete; |
| LibcurlNetworkFetcher(const LibcurlNetworkFetcher&) = delete; |
| LibcurlNetworkFetcher& operator=(const LibcurlNetworkFetcher&) = delete; |
| |
| explicit LibcurlNetworkFetcher(CurlUniquePtr curl); |
| |
| // Overrides for update_client::NetworkFetcher |
| void PostRequest( |
| const GURL& url, |
| const std::string& post_data, |
| const std::string& content_type, |
| const base::flat_map<std::string, std::string>& post_additional_headers, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| PostRequestCompleteCallback post_request_complete_callback) override; |
| |
| base::OnceClosure DownloadToFile( |
| const GURL& url, |
| const base::FilePath& file_path, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| DownloadToFileCompleteCallback download_to_file_complete_callback) |
| override; |
| |
| private: |
| base::SequenceBound<LibcurlNetworkFetcherImpl> impl_; |
| }; |
| |
| LibcurlNetworkFetcher::LibcurlNetworkFetcher(CurlUniquePtr curl) |
| : impl_(base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), |
| std::move(curl), |
| base::SequencedTaskRunner::GetCurrentDefault()) {} |
| |
| void LibcurlNetworkFetcher::PostRequest( |
| const GURL& url, |
| const std::string& post_data, |
| const std::string& content_type, |
| const base::flat_map<std::string, std::string>& post_additional_headers, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| PostRequestCompleteCallback post_request_complete_callback) { |
| impl_.AsyncCall(&LibcurlNetworkFetcherImpl::PostRequest) |
| .WithArgs(url, post_data, content_type, post_additional_headers, |
| std::move(response_started_callback), |
| std::move(progress_callback), |
| std::move(post_request_complete_callback)); |
| } |
| |
| base::OnceClosure LibcurlNetworkFetcher::DownloadToFile( |
| const GURL& url, |
| const base::FilePath& file_path, |
| ResponseStartedCallback response_started_callback, |
| ProgressCallback progress_callback, |
| DownloadToFileCompleteCallback download_to_file_complete_callback) { |
| impl_.AsyncCall(&LibcurlNetworkFetcherImpl::DownloadToFile) |
| .WithArgs(url, file_path, std::move(response_started_callback), |
| std::move(progress_callback), |
| std::move(download_to_file_complete_callback)); |
| return base::DoNothing(); |
| } |
| |
| } // namespace |
| |
| class NetworkFetcherFactory::Impl {}; |
| |
| NetworkFetcherFactory::NetworkFetcherFactory( |
| std::optional<PolicyServiceProxyConfiguration>, |
| scoped_refptr<UpdaterEventLogger>) {} |
| NetworkFetcherFactory::~NetworkFetcherFactory() = default; |
| |
| std::unique_ptr<update_client::NetworkFetcher> NetworkFetcherFactory::Create() |
| const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CurlUniquePtr curl{curl_easy_init()}; |
| if (!curl) { |
| VLOG(1) << "Failed to initialize a curl handle."; |
| return nullptr; |
| } |
| return std::make_unique<LibcurlNetworkFetcher>(std::move(curl)); |
| } |
| |
| } // namespace updater |