diff options
Diffstat (limited to 'fcp/client/http/curl/curl_http_request_handle.cc')
-rw-r--r-- | fcp/client/http/curl/curl_http_request_handle.cc | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/fcp/client/http/curl/curl_http_request_handle.cc b/fcp/client/http/curl/curl_http_request_handle.cc new file mode 100644 index 0000000..874b774 --- /dev/null +++ b/fcp/client/http/curl/curl_http_request_handle.cc @@ -0,0 +1,386 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "fcp/client/http/curl/curl_http_request_handle.h" + +#include <memory> +#include <optional> +#include <string> +#include <utility> + +#include "absl/strings/match.h" +#include "curl/curl.h" +#include "fcp/base/monitoring.h" +#include "fcp/client/http/curl/curl_api.h" +#include "fcp/client/http/curl/curl_header_parser.h" +#include "fcp/client/http/curl/curl_http_response.h" +#include "fcp/client/http/http_client_util.h" + +namespace fcp::client::http::curl { +namespace { +// A type check for the macro. +inline CURLcode AsCode(CURLcode code) { return code; } +/** + * Macro which allows to check for a status code and return from the + * current method if not OK. Example: + * + * Status DoSomething() { + * CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(...)); + * } + */ +#define CURL_RETURN_IF_ERROR(expr) \ + do { \ + CURLcode __code = AsCode(expr); \ + if (__code != CURLE_OK) { \ + FCP_LOG(ERROR) << "Easy handle failed with " \ + << CurlEasyHandle::StrError(__code); \ + return (__code); \ + } \ + } while (false) + +// Add a new element to a C-style list. +curl_slist* AddToCurlHeaderList(curl_slist* header_list, const std::string& key, + const std::string& value) { + // A null pointer is returned if anything went wrong, otherwise a new + // list pointer is returned. + curl_slist* tmp = + curl_slist_append(header_list, absl::StrCat(key, ": ", value).c_str()); + FCP_CHECK(tmp != nullptr); + return tmp; +} + +} // namespace + +size_t CurlHttpRequestHandle::HeaderCallback(char* buffer, size_t size, + size_t n_items, void* user_data) { + auto self = static_cast<CurlHttpRequestHandle*>(user_data); + std::string str_header(static_cast<char*>(buffer), size * n_items); + + self->header_parser_.ParseHeader(str_header); + if (!self->header_parser_.IsLastHeader()) { + return size * n_items; + } + + self->response_ = + std::make_unique<CurlHttpResponse>(self->header_parser_.GetStatusCode(), + self->header_parser_.GetHeaderList()); + + FCP_CHECK(self->callback_ != nullptr); + absl::Status status = + self->callback_->OnResponseStarted(*self->request_, *self->response_); + + if (!status.ok()) { + FCP_LOG(ERROR) << "Called OnResponseStarted. Received status: " << status; + self->callback_->OnResponseError(*self->request_, status); + } + + return size * n_items; +} + +size_t CurlHttpRequestHandle::DownloadCallback(void* body, size_t size, + size_t nmemb, void* user_data) { + auto self = static_cast<CurlHttpRequestHandle*>(user_data); + absl::string_view str_body(static_cast<char*>(body), size * nmemb); + + absl::Status status = self->callback_->OnResponseBody( + *self->request_, *self->response_, str_body); + + if (!status.ok()) { + FCP_LOG(ERROR) << "Called OnResponseBody. Received status: " << status; + self->callback_->OnResponseBodyError(*self->request_, *self->response_, + status); + } + + return size * nmemb; +} + +size_t CurlHttpRequestHandle::UploadCallback(char* buffer, size_t size, + size_t num, void* user_data) { + auto self = static_cast<CurlHttpRequestHandle*>(user_data); + size_t buffer_size = size * num; + + absl::StatusOr<int64_t> read_size = + self->request_->ReadBody(buffer, buffer_size); + if (read_size.ok()) { + return read_size.value(); + } else if (read_size.status().code() == absl::StatusCode::kOutOfRange) { + return 0; + } + return CURL_READFUNC_ABORT; +} + +size_t CurlHttpRequestHandle::ProgressCallback(void* user_data, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow) { + auto self = static_cast<CurlHttpRequestHandle*>(user_data); + absl::MutexLock lock(&self->mutex_); + // Abort is any number except zero. + return (self->is_cancelled_) ? 1 : 0; +} + +CurlHttpRequestHandle::CurlHttpRequestHandle( + std::unique_ptr<HttpRequest> request, + std::unique_ptr<CurlEasyHandle> easy_handle, + const std::string& test_cert_path) + : request_(std::move(request)), + response_(nullptr), + easy_handle_(std::move(easy_handle)), + callback_(nullptr), + is_being_performed_(false), + is_completed_(false), + is_cancelled_(false), + header_list_(nullptr) { + FCP_CHECK(request_ != nullptr); + FCP_CHECK(easy_handle_ != nullptr); + + CURLcode code = InitializeConnection(test_cert_path); + if (code != CURLE_OK) { + FCP_LOG(ERROR) << "easy_handle initialization failed with code " + << CurlEasyHandle::StrError(code); + FCP_LOG(ERROR) << error_buffer_; + callback_->OnResponseError(*request_, absl::InternalError(error_buffer_)); + return; + } +} + +CurlHttpRequestHandle::~CurlHttpRequestHandle() { + curl_slist_free_all(header_list_); +} + +void CurlHttpRequestHandle::Cancel() { + absl::MutexLock lock(&mutex_); + + if (callback_ == nullptr || is_cancelled_ || is_completed_) { + return; + } + if (response_ != nullptr) { + callback_->OnResponseBodyError(*request_, *response_, + absl::CancelledError()); + } else { + callback_->OnResponseError(*request_, absl::CancelledError()); + } + is_cancelled_ = true; +} + +void CurlHttpRequestHandle::MarkAsCompleted() { + absl::MutexLock lock(&mutex_); + + FCP_CHECK(callback_ != nullptr); + if (!is_cancelled_ && !is_completed_) { + if (response_ != nullptr) { + callback_->OnResponseCompleted(*request_, *response_); + } else { + callback_->OnResponseError(*request_, + absl::InternalError("response_ is nullptr")); + } + } + is_completed_ = true; +} + +absl::Status CurlHttpRequestHandle::AddToMulti(CurlMultiHandle* multi_handle, + HttpRequestCallback* callback) { + absl::MutexLock lock(&mutex_); + + FCP_CHECK(callback != nullptr); + FCP_CHECK(multi_handle != nullptr); + + if (is_cancelled_) { + callback->OnResponseError(*request_, absl::CancelledError()); + return absl::CancelledError(); + } else if (is_being_performed_ || is_completed_) { + return absl::ResourceExhaustedError( + "The handle was previously passed to another PerformRequests call."); + } + + is_being_performed_ = true; + callback_ = callback; + + CURLMcode code = multi_handle->AddEasyHandle(easy_handle_.get()); + if (code != CURLM_OK) { + FCP_LOG(ERROR) << "AddEasyHandle failed with code " << code; + FCP_LOG(ERROR) << error_buffer_; + callback_->OnResponseError(*request_, absl::InternalError(error_buffer_)); + return absl::InternalError(error_buffer_); + } + + return absl::OkStatus(); +} + +void CurlHttpRequestHandle::RemoveFromMulti(CurlMultiHandle* multi_handle) { + absl::MutexLock lock(&mutex_); + + FCP_CHECK(multi_handle != nullptr); + CURLMcode code = multi_handle->RemoveEasyHandle(easy_handle_.get()); + if (code != CURLM_OK) { + FCP_LOG(ERROR) << "RemoveEasyHandle failed with code " + << CurlMultiHandle::StrError(code); + FCP_LOG(ERROR) << error_buffer_; + } +} + +HttpRequestHandle::SentReceivedBytes +CurlHttpRequestHandle::TotalSentReceivedBytes() const { + absl::MutexLock lock(&mutex_); + curl_off_t total_sent_bytes = 0; + CURLcode code = + easy_handle_->GetInfo(CURLINFO_SIZE_UPLOAD_T, &total_sent_bytes); + if (code != CURLE_OK) { + FCP_LOG(ERROR) << "TotalSentBytes failed with code " << code; + FCP_LOG(ERROR) << error_buffer_; + } + curl_off_t total_received_bytes = 0; + code = easy_handle_->GetInfo(CURLINFO_SIZE_DOWNLOAD_T, &total_received_bytes); + if (code != CURLE_OK) { + FCP_LOG(ERROR) << "TotalReceivedBytes failed with code " << code; + FCP_LOG(ERROR) << error_buffer_; + } + return {.sent_bytes = total_sent_bytes, + .received_bytes = total_received_bytes}; +} + +CURLcode CurlHttpRequestHandle::InitializeConnection( + const std::string& test_cert_path) { + error_buffer_[0] = 0; + // Needed to read an error message. + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_ERRORBUFFER, error_buffer_)); + + // Skip all signal handling because is not thread-safe. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_NOSIGNAL, 1L)); + + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_URL, std::string(request_->uri()))); + + // Forces curl to follow redirects. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_FOLLOWLOCATION, 1L)); + + // Suppresses headers added by a proxy. + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L)); + + // Force curl to verify the ssl connection. + CURL_RETURN_IF_ERROR( + test_cert_path.empty() + ? easy_handle_->SetOpt(CURLOPT_CAINFO, nullptr) + : easy_handle_->SetOpt(CURLOPT_CAINFO, test_cert_path)); + + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_SSL_VERIFYPEER, 2L)); + + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_SSL_VERIFYHOST, 1L)); + + // Force curl to never timeout. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_TIMEOUT_MS, + std::numeric_limits<int>::max())); + + // Called when a response header received. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt( + CURLOPT_HEADERFUNCTION, &CurlHttpRequestHandle::HeaderCallback)); + + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_HEADERDATA, this)); + + // Called when a response body received. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt( + CURLOPT_WRITEFUNCTION, &CurlHttpRequestHandle::DownloadCallback)); + + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_WRITEDATA, this)); + + // Called to send a request body + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt( + CURLOPT_READFUNCTION, &CurlHttpRequestHandle::UploadCallback)); + + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_READDATA, this)); + + // Called periodically. We use it to check whether the request is cancelled. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt( + CURLOPT_XFERINFOFUNCTION, &CurlHttpRequestHandle::ProgressCallback)); + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_XFERINFODATA, this)); + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_NOPROGRESS, 0L)); + + // Private storage. Used by a multi-handle. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_PRIVATE, this)); + + switch (request_->method()) { + case HttpRequest::Method::kGet: + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_HTTPGET, 1L)); + break; + case HttpRequest::Method::kHead: + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_NOBODY, 1L)); + break; + case HttpRequest::Method::kPost: + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_POST, 1L)); + // Forces curl to use the callback. + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_POSTFIELDS, nullptr)); + break; + case HttpRequest::Method::kPut: + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_UPLOAD, 1L)); + break; + case HttpRequest::Method::kPatch: + case HttpRequest::Method::kDelete: + FCP_LOG(ERROR) << "Unsupported request type"; + return CURLE_UNSUPPORTED_PROTOCOL; + } + + return InitializeHeaders(request_->extra_headers(), request_->method()); +} + +CURLcode CurlHttpRequestHandle::InitializeHeaders( + const HeaderList& extra_headers, HttpRequest::Method method) { + // If no "Accept-Encoding" request header is explicitly specified + // advertise an "Accept-Encoding: gzip" else leave decoded. + std::optional<std::string> accept_encoding = + FindHeader(request_->extra_headers(), kAcceptEncodingHdr); + if (!accept_encoding.has_value()) { + // Libcurl is responsible for the encoding. + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_ACCEPT_ENCODING, kGzipEncodingHdrValue)); + header_parser_.UseCurlEncoding(); + } else { + // The caller is responsible for the encoding. + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_ACCEPT_ENCODING, nullptr)); + } + + for (auto& [key, value] : extra_headers) { + if (absl::EqualsIgnoreCase(key, kAcceptEncodingHdr)) { + continue; + } else if (absl::EqualsIgnoreCase(key, kContentLengthHdr)) { + if (method == HttpRequest::Method::kPost) { + // For post less than 2GB + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_POSTFIELDSIZE, std::stol(value))); + + // Removes the header to prevent libcurl from setting it + // to 'Expect: 100-continue' by default, which causes an additional + // and unnecessary network round trip. + header_list_ = AddToCurlHeaderList(header_list_, kExpectHdr, ""); + } else if (method == HttpRequest::Method::kPut) { + CURL_RETURN_IF_ERROR( + easy_handle_->SetOpt(CURLOPT_INFILESIZE, std::stol(value))); + header_list_ = AddToCurlHeaderList(header_list_, kExpectHdr, ""); + } + } else { + // A user-defined "Expect" header is not supported. + FCP_CHECK(!absl::EqualsIgnoreCase(key, kExpectHdr)); + header_list_ = AddToCurlHeaderList(header_list_, key, value); + } + } + + CURL_RETURN_IF_ERROR(easy_handle_->SetOpt(CURLOPT_HTTPHEADER, header_list_)); + return CURLE_OK; +} +} // namespace fcp::client::http::curl |