// 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 <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/check.h"
#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);
    auto it = response_headers.find(lower);
    return it != response_headers.end() ? it->second : "";
  }

  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);

  curl_error_buf_[0] = '\0';
  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<LoggingNetworkFetcher>(
      std::make_unique<LibcurlNetworkFetcher>(std::move(curl)));
}

}  // namespace updater
