blob: acbb01acb40cda52f5bcccde19921defda8b60a1 [file] [log] [blame]
// 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::Value::Dict& 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::Value::Dict> 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::Value::Dict& 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::Value::Dict 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