aboutsummaryrefslogtreecommitdiff
path: root/fcp/client/http/curl/curl_http_request_handle.cc
diff options
context:
space:
mode:
Diffstat (limited to 'fcp/client/http/curl/curl_http_request_handle.cc')
-rw-r--r--fcp/client/http/curl/curl_http_request_handle.cc386
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