blob: 861afb293adad7735ccf6771a607135d2ee0e9b2 [file] [log] [blame]
// Copyright 2019 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 "media/fuchsia/cdm/service/fuchsia_cdm_manager.h"
#include <fuchsia/media/drm/cpp/fidl.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/fpromise/promise.h>
#include "base/bind.h"
#include "base/callback.h"
#include "base/containers/flat_set.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/fuchsia/file_utils.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "media/fuchsia/cdm/service/provisioning_fetcher_impl.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/origin.h"
namespace media {
namespace {
struct CdmDirectoryInfo {
base::FilePath path;
base::Time last_used;
uint64_t size_bytes;
};
// Enumerates all the files in the directory to determine its size and
// the most recent "last used" time.
// The implementation is based on base::ComputeDirectorySize(), with the
// addition of most-recently-modified calculation, and inclusion of directory
// node sizes toward the total.
CdmDirectoryInfo GetCdmDirectoryInfo(const base::FilePath& path) {
uint64_t directory_size = 0;
base::Time last_used;
base::FileEnumerator enumerator(
path, true /* recursive */,
base::FileEnumerator::DIRECTORIES | base::FileEnumerator::FILES);
while (!enumerator.Next().empty()) {
const base::FileEnumerator::FileInfo info = enumerator.GetInfo();
if (info.GetSize() > 0)
directory_size += info.GetSize();
last_used = std::max(last_used, info.GetLastModifiedTime());
}
return {
.path = path,
.last_used = last_used,
.size_bytes = directory_size,
};
}
void ApplyCdmStorageQuota(base::FilePath cdm_data_path,
uint64_t cdm_data_quota_bytes) {
// TODO(crbug.com/1148334): Migrate to using a platform-provided quota
// mechanism to manage CDM storage.
VLOG(2) << "Enumerating CDM data directories.";
uint64_t directories_size_bytes = 0;
std::vector<CdmDirectoryInfo> directories_info;
// CDM storage consistes of per-origin directories, each containing one or
// more per-key-system sub-directories. Each per-origin-per-key-system
// directory is assumed to be independent of other CDM data.
base::FileEnumerator by_origin(cdm_data_path, false /* recursive */,
base::FileEnumerator::DIRECTORIES);
for (;;) {
const base::FilePath origin_directory = by_origin.Next();
if (origin_directory.empty())
break;
base::FileEnumerator by_key_system(origin_directory, false /* recursive */,
base::FileEnumerator::DIRECTORIES);
for (;;) {
const base::FilePath key_system_directory = by_key_system.Next();
if (key_system_directory.empty())
break;
directories_info.push_back(GetCdmDirectoryInfo(key_system_directory));
directories_size_bytes += directories_info.back().size_bytes;
}
}
if (directories_size_bytes <= cdm_data_quota_bytes)
return;
VLOG(1) << "Removing least recently accessed CDM data.";
// Enumerate directories starting with the least most recently "used",
// deleting them until the the total amount of CDM data is within quota.
std::sort(directories_info.begin(), directories_info.end(),
[](const CdmDirectoryInfo& lhs, const CdmDirectoryInfo& rhs) {
return lhs.last_used < rhs.last_used;
});
base::flat_set<base::FilePath> affected_origin_directories;
for (const auto& directory_info : directories_info) {
if (directories_size_bytes <= cdm_data_quota_bytes)
break;
VLOG(1) << "Removing " << directory_info.path;
base::DeletePathRecursively(directory_info.path);
affected_origin_directories.insert(directory_info.path.DirName());
DCHECK_GE(directories_size_bytes, directory_info.size_bytes);
directories_size_bytes -= directory_info.size_bytes;
}
// Enumerate all the origin directories that had sub-directories deleted,
// and delete any that are now empty.
for (const auto& origin_directory : affected_origin_directories) {
if (base::IsDirectoryEmpty(origin_directory))
base::DeleteFile(origin_directory);
}
}
std::string HexEncodeHash(const std::string& name) {
uint32_t hash = base::PersistentHash(name);
return base::HexEncode(&hash, sizeof(uint32_t));
}
// Returns a nullopt if storage was created successfully.
absl::optional<base::File::Error> CreateStorageDirectory(base::FilePath path) {
base::File::Error error;
bool success = base::CreateDirectoryAndGetError(path, &error);
if (!success) {
return error;
}
return {};
}
} // namespace
// Manages individual KeySystem connections. Provides data stores and
// ProvisioningFetchers to the KeySystem server and associating CDM requests
// with the appropriate data store.
class FuchsiaCdmManager::KeySystemClient {
public:
// Construct an unbound KeySystemClient. The |name| field should be the EME
// name of the key system, such as org.w3.clearkey. It is only used for
// logging purposes.
explicit KeySystemClient(std::string name) : name_(std::move(name)) {}
~KeySystemClient() = default;
// Registers an error handler and binds the KeySystem handle. If Bind returns
// an error, the error handler will not be called.
zx_status_t Bind(
fidl::InterfaceHandle<fuchsia::media::drm::KeySystem> key_system_handle,
base::OnceClosure error_callback) {
key_system_.set_error_handler(
[name = name_, error_callback = std::move(error_callback)](
zx_status_t status) mutable {
ZX_LOG(ERROR, status) << "KeySystem " << name << " closed channel.";
std::move(error_callback).Run();
});
return key_system_.Bind(std::move(key_system_handle));
}
void CreateCdm(
base::FilePath storage_path,
CreateFetcherCB create_fetcher_callback,
fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule>
request) {
absl::optional<DataStoreId> data_store_id = GetDataStoreIdForPath(
std::move(storage_path), std::move(create_fetcher_callback));
if (!data_store_id) {
request.Close(ZX_ERR_NO_RESOURCES);
return;
}
// If this request triggered an AddDataStore() request, then that will be
// processed before this call. If AddDataStore() fails, then the
// |data_store_id| will not be valid and the create call will close the
// |request| with a ZX_ERR_NOT_FOUND epitaph.
key_system_->CreateContentDecryptionModule2(data_store_id.value(),
std::move(request));
}
private:
using DataStoreId = uint32_t;
absl::optional<DataStoreId> GetDataStoreIdForPath(
base::FilePath storage_path,
CreateFetcherCB create_fetcher_callback) {
// If we have already added a data store id for that path, just use that
// one.
auto it = data_store_ids_by_path_.find(storage_path);
if (it != data_store_ids_by_path_.end()) {
return it->second;
}
fidl::InterfaceHandle<fuchsia::io::Directory> data_directory =
base::OpenDirectoryHandle(storage_path);
if (!data_directory.is_valid()) {
DLOG(ERROR) << "Unable to OpenDirectory " << storage_path;
return absl::nullopt;
}
auto provisioning_fetcher = std::make_unique<ProvisioningFetcherImpl>(
std::move(create_fetcher_callback));
DataStoreId data_store_id = next_data_store_id_++;
fuchsia::media::drm::DataStoreParams params;
params.set_data_directory(std::move(data_directory));
params.set_provisioning_fetcher(provisioning_fetcher->Bind(
base::BindOnce(&KeySystemClient::OnProvisioningFetcherError,
base::Unretained(this), provisioning_fetcher.get())));
key_system_->AddDataStore(
data_store_id, std::move(params),
[this, data_store_id, storage_path](
fpromise::result<void, fuchsia::media::drm::Error> result) {
if (result.is_error()) {
DLOG(ERROR) << "Failed to add data store " << data_store_id
<< ", path: " << storage_path;
data_store_ids_by_path_.erase(storage_path);
return;
}
});
provisioning_fetchers_.insert(std::move(provisioning_fetcher));
data_store_ids_by_path_.emplace(std::move(storage_path), data_store_id);
return data_store_id;
}
void OnProvisioningFetcherError(
ProvisioningFetcherImpl* provisioning_fetcher) {
provisioning_fetchers_.erase(provisioning_fetcher);
}
// The EME name of the key system, such as org.w3.clearkey
std::string name_;
// FIDL InterfacePtr to the platform provided KeySystem
fuchsia::media::drm::KeySystemPtr key_system_;
// A set of ProvisioningFetchers, one for each data store that gets added.
// The KeySystem might close the channel even if the data store remains in
// use.
base::flat_set<std::unique_ptr<ProvisioningFetcherImpl>,
base::UniquePtrComparator>
provisioning_fetchers_;
// The next data store id to use when registering data stores with the
// KeySystem. Data store ids are scoped to the KeySystem channel. Value starts
// at 1 because 0 is a reserved sentinel value for
// fuchsia::media::drm::NO_DATA_STORE. The value will be incremented each time
// we add a DataStore.
DataStoreId next_data_store_id_ = 1;
// A map of directory paths to data store ids that have been added to the
// KeySystem.
base::flat_map<base::FilePath, DataStoreId> data_store_ids_by_path_;
};
FuchsiaCdmManager::FuchsiaCdmManager(
CreateKeySystemCallbackMap create_key_system_callbacks_by_name,
base::FilePath cdm_data_path,
absl::optional<uint64_t> cdm_data_quota_bytes)
: create_key_system_callbacks_by_name_(
std::move(create_key_system_callbacks_by_name)),
cdm_data_path_(std::move(cdm_data_path)),
cdm_data_quota_bytes_(std::move(cdm_data_quota_bytes)),
storage_task_runner_(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})) {
// To avoid potential for the CDM directory "cleanup" task removing
// CDM data directories that are in active use, the |storage_task_runner_| is
// sequenced, thereby ensuring cleanup completes before any CDM activities
// start.
if (cdm_data_quota_bytes)
ApplyCdmStorageQuota(cdm_data_path_, *cdm_data_quota_bytes);
}
FuchsiaCdmManager::~FuchsiaCdmManager() = default;
void FuchsiaCdmManager::CreateAndProvision(
const std::string& key_system,
const url::Origin& origin,
CreateFetcherCB create_fetcher_cb,
fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule>
request) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
base::FilePath storage_path = GetStoragePath(key_system, origin);
auto task = base::BindOnce(&CreateStorageDirectory, storage_path);
storage_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, std::move(task),
base::BindOnce(&FuchsiaCdmManager::CreateCdm, weak_factory_.GetWeakPtr(),
key_system, std::move(create_fetcher_cb),
std::move(request), std::move(storage_path)));
}
void FuchsiaCdmManager::set_on_key_system_disconnect_for_test_callback(
base::RepeatingCallback<void(const std::string&)> disconnect_callback) {
on_key_system_disconnect_for_test_callback_ = std::move(disconnect_callback);
}
FuchsiaCdmManager::KeySystemClient*
FuchsiaCdmManager::GetOrCreateKeySystemClient(
const std::string& key_system_name) {
auto client_it = active_key_system_clients_by_name_.find(key_system_name);
if (client_it == active_key_system_clients_by_name_.end()) {
// If there is no active one, attempt to create one.
return CreateKeySystemClient(key_system_name);
}
return client_it->second.get();
}
FuchsiaCdmManager::KeySystemClient* FuchsiaCdmManager::CreateKeySystemClient(
const std::string& key_system_name) {
const auto create_callback_it =
create_key_system_callbacks_by_name_.find(key_system_name);
if (create_callback_it == create_key_system_callbacks_by_name_.cend()) {
DLOG(ERROR) << "Key system is not supported: " << key_system_name;
return nullptr;
}
auto key_system_client = std::make_unique<KeySystemClient>(key_system_name);
zx_status_t status = key_system_client->Bind(
create_callback_it->second.Run(),
base::BindOnce(&FuchsiaCdmManager::OnKeySystemClientError,
base::Unretained(this), key_system_name));
if (status != ZX_OK) {
ZX_DLOG(ERROR, status) << "Unable to bind to KeySystem";
return nullptr;
}
KeySystemClient* key_system_client_ptr = key_system_client.get();
active_key_system_clients_by_name_.emplace(key_system_name,
std::move(key_system_client));
return key_system_client_ptr;
}
base::FilePath FuchsiaCdmManager::GetStoragePath(const std::string& key_system,
const url::Origin& origin) {
return cdm_data_path_.Append(HexEncodeHash(origin.Serialize()))
.Append(HexEncodeHash(key_system));
}
void FuchsiaCdmManager::CreateCdm(
const std::string& key_system_name,
CreateFetcherCB create_fetcher_cb,
fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule>
request,
base::FilePath storage_path,
absl::optional<base::File::Error> storage_creation_error) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (storage_creation_error) {
DLOG(ERROR) << "Failed to create directory: " << storage_path
<< ", error: " << *storage_creation_error;
request.Close(ZX_ERR_NO_RESOURCES);
return;
}
KeySystemClient* key_system_client =
GetOrCreateKeySystemClient(key_system_name);
if (!key_system_client) {
// GetOrCreateKeySystemClient will log the reason for failure.
request.Close(ZX_ERR_NOT_FOUND);
return;
}
key_system_client->CreateCdm(std::move(storage_path),
std::move(create_fetcher_cb),
std::move(request));
}
void FuchsiaCdmManager::OnKeySystemClientError(
const std::string& key_system_name) {
if (on_key_system_disconnect_for_test_callback_) {
on_key_system_disconnect_for_test_callback_.Run(key_system_name);
}
active_key_system_clients_by_name_.erase(key_system_name);
}
} // namespace media