blob: f7253cd3076da5dfe8da5d5ad45ec9f3157b2af3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media/webrtc/webrtc_event_log_uploader.h"
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/load_flags.h"
#include "net/base/mime_util.h"
#include "net/http/http_status_code.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 "ui/base/text/bytes_formatting.h"
namespace webrtc_event_logging {
namespace {
// TODO(crbug.com/817495): Eliminate the duplication with other uploaders.
const char kUploadContentType[] = "multipart/form-data";
const char kBoundary[] = "----**--yradnuoBgoLtrapitluMklaTelgooG--**----";
constexpr size_t kExpectedMimeOverheadBytes = 1000; // Intentional overshot.
// TODO(crbug.com/817495): Eliminate the duplication with other uploaders.
#if BUILDFLAG(IS_WIN)
const char kProduct[] = "Chrome";
#elif BUILDFLAG(IS_MAC)
const char kProduct[] = "Chrome_Mac";
#elif BUILDFLAG(IS_CHROMEOS_ASH)
const char kProduct[] = "Chrome_ChromeOS";
#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
const char kProduct[] = "Chrome_Linux";
#elif BUILDFLAG(IS_ANDROID)
const char kProduct[] = "Chrome_Android";
#elif BUILDFLAG(IS_FUCHSIA)
const char kProduct[] = "Chrome_Fuchsia";
#else
#error Platform not supported.
#endif
constexpr net::NetworkTrafficAnnotationTag
kWebrtcEventLogUploaderTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("webrtc_event_log_uploader", R"(
semantics {
sender: "WebRTC Event Log uploader module"
description:
"Uploads a WebRTC event log to a server called Crash. These logs "
"will not contain private information. They will be used to "
"improve WebRTC (fix bugs, tune performance, etc.)."
trigger:
"A Google service (e.g. Hangouts/Meet) has requested a peer "
"connection to be logged, and the resulting event log to be uploaded "
"at a time deemed to cause the least interference to the user (i.e., "
"when the user is not busy making other VoIP calls)."
data:
"WebRTC events such as the timing of audio playout (but not the "
"content), timing and size of RTP packets sent/received, etc."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting: "Feature controlled only through Chrome policy; "
"no user-facing control surface."
chrome_policy {
WebRtcEventLogCollectionAllowed {
WebRtcEventLogCollectionAllowed: false
}
}
})");
void AddFileContents(const char* filename,
const std::string& file_contents,
const std::string& content_type,
std::string* post_data) {
// net::AddMultipartValueForUpload does almost what we want to do here, except
// that it does not add the "filename" attribute. We hack it to force it to.
std::string mime_value_name =
base::StringPrintf("%s\"; filename=\"%s\"", filename, filename);
net::AddMultipartValueForUpload(mime_value_name, file_contents, kBoundary,
content_type, post_data);
}
std::string MimeContentType() {
const char kBoundaryKeywordAndMisc[] = "; boundary=";
std::string content_type;
content_type.reserve(sizeof(content_type) + sizeof(kBoundaryKeywordAndMisc) +
sizeof(kBoundary));
content_type.append(kUploadContentType);
content_type.append(kBoundaryKeywordAndMisc);
content_type.append(kBoundary);
return content_type;
}
void BindURLLoaderFactoryReceiver(
mojo::PendingReceiver<network::mojom::URLLoaderFactory>
url_loader_factory_receiver) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory =
g_browser_process->shared_url_loader_factory();
DCHECK(shared_url_loader_factory);
shared_url_loader_factory->Clone(std::move(url_loader_factory_receiver));
}
void OnURLLoadUploadProgress(uint64_t current, uint64_t total) {
ui::DataUnits unit = ui::GetByteDisplayUnits(total);
VLOG(1) << "WebRTC event log upload progress: "
<< FormatBytesWithUnits(current, unit, false) << " / "
<< FormatBytesWithUnits(total, unit, true) << ".";
}
} // namespace
const char WebRtcEventLogUploaderImpl::kUploadURL[] =
"https://clients2.google.com/cr/report";
WebRtcEventLogUploaderImpl::Factory::Factory(
scoped_refptr<base::SequencedTaskRunner> task_runner)
: task_runner_(std::move(task_runner)) {}
WebRtcEventLogUploaderImpl::Factory::~Factory() = default;
std::unique_ptr<WebRtcEventLogUploader>
WebRtcEventLogUploaderImpl::Factory::Create(const WebRtcLogFileInfo& log_file,
UploadResultCallback callback) {
return std::make_unique<WebRtcEventLogUploaderImpl>(
task_runner_, log_file, std::move(callback), kMaxRemoteLogFileSizeBytes);
}
std::unique_ptr<WebRtcEventLogUploader>
WebRtcEventLogUploaderImpl::Factory::CreateWithCustomMaxSizeForTesting(
const WebRtcLogFileInfo& log_file,
UploadResultCallback callback,
size_t max_log_file_size_bytes) {
return std::make_unique<WebRtcEventLogUploaderImpl>(
task_runner_, log_file, std::move(callback), max_log_file_size_bytes);
}
WebRtcEventLogUploaderImpl::WebRtcEventLogUploaderImpl(
scoped_refptr<base::SequencedTaskRunner> task_runner,
const WebRtcLogFileInfo& log_file,
UploadResultCallback callback,
size_t max_log_file_size_bytes)
: task_runner_(std::move(task_runner)),
log_file_(log_file),
callback_(std::move(callback)),
max_log_file_size_bytes_(max_log_file_size_bytes) {
history_file_writer_ = WebRtcEventLogHistoryFileWriter::Create(
GetWebRtcEventLogHistoryFilePath(log_file_.path));
if (!history_file_writer_) {
// File either could not be created, or, if a different error occurred,
// Create() will have tried to remove the file it has created.
UmaRecordWebRtcEventLoggingUpload(
WebRtcEventLoggingUploadUma::kHistoryFileCreationError);
ReportResult(false);
return;
}
const base::Time now = std::max(base::Time::Now(), log_file.last_modified);
if (!history_file_writer_->WriteCaptureTime(log_file.last_modified) ||
!history_file_writer_->WriteUploadTime(now)) {
LOG(ERROR) << "Writing to history file failed.";
UmaRecordWebRtcEventLoggingUpload(
WebRtcEventLoggingUploadUma::kHistoryFileWriteError);
DeleteHistoryFile(); // Avoid partial, potentially-corrupt history files.
ReportResult(false);
return;
}
std::string upload_data;
if (!PrepareUploadData(&upload_data)) {
// History file will reflect a failed upload attempt.
ReportResult(false); // UMA recorded by PrepareUploadData().
return;
}
StartUpload(upload_data);
}
WebRtcEventLogUploaderImpl::~WebRtcEventLogUploaderImpl() {
// WebRtcEventLogUploaderImpl objects' deletion scenarios:
// 1. Upload started and finished - |url_loader_| should have been reset
// so that we would be able to DCHECK and demonstrate that the determinant
// is maintained.
// 2. Upload started and cancelled - behave similarly to a finished upload.
// 3. The upload was never started, due to an early failure (e.g. file not
// found). In that case, |url_loader_| will not have been set.
// 4. Chrome shutdown.
if (task_runner_->RunsTasksInCurrentSequence()) { // Scenarios 1-3.
DCHECK(!url_loader_);
} else { // # Scenario #4 - Chrome shutdown.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
bool will_delete =
task_runner_->DeleteSoon(FROM_HERE, url_loader_.release());
DCHECK(!will_delete)
<< "Task runners must have been stopped by this stage of shutdown.";
}
}
const WebRtcLogFileInfo& WebRtcEventLogUploaderImpl::GetWebRtcLogFileInfo()
const {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
return log_file_;
}
void WebRtcEventLogUploaderImpl::Cancel() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
if (url_loader_.get() == nullptr) {
// Either the upload has already completed, or it never properly started.
return;
}
// Stop the upload.
url_loader_.reset();
// Note edge case - the upload might on very rare occasions already have
// finished, with the on-complete callback pending on the queue.
// In those very rare occasions, we will record the UMA for cancellation
// as well as the success/failure of the upload. Also, we'll post to replies
// back to the owner of this WebRtcEventLogUploaderImpl object.
UmaRecordWebRtcEventLoggingUpload(
WebRtcEventLoggingUploadUma::kUploadCancelled);
ReportResult(/*upload_successful=*/false, /*delete_history_file=*/true);
}
bool WebRtcEventLogUploaderImpl::PrepareUploadData(std::string* upload_data) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
std::string log_file_contents;
if (!base::ReadFileToStringWithMaxSize(log_file_.path, &log_file_contents,
max_log_file_size_bytes_)) {
LOG(WARNING) << "Couldn't read event log file, or max file size exceeded.";
UmaRecordWebRtcEventLoggingUpload(
WebRtcEventLoggingUploadUma::kLogFileReadError);
return false;
}
DCHECK(upload_data->empty());
upload_data->reserve(log_file_contents.size() + kExpectedMimeOverheadBytes);
const std::string filename_str = log_file_.path.BaseName().MaybeAsASCII();
if (filename_str.empty()) {
LOG(WARNING) << "Log filename is not according to acceptable format.";
UmaRecordWebRtcEventLoggingUpload(
WebRtcEventLoggingUploadUma::kLogFileNameError);
return false;
}
const char* filename = filename_str.c_str();
net::AddMultipartValueForUpload("prod", kProduct, kBoundary, std::string(),
upload_data);
net::AddMultipartValueForUpload("ver",
version_info::GetVersionNumber() + "-webrtc",
kBoundary, std::string(), upload_data);
net::AddMultipartValueForUpload("guid", "0", kBoundary, std::string(),
upload_data);
net::AddMultipartValueForUpload("type", filename, kBoundary, std::string(),
upload_data);
AddFileContents(filename, log_file_contents, "application/log", upload_data);
net::AddMultipartFinalDelimiterForUpload(kBoundary, upload_data);
return true;
}
void WebRtcEventLogUploaderImpl::StartUpload(const std::string& upload_data) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(kUploadURL);
resource_request->method = "POST";
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
// Create a new mojo pipe. It's safe to pass this around and use
// immediately, even though it needs to finish initialization on the UI
// thread.
mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory_remote;
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(BindURLLoaderFactoryReceiver,
url_loader_factory_remote.BindNewPipeAndPassReceiver()));
url_loader_ = network::SimpleURLLoader::Create(
std::move(resource_request), kWebrtcEventLogUploaderTrafficAnnotation);
url_loader_->AttachStringForUpload(upload_data, MimeContentType());
url_loader_->SetOnUploadProgressCallback(
base::BindRepeating(OnURLLoadUploadProgress));
url_loader_->DownloadToString(
url_loader_factory_remote.get(),
base::BindOnce(&WebRtcEventLogUploaderImpl::OnURLLoadComplete,
weak_ptr_factory_.GetWeakPtr()),
kWebRtcEventLogMaxUploadIdBytes);
}
void WebRtcEventLogUploaderImpl::OnURLLoadComplete(
std::unique_ptr<std::string> response_body) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK(url_loader_);
if (response_body.get() != nullptr && response_body->empty()) {
LOG(WARNING) << "SimpleURLLoader reported upload successful, "
<< "but report ID unknown.";
}
const bool upload_successful =
(response_body.get() != nullptr && !response_body->empty());
// NetError() is 0 when no error occurred.
UmaRecordWebRtcEventLoggingNetErrorType(url_loader_->NetError());
DCHECK(history_file_writer_);
if (upload_successful) {
if (!history_file_writer_->WriteUploadId(*response_body)) {
// Discard the incomplete, potentially now corrupt history file, but the
// upload is still considered successful.
LOG(ERROR) << "Failed to write upload ID to history file.";
DeleteHistoryFile();
}
} else {
LOG(WARNING) << "Upload unsuccessful.";
// By not writing an UploadId to the history file, it is inferrable that
// the upload was initiated, but did not end successfully.
}
UmaRecordWebRtcEventLoggingUpload(
upload_successful ? WebRtcEventLoggingUploadUma::kSuccess
: WebRtcEventLoggingUploadUma::kUploadFailure);
url_loader_.reset(); // Explicitly maintain determinant.
ReportResult(upload_successful);
}
void WebRtcEventLogUploaderImpl::ReportResult(bool upload_successful,
bool delete_history_file) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
if (!callback_) {
// ReportResult called twice. Can happen for example if an upload terminates
// but is cancelled while the callback from |url_loader_| is still pending
// in the queue.
return;
}
// * If the upload was successful, the file is no longer needed.
// * If the upload failed, we don't want to retry, because we run the risk of
// uploading significant amounts of data once again, only for the upload to
// fail again after (as an example) wasting 50MBs of upload bandwidth.
// * If the file was not found, this will simply have no effect (other than
// to LOG() an error).
// TODO(crbug.com/775415): Provide refined retrial behavior.
DeleteLogFile();
if (delete_history_file) {
DeleteHistoryFile();
}
// Release hold of history file, allowing it to be read, moved or deleted.
history_file_writer_.reset();
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback_), log_file_.path, upload_successful));
}
void WebRtcEventLogUploaderImpl::DeleteLogFile() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
const bool deletion_successful = base::DeleteFile(log_file_.path);
if (!deletion_successful) {
// This is a somewhat serious (though unlikely) error, because now we'll
// try to upload this file again next time Chrome launches.
LOG(ERROR) << "Could not delete pending WebRTC event log file.";
}
}
void WebRtcEventLogUploaderImpl::DeleteHistoryFile() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
if (!history_file_writer_) {
LOG(ERROR) << "Deletion of history file attempted after uploader "
<< "has relinquished ownership of it.";
return;
}
history_file_writer_->Delete();
history_file_writer_.reset();
}
} // namespace webrtc_event_logging