Kaido Kert | 25902c6 | 2024-06-17 17:10:28 -0700 | [diff] [blame^] | 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. |
| 4 | |
| 5 | #include "media/mojo/services/fuchsia_cdm_manager.h" |
| 6 | |
| 7 | #include <fuchsia/media/drm/cpp/fidl.h> |
| 8 | #include <lib/fidl/cpp/binding_set.h> |
| 9 | #include <lib/fpromise/promise.h> |
| 10 | |
| 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" |
| 29 | |
| 30 | namespace media { |
| 31 | |
| 32 | namespace { |
| 33 | |
| 34 | struct CdmDirectoryInfo { |
| 35 | base::FilePath path; |
| 36 | base::Time last_used; |
| 37 | uint64_t size_bytes; |
| 38 | }; |
| 39 | |
| 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; |
| 47 | base::Time last_used; |
| 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(); |
| 55 | } |
| 56 | last_used = std::max(last_used, info.GetLastModifiedTime()); |
| 57 | } |
| 58 | return { |
| 59 | .path = path, |
| 60 | .last_used = last_used, |
| 61 | .size_bytes = directory_size, |
| 62 | }; |
| 63 | } |
| 64 | |
| 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."; |
| 70 | |
| 71 | uint64_t directories_size_bytes = 0; |
| 72 | std::vector<CdmDirectoryInfo> directories_info; |
| 73 | |
| 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); |
| 79 | for (;;) { |
| 80 | const base::FilePath origin_directory = by_origin.Next(); |
| 81 | if (origin_directory.empty()) { |
| 82 | break; |
| 83 | } |
| 84 | base::FileEnumerator by_key_system(origin_directory, false /* recursive */, |
| 85 | base::FileEnumerator::DIRECTORIES); |
| 86 | for (;;) { |
| 87 | const base::FilePath key_system_directory = by_key_system.Next(); |
| 88 | if (key_system_directory.empty()) { |
| 89 | break; |
| 90 | } |
| 91 | directories_info.push_back(GetCdmDirectoryInfo(key_system_directory)); |
| 92 | directories_size_bytes += directories_info.back().size_bytes; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | if (directories_size_bytes <= cdm_data_quota_bytes) { |
| 97 | return; |
| 98 | } |
| 99 | |
| 100 | VLOG(1) << "Removing least recently accessed CDM data."; |
| 101 | |
| 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; |
| 107 | }); |
| 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) { |
| 111 | break; |
| 112 | } |
| 113 | |
| 114 | VLOG(1) << "Removing " << directory_info.path; |
| 115 | base::DeletePathRecursively(directory_info.path); |
| 116 | affected_origin_directories.insert(directory_info.path.DirName()); |
| 117 | |
| 118 | DCHECK_GE(directories_size_bytes, directory_info.size_bytes); |
| 119 | directories_size_bytes -= directory_info.size_bytes; |
| 120 | } |
| 121 | |
| 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); |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | std::string HexEncodeHash(const std::string& name) { |
| 132 | uint32_t hash = base::PersistentHash(name); |
| 133 | return base::HexEncode(&hash, sizeof(uint32_t)); |
| 134 | } |
| 135 | |
| 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); |
| 140 | if (!success) { |
| 141 | return error; |
| 142 | } |
| 143 | return {}; |
| 144 | } |
| 145 | |
| 146 | FuchsiaCdmManager* g_fuchsia_cdm_manager_instance = nullptr; |
| 147 | |
| 148 | } // namespace |
| 149 | |
| 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 { |
| 154 | public: |
| 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 |
| 157 | // logging purposes. |
| 158 | explicit KeySystemClient(std::string name) : name_(std::move(name)) {} |
| 159 | ~KeySystemClient() = default; |
| 160 | |
| 161 | // Registers an error handler and binds the KeySystem handle. If Bind returns |
| 162 | // an error, the error handler will not be called. |
| 163 | zx_status_t Bind( |
| 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(); |
| 171 | }); |
| 172 | |
| 173 | return key_system_.Bind(std::move(key_system_handle)); |
| 174 | } |
| 175 | |
| 176 | void CreateCdm( |
| 177 | base::FilePath storage_path, |
| 178 | CreateFetcherCB create_fetcher_callback, |
| 179 | fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule> |
| 180 | request) { |
| 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); |
| 185 | return; |
| 186 | } |
| 187 | |
| 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(), |
| 193 | std::move(request)); |
| 194 | } |
| 195 | |
| 196 | private: |
| 197 | using DataStoreId = uint32_t; |
| 198 | |
| 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 |
| 203 | // one. |
| 204 | auto it = data_store_ids_by_path_.find(storage_path); |
| 205 | if (it != data_store_ids_by_path_.end()) { |
| 206 | return it->second; |
| 207 | } |
| 208 | |
| 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; |
| 214 | } |
| 215 | |
| 216 | auto provisioning_fetcher = |
| 217 | std::make_unique<FuchsiaCdmProvisioningFetcherImpl>( |
| 218 | std::move(create_fetcher_callback)); |
| 219 | |
| 220 | DataStoreId data_store_id = next_data_store_id_++; |
| 221 | |
| 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()))); |
| 227 | |
| 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); |
| 236 | return; |
| 237 | } |
| 238 | }); |
| 239 | |
| 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; |
| 243 | } |
| 244 | |
| 245 | void OnProvisioningFetcherError( |
| 246 | FuchsiaCdmProvisioningFetcherImpl* provisioning_fetcher) { |
| 247 | provisioning_fetchers_.erase(provisioning_fetcher); |
| 248 | } |
| 249 | |
| 250 | // The EME name of the key system, such as org.w3.clearkey |
| 251 | std::string name_; |
| 252 | |
| 253 | // FIDL InterfacePtr to the platform provided KeySystem |
| 254 | fuchsia::media::drm::KeySystemPtr key_system_; |
| 255 | |
| 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 |
| 258 | // use. |
| 259 | base::flat_set<std::unique_ptr<FuchsiaCdmProvisioningFetcherImpl>, |
| 260 | base::UniquePtrComparator> |
| 261 | provisioning_fetchers_; |
| 262 | |
| 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; |
| 269 | |
| 270 | // A map of directory paths to data store ids that have been added to the |
| 271 | // KeySystem. |
| 272 | base::flat_map<base::FilePath, DataStoreId> data_store_ids_by_path_; |
| 273 | }; |
| 274 | |
| 275 | // static |
| 276 | FuchsiaCdmManager* FuchsiaCdmManager::GetInstance() { |
| 277 | return g_fuchsia_cdm_manager_instance; |
| 278 | } |
| 279 | |
| 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 |
| 293 | // start. |
| 294 | if (cdm_data_quota_bytes_) { |
| 295 | ApplyCdmStorageQuota(cdm_data_path_, *cdm_data_quota_bytes_); |
| 296 | } |
| 297 | |
| 298 | DCHECK(!g_fuchsia_cdm_manager_instance); |
| 299 | g_fuchsia_cdm_manager_instance = this; |
| 300 | } |
| 301 | |
| 302 | FuchsiaCdmManager::~FuchsiaCdmManager() { |
| 303 | DCHECK_EQ(g_fuchsia_cdm_manager_instance, this); |
| 304 | g_fuchsia_cdm_manager_instance = nullptr; |
| 305 | } |
| 306 | |
| 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> |
| 312 | request) { |
| 313 | DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| 314 | |
| 315 | base::FilePath storage_path = GetStoragePath(key_system, origin); |
| 316 | |
| 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))); |
| 323 | } |
| 324 | |
| 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); |
| 328 | } |
| 329 | |
| 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); |
| 337 | } |
| 338 | return client_it->second.get(); |
| 339 | } |
| 340 | |
| 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; |
| 347 | return nullptr; |
| 348 | } |
| 349 | |
| 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"; |
| 357 | return nullptr; |
| 358 | } |
| 359 | |
| 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; |
| 364 | } |
| 365 | |
| 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)); |
| 370 | } |
| 371 | |
| 372 | void FuchsiaCdmManager::CreateCdm( |
| 373 | const std::string& key_system_name, |
| 374 | CreateFetcherCB create_fetcher_cb, |
| 375 | fidl::InterfaceRequest<fuchsia::media::drm::ContentDecryptionModule> |
| 376 | request, |
| 377 | base::FilePath storage_path, |
| 378 | absl::optional<base::File::Error> storage_creation_error) { |
| 379 | DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| 380 | |
| 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); |
| 385 | return; |
| 386 | } |
| 387 | |
| 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); |
| 393 | return; |
| 394 | } |
| 395 | |
| 396 | key_system_client->CreateCdm(std::move(storage_path), |
| 397 | std::move(create_fetcher_cb), |
| 398 | std::move(request)); |
| 399 | } |
| 400 | |
| 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); |
| 405 | } |
| 406 | |
| 407 | active_key_system_clients_by_name_.erase(key_system_name); |
| 408 | } |
| 409 | |
| 410 | } // namespace media |