blob: 266da73dd23e2bdf6d322426319751ba38c09850 [file] [log] [blame]
// Copyright (c) 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 "content/browser/webrtc/webrtc_remote_event_log_manager.h"
#include <algorithm>
#include <limits>
#include "base/big_endian.h"
#include "base/bind.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/rand_util.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "content/public/browser/browser_thread.h"
namespace content {
// TODO(eladalon): Block remote-bound logging on mobile devices.
// https://crbug.com/775415
const size_t kMaxRemoteLogFileMetadataSizeBytes = 0xffffu; // 65535
static_assert(kMaxRemoteLogFileMetadataSizeBytes <= 0xFFFFFFu,
"Only 24 bits available for encoding the metadata's length.");
const size_t kMaxRemoteLogFileSizeBytes = (1u << 29); // ~500MBs
namespace {
const base::FilePath::CharType kRemoteBoundLogSubDirectory[] =
FILE_PATH_LITERAL("webrtc_event_logs");
// Purge from local disk a log file which could not be properly started
// (e.g. error encountered when attempting to write the log header).
void DiscardLogFile(base::File* file, const base::FilePath& file_path) {
file->Close();
if (!base::DeleteFile(file_path, /*recursive=*/false)) {
LOG(ERROR) << "Failed to delete " << file_path << ".";
}
}
bool AreLogParametersValid(size_t max_file_size_bytes,
const std::string& metadata) {
if (max_file_size_bytes == kWebRtcEventLogManagerUnlimitedFileSize) {
LOG(WARNING) << "Unlimited file sizes not allowed for remote-bound logs.";
return false;
}
if (max_file_size_bytes > kMaxRemoteLogFileSizeBytes) {
LOG(WARNING) << "File size exceeds maximum allowed.";
return false;
}
if (metadata.length() > kMaxRemoteLogFileMetadataSizeBytes) {
LOG(ERROR) << "Excessively long metadata.";
return false;
}
if (metadata.size() + kRemoteBoundLogFileHeaderSizeBytes >=
max_file_size_bytes) {
LOG(ERROR) << "Max file size and metadata must leave room for event log.";
return false;
}
return true;
}
} // namespace
const size_t kMaxActiveRemoteBoundWebRtcEventLogs = 3;
const size_t kMaxPendingRemoteBoundWebRtcEventLogs = 5;
static_assert(kMaxActiveRemoteBoundWebRtcEventLogs <=
kMaxPendingRemoteBoundWebRtcEventLogs,
"This assumption affects unit test coverage.");
const base::TimeDelta kRemoteBoundWebRtcEventLogsMaxRetention =
base::TimeDelta::FromDays(3);
const base::FilePath::CharType kRemoteBoundLogExtension[] =
FILE_PATH_LITERAL("log");
const uint8_t kRemoteBoundWebRtcEventLogFileVersion = 0;
const size_t kRemoteBoundLogFileHeaderSizeBytes = sizeof(uint32_t);
WebRtcRemoteEventLogManager::WebRtcRemoteEventLogManager(
WebRtcRemoteEventLogsObserver* observer)
: observer_(observer),
uploader_factory_(new WebRtcEventLogUploaderImpl::Factory) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DETACH_FROM_SEQUENCE(io_task_sequence_checker_);
}
WebRtcRemoteEventLogManager::~WebRtcRemoteEventLogManager() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// TODO(eladalon): Purge from disk files which were being uploaded while
// destruction took place, thereby avoiding endless attempts to upload
// the same file. https://crbug.com/775415
}
void WebRtcRemoteEventLogManager::EnableForBrowserContext(
BrowserContextId browser_context_id,
const base::FilePath& browser_context_dir) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
DCHECK(!BrowserContextEnabled(browser_context_id)) << "Already enabled.";
const base::FilePath remote_bound_logs_dir =
GetLogsDirectoryPath(browser_context_dir);
if (!MaybeCreateLogsDirectory(remote_bound_logs_dir)) {
LOG(WARNING)
<< "WebRtcRemoteEventLogManager couldn't create logs directory.";
return;
}
AddPendingLogs(browser_context_id, remote_bound_logs_dir);
enabled_browser_contexts_.insert(browser_context_id);
}
// TODO(eladalon): Add unit tests. https://crbug.com/775415
void WebRtcRemoteEventLogManager::DisableForBrowserContext(
BrowserContextId browser_context_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
if (!BrowserContextEnabled(browser_context_id)) {
return; // Enabling may have failed due to lacking permissions.
}
enabled_browser_contexts_.erase(browser_context_id);
#if DCHECK_IS_ON()
// All of the RPHs associated with this BrowserContext must already have
// exited, which should have implicitly stopped all active logs.
auto pred = [browser_context_id](decltype(active_logs_)::value_type& log) {
return log.first.browser_context_id == browser_context_id;
};
DCHECK(std::count_if(active_logs_.begin(), active_logs_.end(), pred) == 0u);
#endif
// Pending logs for this BrowserContext are no longer eligible for upload.
// (Active uploads, if any, are not affected.)
for (auto it = pending_logs_.begin(); it != pending_logs_.end();) {
if (it->browser_context_id == browser_context_id) {
it = pending_logs_.erase(it);
} else {
++it;
}
}
}
bool WebRtcRemoteEventLogManager::PeerConnectionAdded(
const PeerConnectionKey& key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
PrunePendingLogs(); // Infrequent event - good opportunity to prune.
const auto result = active_peer_connections_.insert(key);
return result.second;
}
bool WebRtcRemoteEventLogManager::PeerConnectionRemoved(
const PeerConnectionKey& key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
PrunePendingLogs(); // Infrequent event - good opportunity to prune.
const auto peer_connection = active_peer_connections_.find(key);
if (peer_connection == active_peer_connections_.end()) {
return false;
}
MaybeStopRemoteLogging(key);
active_peer_connections_.erase(peer_connection);
MaybeStartUploading();
return true;
}
bool WebRtcRemoteEventLogManager::StartRemoteLogging(
const PeerConnectionKey& key,
const base::FilePath& browser_context_dir,
size_t max_file_size_bytes,
const std::string& metadata) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
if (!AreLogParametersValid(max_file_size_bytes, metadata)) {
return false;
}
if (!BrowserContextEnabled(key.browser_context_id)) {
return false;
}
// May not start logging inactive peer connections.
if (active_peer_connections_.find(key) == active_peer_connections_.end()) {
return false;
}
// May not restart active remote logs.
auto it = active_logs_.find(key);
if (it != active_logs_.end()) {
LOG(ERROR) << "Remote logging already underway for ("
<< key.render_process_id << ", " << key.lid << ").";
return false;
}
// This is a good opportunity to prune the list of pending logs, potentially
// making room for this file.
PrunePendingLogs();
if (!AdditionalActiveLogAllowed(key.browser_context_id)) {
return false;
}
return StartWritingLog(key, browser_context_dir, max_file_size_bytes,
metadata);
}
bool WebRtcRemoteEventLogManager::EventLogWrite(const PeerConnectionKey& key,
const std::string& message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
auto it = active_logs_.find(key);
if (it == active_logs_.end()) {
return false;
}
return WriteToLogFile(it, message);
}
void WebRtcRemoteEventLogManager::RenderProcessHostExitedDestroyed(
int render_process_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
// Closing files will call MaybeStartUploading(). Avoid letting that upload
// any recently expired files.
PrunePendingLogs();
// Remove all of the peer connections associated with this render process.
// It's important to do this before closing the actual files, because closing
// files can trigger a new upload if no active peer connections are present.
auto pc_it = active_peer_connections_.begin();
while (pc_it != active_peer_connections_.end()) {
if (pc_it->render_process_id == render_process_id) {
pc_it = active_peer_connections_.erase(pc_it);
} else {
++pc_it;
}
}
// Close all of the files that were associated with peer connections which
// belonged to this render process.
auto log_it = active_logs_.begin();
while (log_it != active_logs_.end()) {
if (log_it->first.render_process_id == render_process_id) {
log_it = CloseLogFile(log_it);
} else {
++log_it;
}
}
// Though CloseLogFile() calls this, it's important to also do this
// explicitly, since it could be that no files were closed, but some
// active PeerConnections that were suppressing uploading are now gone.
MaybeStartUploading();
}
void WebRtcRemoteEventLogManager::OnWebRtcEventLogUploadComplete(
const base::FilePath& file_path,
bool upload_successful) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
// Post a task to deallocate the uploader (can't do this directly,
// because this function is a callback from the uploader), potentially
// starting a new upload for the next file.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(
&WebRtcRemoteEventLogManager::OnWebRtcEventLogUploadCompleteInternal,
base::Unretained(this)));
// TODO(eladalon): Send indication of success/failure back to JS.
// https://crbug.com/775415
}
void WebRtcRemoteEventLogManager::SetWebRtcEventLogUploaderFactoryForTesting(
std::unique_ptr<WebRtcEventLogUploader::Factory> uploader_factory) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
uploader_factory_ = std::move(uploader_factory);
// Unit tests would initially set a null uploader factory, so that files would
// be kept around. Some tests would later change to a different factory
// (e.g. one that always simulates upload failure); in that case, we should
// get rid of the null uploader, since it never terminates.
uploader_.reset();
MaybeStartUploading();
}
base::FilePath WebRtcRemoteEventLogManager::GetLogsDirectoryPath(
const base::FilePath& browser_context_dir) {
return browser_context_dir.Append(kRemoteBoundLogSubDirectory);
}
bool WebRtcRemoteEventLogManager::BrowserContextEnabled(
BrowserContextId browser_context_id) const {
const auto it = enabled_browser_contexts_.find(browser_context_id);
return it != enabled_browser_contexts_.cend();
}
WebRtcRemoteEventLogManager::LogFilesMap::iterator
WebRtcRemoteEventLogManager::CloseLogFile(LogFilesMap::iterator it) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
const PeerConnectionKey peer_connection = it->first;
it->second.file.Flush();
it = active_logs_.erase(it); // file.Close() called by destructor.
if (observer_) {
observer_->OnRemoteLogStopped(peer_connection);
}
MaybeStartUploading();
return it;
}
bool WebRtcRemoteEventLogManager::MaybeCreateLogsDirectory(
const base::FilePath& remote_bound_logs_dir) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
if (base::PathExists(remote_bound_logs_dir)) {
if (!base::DirectoryExists(remote_bound_logs_dir)) {
LOG(ERROR) << "Path for remote-bound logs is taken by a non-directory.";
return false;
}
} else if (!base::CreateDirectory(remote_bound_logs_dir)) {
LOG(ERROR) << "Failed to create the local directory for remote-bound logs.";
return false;
}
// TODO(eladalon): Test for appropriate permissions. https://crbug.com/775415
return true;
}
void WebRtcRemoteEventLogManager::AddPendingLogs(
BrowserContextId browser_context_id,
const base::FilePath& remote_bound_logs_dir) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
base::FilePath::StringType pattern =
base::FilePath::StringType(FILE_PATH_LITERAL("*")) +
kRemoteBoundLogExtension;
base::FileEnumerator enumerator(remote_bound_logs_dir,
/*recursive=*/false,
base::FileEnumerator::FILES, pattern);
for (auto path = enumerator.Next(); !path.empty(); path = enumerator.Next()) {
const auto last_modified = enumerator.GetInfo().GetLastModifiedTime();
auto it = pending_logs_.emplace(browser_context_id, path, last_modified);
DCHECK(it.second); // No pre-existing entry.
}
MaybeStartUploading();
}
bool WebRtcRemoteEventLogManager::StartWritingLog(
const PeerConnectionKey& key,
const base::FilePath& browser_context_dir,
size_t max_file_size_bytes,
const std::string& metadata) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
// WriteAtCurrentPos() only allows writing up to max-int at a time. We could
// iterate to do more, but we don't expect to ever need to, so it's easier
// to disallow it.
if (metadata.length() >
static_cast<size_t>(std::numeric_limits<int>::max())) {
LOG(WARNING) << "Metadata too long to be written in one go.";
return false;
}
// Randomize a new filename. In the highly unlikely event that this filename
// is already taken, it will be treated the same way as any other failure
// to start the log file.
// TODO(eladalon): Add a unit test for above comment. https://crbug.com/775415
const std::string unique_filename =
"event_log_" + std::to_string(base::RandUint64());
const base::FilePath base_path = GetLogsDirectoryPath(browser_context_dir);
const base::FilePath file_path = base_path.AppendASCII(unique_filename)
.AddExtension(kRemoteBoundLogExtension);
// Attempt to create the file.
constexpr int file_flags = base::File::FLAG_CREATE | base::File::FLAG_WRITE |
base::File::FLAG_EXCLUSIVE_WRITE;
base::File file(file_path, file_flags);
if (!file.IsValid() || !file.created()) {
LOG(WARNING) << "Couldn't create and/or open remote WebRTC event log file.";
return false;
}
const uint32_t header_host_order =
static_cast<uint32_t>(metadata.length()) |
(kRemoteBoundWebRtcEventLogFileVersion << 24);
static_assert(kRemoteBoundLogFileHeaderSizeBytes == sizeof(uint32_t),
"Restructure this otherwise.");
char header[sizeof(uint32_t)];
base::WriteBigEndian<uint32_t>(header, header_host_order);
int written = file.WriteAtCurrentPos(header, arraysize(header));
if (written != arraysize(header)) {
LOG(WARNING) << "Failed to write header to log file.";
DiscardLogFile(&file, file_path);
return false;
}
const int metadata_length = static_cast<int>(metadata.length());
written = file.WriteAtCurrentPos(metadata.c_str(), metadata_length);
if (written != metadata_length) {
LOG(WARNING) << "Failed to write metadata to log file.";
DiscardLogFile(&file, file_path);
return false;
}
// Record that we're now writing this remote-bound log to this file.
const size_t header_and_metadata_size_bytes =
kRemoteBoundLogFileHeaderSizeBytes + metadata_length;
const auto it = active_logs_.emplace(
key, LogFile(file_path, std::move(file), max_file_size_bytes,
header_and_metadata_size_bytes));
DCHECK(it.second);
observer_->OnRemoteLogStarted(key, file_path);
return true;
}
void WebRtcRemoteEventLogManager::MaybeStopRemoteLogging(
const PeerConnectionKey& key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
const auto it = active_logs_.find(key);
if (it == active_logs_.end()) {
return;
}
it->second.file.Flush();
it->second.file.Close();
// The current time is a good enough approximation of the file's last
// modification time.
const base::Time last_modified = base::Time::Now();
// The stopped log becomes a pending log. It is no longer an active log.
const auto emplace_result = pending_logs_.emplace(
key.browser_context_id, it->second.path, last_modified);
DCHECK(emplace_result.second); // No pre-existing entry.
active_logs_.erase(it);
observer_->OnRemoteLogStopped(key);
MaybeStartUploading();
}
void WebRtcRemoteEventLogManager::PrunePendingLogs() {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
const base::Time oldest_non_expired_timestamp =
base::Time::Now() - kRemoteBoundWebRtcEventLogsMaxRetention;
for (auto it = pending_logs_.begin(); it != pending_logs_.end();) {
if (it->last_modified < oldest_non_expired_timestamp) {
if (!base::DeleteFile(it->path, /*recursive=*/false)) {
LOG(ERROR) << "Failed to delete " << it->path << ".";
}
it = pending_logs_.erase(it);
} else {
++it;
}
}
}
bool WebRtcRemoteEventLogManager::AdditionalActiveLogAllowed(
BrowserContextId browser_context_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
// Limit over concurrently active logs (across BrowserContext-s).
if (active_logs_.size() >= kMaxActiveRemoteBoundWebRtcEventLogs) {
return false;
}
// Limit over the number of pending logs (per BrowserContext). We count active
// logs too, since they become pending logs once completed.
const size_t active_count = std::count_if(
active_logs_.begin(), active_logs_.end(),
[browser_context_id](const decltype(active_logs_)::value_type& log) {
return log.first.browser_context_id == browser_context_id;
});
const size_t pending_count = std::count_if(
pending_logs_.begin(), pending_logs_.end(),
[browser_context_id](const decltype(pending_logs_)::value_type& log) {
return log.browser_context_id == browser_context_id;
});
return active_count + pending_count < kMaxPendingRemoteBoundWebRtcEventLogs;
}
bool WebRtcRemoteEventLogManager::UploadingAllowed() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
return active_peer_connections_.empty();
}
void WebRtcRemoteEventLogManager::MaybeStartUploading() {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
PrunePendingLogs(); // Avoid uploading freshly expired files.
if (uploader_) {
return; // Upload already underway.
}
if (pending_logs_.empty()) {
return; // Nothing to upload.
}
if (!UploadingAllowed()) {
return;
}
// The uploader takes ownership of the file; it's no longer considered to be
// pending. (If the upload fails, the log will be deleted.)
// TODO(eladalon): Add more refined retry behavior, so that we would not
// delete the log permanently if the network is just down, on the one hand,
// but also would not be uploading unlimited data on endless retries on the
// other hand. https://crbug.com/775415
uploader_ = uploader_factory_->Create(pending_logs_.begin()->path, this);
pending_logs_.erase(pending_logs_.begin());
}
void WebRtcRemoteEventLogManager::OnWebRtcEventLogUploadCompleteInternal() {
DCHECK_CALLED_ON_VALID_SEQUENCE(io_task_sequence_checker_);
uploader_.reset();
MaybeStartUploading();
}
} // namespace content