// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "remoting/host/crash/crash_file_uploader.h"

#include <list>
#include <memory>
#include <string>
#include <utility>

#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner_thread_mode.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/buildflag.h"
#include "net/base/load_flags.h"
#include "net/base/mime_util.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "remoting/base/crash/breakpad_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace remoting {

namespace {

// Post multi-part form-data names for the upload request.
constexpr char kProductNameKey[] = "prod";
constexpr char kProductVersionKey[] = "ver";
constexpr char kProcessUptimeKey[] = "ptime";
constexpr char kMinidumpFileKey[] = "upload_file_minidump";
constexpr char kMinidumpFileName[] = "dump";

#if BUILDFLAG(IS_WIN)
constexpr char kProductNameValue[] = "Chromoting";
#elif BUILDFLAG(IS_LINUX)
constexpr char kProductNameValue[] = "Chromoting_Linux";
#elif BUILDFLAG(IS_MAC)
constexpr char kProductNameValue[] = "Chromoting_Mac";
#else
#error Platform not supported
#endif

const base::FilePath::CharType kDumpExtension[] = FILE_PATH_LITERAL("dmp");
const base::FilePath::CharType kJsonExtension[] = FILE_PATH_LITERAL("json");
const base::FilePath::CharType kLastUploadTimeFilePath[] =
    FILE_PATH_LITERAL("last_upload_time.txt");
const base::FilePath::CharType kUploadResultFilePath[] =
    FILE_PATH_LITERAL("upload_result.txt");

constexpr char kCrashReportUploadUrl[] =
    "https://clients2.google.com/cr/report";

// Used to reserve space for the non-file data in the post form to prevent
// string re-allocations which building up the multi-part post form.
constexpr int kPostFormReservationSize = 4096;

// A successful response should only contain the report_id so this is bigger
// than is needed for a typical response but it prevents a ridiculously large
// value and will ensure we don't reject the response if the format changes in
// the future.
constexpr size_t kMaxResponseSize = 1024;

// Throttle uploads to 1 per hour.
constexpr base::TimeDelta kUploadRateLimitWindow = base::Hours(1);

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("crash_file_uploader",
                                        R"(
        semantics {
          sender: "Chrome Remote Desktop"
          description:
            "Uploads crash reports generated by Chrome Remote Desktop."
          trigger:
            "The upload request is made when a minidump file is generated by "
            "the Chrome Remote Desktop host service after the user has opted "
            "into crash reporting."
          data: "Minidump file and product-specific info (e.g. version)."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              email: "chromoting-team@google.com"
            }
          }
          user_data {
            type: ARBITRARY_DATA
          }
          last_reviewed: "2023-05-12"
        }
        policy {
          cookies_allowed: NO
          setting:
            "This request will not be sent if crash reporting is not enabled "
            "for Chrome Remote Desktop."
          policy_exception_justification:
            "Not implemented."
        })");

base::FilePath GetLastUploadTimeFilePath() {
  return GetMinidumpDirectoryPath().Append(kLastUploadTimeFilePath);
}

base::FilePath GetCrashDirectoryPath(const base::FilePath& crash_guid) {
  return GetMinidumpDirectoryPath().Append(crash_guid);
}

base::FilePath GetCrashFileBase(const base::FilePath& crash_guid) {
  return GetCrashDirectoryPath(crash_guid).Append(crash_guid);
}

base::FilePath GetDumpFilePath(const base::FilePath& crash_guid) {
  return GetCrashFileBase(crash_guid).AddExtension(kDumpExtension);
}

base::FilePath GetMetadataFilePath(const base::FilePath& crash_guid) {
  return GetCrashFileBase(crash_guid).AddExtension(kJsonExtension);
}

bool RetrieveCrashReportDetails(const base::FilePath& crash_guid,
                                std::string& minidump_file_contents,
                                base::DictValue& metadata,
                                std::string& error_reason) {
  base::FilePath minidump_file_path = GetDumpFilePath(crash_guid);
  if (!base::PathExists(minidump_file_path)) {
    error_reason = "Upload directory is missing the minidump file";
    return false;
  }

  std::string minidump_string;
  if (!base::ReadFileToString(minidump_file_path, &minidump_string)) {
    error_reason = "Failed to read dump file contents";
    return false;
  }

  base::FilePath metadata_file = GetMetadataFilePath(crash_guid);
  if (!base::PathExists(metadata_file)) {
    error_reason = "Upload directory is missing the metadata file";
    return false;
  }

  std::string metadata_file_contents;
  if (!base::ReadFileToString(metadata_file, &metadata_file_contents)) {
    error_reason = "Failed to read metadata file";
    return false;
  }

  std::optional<base::DictValue> opt_metadata = base::JSONReader::ReadDict(
      metadata_file_contents, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!opt_metadata.has_value()) {
    error_reason = "Failed to parse metadata file contents";
    return false;
  }

  // Ensure the metadata file has the required fields. Note that the metadata
  // file may contain additional fields (e.g. for troubleshooting or manual
  // uploading) but there are only a few which are required for the upload.
  const std::string* version =
      opt_metadata->FindString(kBreakpadProductVersionKey);
  if (version == nullptr || version->empty()) {
    error_reason = "Metadata file is missing the product version field";
    return false;
  }
  const std::string* uptime =
      opt_metadata->FindString(kBreakpadProcessUptimeKey);
  if (uptime == nullptr || uptime->empty()) {
    error_reason = "Metadata file is missing the process uptime field";
    return false;
  }

  metadata = std::move(*opt_metadata);
  minidump_file_contents = std::move(minidump_string);
  return true;
}

void DeleteDumpFileAndWriteResult(const base::FilePath& crash_file,
                                  const std::string& result) {
  if (!base::DeleteFile(crash_file)) {
    LOG(WARNING) << "Failed to delete minidump file: " << crash_file;
  }

  base::FilePath result_file =
      crash_file.DirName().Append(kUploadResultFilePath);
  if (!base::WriteFile(result_file, result + "\r\n")) {
    LOG(WARNING) << "Failed to write upload result to: " << result_file;
  }
}

std::unique_ptr<network::SimpleURLLoader> CreateSimpleUrlLoader() {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = GURL(kCrashReportUploadUrl);
  resource_request->load_flags =
      net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  resource_request->method = net::HttpRequestHeaders::kPostMethod;

  std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
      network::SimpleURLLoader::Create(std::move(resource_request),
                                       kTrafficAnnotation);
  simple_url_loader->SetTimeoutDuration(base::Seconds(60));
  simple_url_loader->SetAllowHttpErrorResults(false);
  simple_url_loader->SetRetryOptions(
      3, network::SimpleURLLoader::RETRY_ON_5XX |
             network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);

  return simple_url_loader;
}

void GenerateMultiPartPostData(const base::DictValue& metadata,
                               const std::string& minidump_data,
                               std::string& post_data,
                               std::string& content_type) {
  post_data.clear();
  content_type.clear();

  post_data.reserve(minidump_data.size() + kPostFormReservationSize);

  std::string mime_boundary = net::GenerateMimeMultipartBoundary();

  // Add the product name.
  net::AddMultipartValueForUpload(kProductNameKey, kProductNameValue,
                                  mime_boundary, std::string(), &post_data);
  // Add the product version.
  const std::string* version = metadata.FindString(kBreakpadProductVersionKey);
  net::AddMultipartValueForUpload(kProductVersionKey, *version, mime_boundary,
                                  std::string(), &post_data);
  // Add the process uptime.
  const std::string* uptime = metadata.FindString(kBreakpadProcessUptimeKey);
  net::AddMultipartValueForUpload(kProcessUptimeKey, *uptime, mime_boundary,
                                  std::string(), &post_data);
  // Add the minidump file.
  net::AddMultipartValueForUploadWithFileName(
      kMinidumpFileKey, kMinidumpFileName, minidump_data, mime_boundary,
      "application/octet-stream", &post_data);
  // Add the final delimiter.
  net::AddMultipartFinalDelimiterForUpload(mime_boundary, &post_data);
  post_data.shrink_to_fit();

  content_type = "multipart/form-data; boundary=" + mime_boundary;
}

void DeleteFileWithLogging(const base::FilePath& file_to_delete) {
  if (!base::DeleteFile(file_to_delete)) {
    LOG(WARNING) << "Failed to delete file: " << file_to_delete;
  }
}

bool SkipUploadDueToRateLimiting() {
  base::FilePath last_upload_time_file = GetLastUploadTimeFilePath();
  if (!base::PathExists(last_upload_time_file)) {
    return false;
  }

  std::string last_upload_time_str;
  if (!base::ReadFileToString(last_upload_time_file, &last_upload_time_str)) {
    LOG(WARNING) << "Failed to read file: " << last_upload_time_file;
    DeleteFileWithLogging(last_upload_time_file);
    return false;
  }

  time_t last_upload_time_t = 0;
  if (!base::StringToInt64(last_upload_time_str, &last_upload_time_t)) {
    LOG(WARNING) << "Failed to convert last_upload_time: "
                 << last_upload_time_str;
    DeleteFileWithLogging(last_upload_time_file);
    return false;
  }

  auto time_since_last_upload = base::Time::NowFromSystemTime() -
                                base::Time::FromTimeT(last_upload_time_t);
  return time_since_last_upload < kUploadRateLimitWindow;
}

void UpdateLastUploadTimeFile() {
  std::string now =
      base::NumberToString(base::Time::NowFromSystemTime().ToTimeT());
  if (!base::WriteFile(GetLastUploadTimeFilePath(), now)) {
    LOG(WARNING) << "Failed to write current time to upload rate limiting file "
                 << ", crash reporting will not be rate limited correctly";
  }
}

}  // namespace

class CrashFileUploader::Core {
 public:
  explicit Core(
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
  Core(const Core&) = delete;
  Core& operator=(const Core&) = delete;
  ~Core();

  void Upload(const base::FilePath& crash_guid);

 private:
  using SimpleURLLoaderList =
      std::list<std::unique_ptr<network::SimpleURLLoader>>;
  void OnUploadComplete(SimpleURLLoaderList::iterator it,
                        base::FilePath crash_guid,
                        std::optional<std::string> response_body);

  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
  SimpleURLLoaderList simple_url_loaders_;

  THREAD_CHECKER(thread_checker_);

  base::WeakPtrFactory<Core> weak_ptr_factory_{this};
};

CrashFileUploader::Core::Core(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : url_loader_factory_(url_loader_factory) {
  DETACH_FROM_THREAD(thread_checker_);
}

CrashFileUploader::Core::~Core() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

void CrashFileUploader::Core::Upload(const base::FilePath& crash_guid) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  base::FilePath crash_report_directory = GetCrashDirectoryPath(crash_guid);
  LOG(INFO) << "Validating crash report files in " << crash_report_directory;

  if (!base::DirectoryExists(crash_report_directory)) {
    LOG(ERROR) << "Upload directory does not exist for report: " << crash_guid;
    return;
  }

  if (SkipUploadDueToRateLimiting()) {
    std::string rate_limit_error("Upload skipped due to rate limiting");
    LOG(WARNING) << rate_limit_error;
    DeleteDumpFileAndWriteResult(GetDumpFilePath(crash_guid), rate_limit_error);
    return;
  }

  base::DictValue metadata;
  std::string minidump_data;
  std::string error;
  if (!RetrieveCrashReportDetails(crash_guid, minidump_data, metadata, error)) {
    LOG(ERROR) << "Failed to retrieve crash report details: " << error;
    DeleteDumpFileAndWriteResult(GetDumpFilePath(crash_guid), error);
    return;
  }

  LOG(INFO) << "Sending crash report (" << crash_guid << ") to "
            << kCrashReportUploadUrl;

  auto simple_url_loader = CreateSimpleUrlLoader();
  auto* simple_url_loader_ptr = simple_url_loader.get();
  auto it = simple_url_loaders_.insert(simple_url_loaders_.begin(),
                                       std::move(simple_url_loader));

  std::string post_data;
  std::string content_type;
  GenerateMultiPartPostData(metadata, minidump_data, post_data, content_type);
  simple_url_loader_ptr->AttachStringForUpload(post_data, content_type);
  simple_url_loader_ptr->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&CrashFileUploader::Core::OnUploadComplete,
                     weak_ptr_factory_.GetWeakPtr(), std::move(it),
                     std::move(crash_guid)),
      kMaxResponseSize);
}

void CrashFileUploader::Core::OnUploadComplete(
    SimpleURLLoaderList::iterator it,
    base::FilePath crash_guid,
    std::optional<std::string> response_body) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  std::string upload_result;
  base::FilePath crash_dump = crash_guid.AddExtension(kDumpExtension);
  if ((*it)->NetError() == net::OK) {
    std::string report_id = std::move(response_body).value_or("empty");
    // Result file format looks like:
    // report_id: <id_from_crash_service>
    // go/crash/<id_from_crash_service>
    upload_result =
        "report_id: " + report_id + "\r\n" + "http://go/crash/" + report_id;
    // Include the crash report id and go link in the host log.
    LOG(INFO) << "Successfully uploaded: " << crash_dump << "\r\n"
              << "    report_id: " << report_id << "\r\n"
              << "    http://go/crash/" << report_id << "\r\n"
              << "    Please note that it may take a few minutes to finish "
              << "processing the report.";
    UpdateLastUploadTimeFile();
  } else {
    std::string response_code = "<unknown>";
    auto* response_info = (*it)->ResponseInfo();
    if (response_info && response_info->headers) {
      response_code =
          base::NumberToString(response_info->headers->response_code());
    }
    LOG(ERROR) << "Failed to upload crash report: " << crash_dump
               << ", response_code: " << response_code;
    upload_result = "Upload failed, response code: " + response_code;
  }

  simple_url_loaders_.erase(it);
  DeleteDumpFileAndWriteResult(GetDumpFilePath(crash_guid), upload_result);
}

CrashFileUploader::CrashFileUploader(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    scoped_refptr<base::SingleThreadTaskRunner> core_task_runner)
    : core_(std::make_unique<CrashFileUploader::Core>(url_loader_factory)),
      core_task_runner_(core_task_runner) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

CrashFileUploader::~CrashFileUploader() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  core_task_runner_->DeleteSoon(FROM_HERE, core_.release());
}

void CrashFileUploader::Upload(const base::FilePath& crash_guid) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  core_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&CrashFileUploader::Core::Upload,
                                base::Unretained(core_.get()), crash_guid));
}

}  // namespace remoting
