1 // Copyright 2019 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "media/mojo/services/fuchsia_cdm_manager.h"
7 #include <fuchsia/media/drm/cpp/fidl.h>
8 #include <lib/fidl/cpp/binding_set.h>
9 #include <lib/fpromise/promise.h>
11 #include "base/containers/flat_set.h"
12 #include "base/containers/unique_ptr_adapters.h"
13 #include "base/files/file_enumerator.h"
14 #include "base/files/file_path.h"
15 #include "base/files/file_util.h"
16 #include "base/fuchsia/file_utils.h"
17 #include "base/fuchsia/fuchsia_logging.h"
18 #include "base/functional/bind.h"
19 #include "base/functional/callback.h"
20 #include "base/hash/hash.h"
21 #include "base/logging.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/task/task_traits.h"
24 #include "base/task/thread_pool.h"
25 #include "base/time/time.h"
26 #include "media/mojo/services/fuchsia_cdm_provisioning_fetcher_impl.h"
27 #include "third_party/abseil-cpp/absl/types/optional.h"
28 #include "url/origin.h"
34 struct CdmDirectoryInfo {
40 // Enumerates all the files in the directory to determine its size and
41 // the most recent "last used" time.
42 // The implementation is based on base::ComputeDirectorySize(), with the
43 // addition of most-recently-modified calculation, and inclusion of directory
44 // node sizes toward the total.
45 CdmDirectoryInfo GetCdmDirectoryInfo(const base::FilePath& path) {
46 uint64_t directory_size = 0;
48 base::FileEnumerator enumerator(
49 path, true /* recursive */,
50 base::FileEnumerator::DIRECTORIES | base::FileEnumerator::FILES);
51 while (!enumerator.Next().empty()) {
52 const base::FileEnumerator::FileInfo info = enumerator.GetInfo();
53 if (info.GetSize() > 0) {
54 directory_size += info.GetSize();
56 last_used = std::max(last_used, info.GetLastModifiedTime());
60 .last_used = last_used,
61 .size_bytes = directory_size,
65 void ApplyCdmStorageQuota(base::FilePath cdm_data_path,
66 uint64_t cdm_data_quota_bytes) {
67 // TODO(crbug.com/1148334): Migrate to using a platform-provided quota
68 // mechanism to manage CDM storage.
69 VLOG(2) << "Enumerating CDM data directories.";
71 uint64_t directories_size_bytes = 0;
72 std::vector<CdmDirectoryInfo> directories_info;
74 // CDM storage consistes of per-origin directories, each containing one or
75 // more per-key-system sub-directories. Each per-origin-per-key-system
76 // directory is assumed to be independent of other CDM data.
77 base::FileEnumerator by_origin(cdm_data_path, false /* recursive */,
78 base::FileEnumerator::DIRECTORIES);
80 const base::FilePath origin_directory = by_origin.Next();
81 if (origin_directory.empty()) {
84 base::FileEnumerator by_key_system(origin_directory, false /* recursive */,
85 base::FileEnumerator::DIRECTORIES);
87 const base::FilePath key_system_directory = by_key_system.Next();
88 if (key_system_directory.empty()) {
91 directories_info.push_back(GetCdmDirectoryInfo(key_system_directory));
92 directories_size_bytes += directories_info.back().size_bytes;
96 if (directories_size_bytes <= cdm_data_quota_bytes) {
100 VLOG(1) << "Removing least recently accessed CDM data.";
102 // Enumerate directories starting with the least most recently "used",
103 // deleting them until the the total amount of CDM data is within quota.
104 std::sort(directories_info.begin(), directories_info.end(),
105 [](const CdmDirectoryInfo& lhs, const CdmDirectoryInfo& rhs) {
106 return lhs.last_used < rhs.last_used;
108 base::flat_set<base::FilePath> affected_origin_directories;
109 for (const auto& directory_info : directories_info) {
110 if (directories_size_bytes <= cdm_data_quota_bytes) {
114 VLOG(1) << "Removing " << directory_info.path;
115 base::DeletePathRecursively(directory_info.path);
116 affected_origin_directories.insert(directory_info.path.DirName());
118 DCHECK_GE(directories_size_bytes, directory_info.size_bytes);
119 directories_size_bytes -= directory_info.size_bytes;
122 // Enumerate all the origin directories that had sub-directories deleted,
123 // and delete any that are now empty.
124 for (const auto& origin_directory : affected_origin_directories) {
125 if (base::IsDirectoryEmpty(origin_directory)) {
126 base::DeleteFile(origin_directory);
131 std::string HexEncodeHash(const std::string& name) {
132 uint32_t hash = base::PersistentHash(name);
133 return base::HexEncode(&hash, sizeof(uint32_t));
136 // Returns a nullopt if storage was created successfully.
137 absl::optional<base::File::Error> CreateStorageDirectory(base::FilePath path) {
138 base::File::Error error;
139 bool success = base::CreateDirectoryAndGetError(path, &error);
146 FuchsiaCdmManager* g_fuchsia_cdm_manager_instance = nullptr;
150 // Manages individual KeySystem connections. Provides data stores and
151 // ProvisioningFetchers to the KeySystem server and associating CDM requests
152 // with the appropriate data store.
153 class FuchsiaCdmManager::KeySystemClient {
155 // Construct an unbound KeySystemClient. The |name| field should be the EME
156 // name of the key system, such as org.w3.clearkey. It is only used for
158 explicit KeySystemClient(std::string name) : name_(std::move(name)) {}
159 ~KeySystemClient() = default;
161 // Registers an error handler and binds the KeySystem handle. If Bind returns
162 // an error, the error handler will not be called.
164 fidl::InterfaceHandle<fuchsia::media::drm::KeySystem> key_system_handle,
165 base::OnceClosure error_callback) {
166 key_system_.set_error_handler(
167 [name = name_, error_callback = std::move(error_callback)](
168 zx_status_t status) mutable {
169 ZX_LOG(ERROR, status) << "KeySystem " << name << " closed channel.";
170 std::move(error_callback).Run();
173 return key_system_.Bind(std::move(key_system_handle));
177 base::FilePath storage_path,
178 CreateFetcherCB create_fetcher_callback,
179 fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule>
181 absl::optional<DataStoreId> data_store_id = GetDataStoreIdForPath(
182 std::move(storage_path), std::move(create_fetcher_callback));
183 if (!data_store_id) {
184 request.Close(ZX_ERR_NO_RESOURCES);
188 // If this request triggered an AddDataStore() request, then that will be
189 // processed before this call. If AddDataStore() fails, then the
190 // |data_store_id| will not be valid and the create call will close the
191 // |request| with a ZX_ERR_NOT_FOUND epitaph.
192 key_system_->CreateContentDecryptionModule2(data_store_id.value(),
197 using DataStoreId = uint32_t;
199 absl::optional<DataStoreId> GetDataStoreIdForPath(
200 base::FilePath storage_path,
201 CreateFetcherCB create_fetcher_callback) {
202 // If we have already added a data store id for that path, just use that
204 auto it = data_store_ids_by_path_.find(storage_path);
205 if (it != data_store_ids_by_path_.end()) {
209 fidl::InterfaceHandle<fuchsia::io::Directory> data_directory =
210 base::OpenDirectoryHandle(storage_path);
211 if (!data_directory.is_valid()) {
212 DLOG(ERROR) << "Unable to OpenDirectory " << storage_path;
213 return absl::nullopt;
216 auto provisioning_fetcher =
217 std::make_unique<FuchsiaCdmProvisioningFetcherImpl>(
218 std::move(create_fetcher_callback));
220 DataStoreId data_store_id = next_data_store_id_++;
222 fuchsia::media::drm::DataStoreParams params;
223 params.set_data_directory(std::move(data_directory));
224 params.set_provisioning_fetcher(provisioning_fetcher->Bind(
225 base::BindOnce(&KeySystemClient::OnProvisioningFetcherError,
226 base::Unretained(this), provisioning_fetcher.get())));
228 key_system_->AddDataStore(
229 data_store_id, std::move(params),
230 [this, data_store_id, storage_path](
231 fpromise::result<void, fuchsia::media::drm::Error> result) {
232 if (result.is_error()) {
233 DLOG(ERROR) << "Failed to add data store " << data_store_id
234 << ", path: " << storage_path;
235 data_store_ids_by_path_.erase(storage_path);
240 provisioning_fetchers_.insert(std::move(provisioning_fetcher));
241 data_store_ids_by_path_.emplace(std::move(storage_path), data_store_id);
242 return data_store_id;
245 void OnProvisioningFetcherError(
246 FuchsiaCdmProvisioningFetcherImpl* provisioning_fetcher) {
247 provisioning_fetchers_.erase(provisioning_fetcher);
250 // The EME name of the key system, such as org.w3.clearkey
253 // FIDL InterfacePtr to the platform provided KeySystem
254 fuchsia::media::drm::KeySystemPtr key_system_;
256 // A set of ProvisioningFetchers, one for each data store that gets added.
257 // The KeySystem might close the channel even if the data store remains in
259 base::flat_set<std::unique_ptr<FuchsiaCdmProvisioningFetcherImpl>,
260 base::UniquePtrComparator>
261 provisioning_fetchers_;
263 // The next data store id to use when registering data stores with the
264 // KeySystem. Data store ids are scoped to the KeySystem channel. Value starts
265 // at 1 because 0 is a reserved sentinel value for
266 // fuchsia::media::drm::NO_DATA_STORE. The value will be incremented each time
267 // we add a DataStore.
268 DataStoreId next_data_store_id_ = 1;
270 // A map of directory paths to data store ids that have been added to the
272 base::flat_map<base::FilePath, DataStoreId> data_store_ids_by_path_;
276 FuchsiaCdmManager* FuchsiaCdmManager::GetInstance() {
277 return g_fuchsia_cdm_manager_instance;
280 FuchsiaCdmManager::FuchsiaCdmManager(
281 CreateKeySystemCallbackMap create_key_system_callbacks_by_name,
282 base::FilePath cdm_data_path,
283 absl::optional<uint64_t> cdm_data_quota_bytes)
284 : create_key_system_callbacks_by_name_(
285 std::move(create_key_system_callbacks_by_name)),
286 cdm_data_path_(std::move(cdm_data_path)),
287 cdm_data_quota_bytes_(std::move(cdm_data_quota_bytes)),
288 storage_task_runner_(
289 base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})) {
290 // To avoid potential for the CDM directory "cleanup" task removing
291 // CDM data directories that are in active use, the |storage_task_runner_| is
292 // sequenced, thereby ensuring cleanup completes before any CDM activities
294 if (cdm_data_quota_bytes_) {
295 ApplyCdmStorageQuota(cdm_data_path_, *cdm_data_quota_bytes_);
298 DCHECK(!g_fuchsia_cdm_manager_instance);
299 g_fuchsia_cdm_manager_instance = this;
302 FuchsiaCdmManager::~FuchsiaCdmManager() {
303 DCHECK_EQ(g_fuchsia_cdm_manager_instance, this);
304 g_fuchsia_cdm_manager_instance = nullptr;
307 void FuchsiaCdmManager::CreateAndProvision(
308 const std::string& key_system,
309 const url::Origin& origin,
310 CreateFetcherCB create_fetcher_cb,
311 fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule>
313 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
315 base::FilePath storage_path = GetStoragePath(key_system, origin);
317 auto task = base::BindOnce(&CreateStorageDirectory, storage_path);
318 storage_task_runner_->PostTaskAndReplyWithResult(
319 FROM_HERE, std::move(task),
320 base::BindOnce(&FuchsiaCdmManager::CreateCdm, weak_factory_.GetWeakPtr(),
321 key_system, std::move(create_fetcher_cb),
322 std::move(request), std::move(storage_path)));
325 void FuchsiaCdmManager::set_on_key_system_disconnect_for_test_callback(
326 base::RepeatingCallback<void(const std::string&)> disconnect_callback) {
327 on_key_system_disconnect_for_test_callback_ = std::move(disconnect_callback);
330 FuchsiaCdmManager::KeySystemClient*
331 FuchsiaCdmManager::GetOrCreateKeySystemClient(
332 const std::string& key_system_name) {
333 auto client_it = active_key_system_clients_by_name_.find(key_system_name);
334 if (client_it == active_key_system_clients_by_name_.end()) {
335 // If there is no active one, attempt to create one.
336 return CreateKeySystemClient(key_system_name);
338 return client_it->second.get();
341 FuchsiaCdmManager::KeySystemClient* FuchsiaCdmManager::CreateKeySystemClient(
342 const std::string& key_system_name) {
343 const auto create_callback_it =
344 create_key_system_callbacks_by_name_.find(key_system_name);
345 if (create_callback_it == create_key_system_callbacks_by_name_.cend()) {
346 DLOG(ERROR) << "Key system is not supported: " << key_system_name;
350 auto key_system_client = std::make_unique<KeySystemClient>(key_system_name);
351 zx_status_t status = key_system_client->Bind(
352 create_callback_it->second.Run(),
353 base::BindOnce(&FuchsiaCdmManager::OnKeySystemClientError,
354 base::Unretained(this), key_system_name));
355 if (status != ZX_OK) {
356 ZX_DLOG(ERROR, status) << "Unable to bind to KeySystem";
360 KeySystemClient* key_system_client_ptr = key_system_client.get();
361 active_key_system_clients_by_name_.emplace(key_system_name,
362 std::move(key_system_client));
363 return key_system_client_ptr;
366 base::FilePath FuchsiaCdmManager::GetStoragePath(const std::string& key_system,
367 const url::Origin& origin) {
368 return cdm_data_path_.Append(HexEncodeHash(origin.Serialize()))
369 .Append(HexEncodeHash(key_system));
372 void FuchsiaCdmManager::CreateCdm(
373 const std::string& key_system_name,
374 CreateFetcherCB create_fetcher_cb,
375 fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule>
377 base::FilePath storage_path,
378 absl::optional<base::File::Error> storage_creation_error) {
379 DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
381 if (storage_creation_error) {
382 DLOG(ERROR) << "Failed to create directory: " << storage_path
383 << ", error: " << *storage_creation_error;
384 request.Close(ZX_ERR_NO_RESOURCES);
388 KeySystemClient* key_system_client =
389 GetOrCreateKeySystemClient(key_system_name);
390 if (!key_system_client) {
391 // GetOrCreateKeySystemClient will log the reason for failure.
392 request.Close(ZX_ERR_NOT_FOUND);
396 key_system_client->CreateCdm(std::move(storage_path),
397 std::move(create_fetcher_cb),
401 void FuchsiaCdmManager::OnKeySystemClientError(
402 const std::string& key_system_name) {
403 if (on_key_system_disconnect_for_test_callback_) {
404 on_key_system_disconnect_for_test_callback_.Run(key_system_name);
407 active_key_system_clients_by_name_.erase(key_system_name);