| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/mojo/services/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/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/functional/bind.h" |
| #include "base/functional/callback.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 "base/time/time.h" |
| #include "media/mojo/services/fuchsia_cdm_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 {}; |
| } |
| |
| FuchsiaCdmManager* g_fuchsia_cdm_manager_instance = nullptr; |
| |
| } // 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<FuchsiaCdmProvisioningFetcherImpl>( |
| 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( |
| FuchsiaCdmProvisioningFetcherImpl* 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<FuchsiaCdmProvisioningFetcherImpl>, |
| 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_; |
| }; |
| |
| // static |
| FuchsiaCdmManager* FuchsiaCdmManager::GetInstance() { |
| return g_fuchsia_cdm_manager_instance; |
| } |
| |
| 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_); |
| } |
| |
| DCHECK(!g_fuchsia_cdm_manager_instance); |
| g_fuchsia_cdm_manager_instance = this; |
| } |
| |
| FuchsiaCdmManager::~FuchsiaCdmManager() { |
| DCHECK_EQ(g_fuchsia_cdm_manager_instance, this); |
| g_fuchsia_cdm_manager_instance = nullptr; |
| } |
| |
| 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 |