| // Copyright 2016 The Chromium Embedded Framework Authors. Portions copyright |
| // 2013 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 "libcef/common/widevine_loader.h" |
| |
| #if BUILDFLAG(ENABLE_WIDEVINE) && BUILDFLAG(ENABLE_LIBRARY_CDMS) |
| |
| #include "libcef/browser/context.h" |
| #include "libcef/browser/thread_util.h" |
| #include "libcef/common/cef_switches.h" |
| |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/native_library.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "content/browser/plugin_service_impl.h" |
| #include "content/public/browser/cdm_registry.h" |
| #include "content/public/common/cdm_info.h" |
| #include "content/public/common/content_switches.h" |
| #include "media/cdm/cdm_host_file.h" |
| #include "media/cdm/supported_cdm_versions.h" |
| #include "services/service_manager/embedder/switches.h" |
| #include "services/service_manager/sandbox/switches.h" |
| #include "third_party/widevine/cdm/widevine_cdm_common.h" // nogncheck |
| |
| namespace { |
| |
| base::LazyInstance<CefWidevineLoader>::Leaky g_widevine_loader = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| // Based on chrome/browser/component_updater/widevine_cdm_component_installer.cc |
| |
| // Name of the Widevine CDM OS in the component manifest. |
| const char kWidevineCdmOs[] = |
| #if defined(OS_MACOSX) |
| "mac"; |
| #elif defined(OS_WIN) |
| "win"; |
| #else // OS_LINUX, etc. TODO(viettrungluu): Separate out Chrome OS and Android? |
| "linux"; |
| #endif |
| |
| // Name of the Widevine CDM architecture in the component manifest. |
| const char kWidevineCdmArch[] = |
| #if defined(ARCH_CPU_X86) |
| "ia32"; // This differs from the component updater which uses "x86". |
| #elif defined(ARCH_CPU_X86_64) |
| "x64"; |
| #else // TODO(viettrungluu): Support an ARM check? |
| "???"; |
| #endif |
| |
| // The CDM OS and architecture. |
| const char kCdmOsName[] = "os"; |
| const char kCdmArchName[] = "arch"; |
| |
| // The CDM version (e.g. "1.4.8.903"). |
| const char kCdmVersionName[] = "version"; |
| |
| // The CDM manifest includes several custom values, all beginning with "x-cdm-". |
| // All values are strings. |
| // All values that are lists are delimited by commas. No trailing commas. |
| // For example, "1,2,4". |
| const char kCdmValueDelimiter[] = ","; |
| // The following entries are required. |
| // Interface versions are lists of integers (e.g. "1" or "1,2,4"). |
| // These are checked in this file before registering the CDM. |
| // All match the interface versions from content_decryption_module.h that the |
| // CDM supports. |
| // Matches CDM_MODULE_VERSION. |
| const char kCdmModuleVersionsName[] = "x-cdm-module-versions"; |
| // Matches supported ContentDecryptionModule_* version(s). |
| const char kCdmInterfaceVersionsName[] = "x-cdm-interface-versions"; |
| // Matches supported Host_* version(s). |
| const char kCdmHostVersionsName[] = "x-cdm-host-versions"; |
| // The codecs list is a list of simple codec names (e.g. "vp8,vorbis"). |
| // The list is passed to other parts of Chrome. |
| const char kCdmCodecsListName[] = "x-cdm-codecs"; |
| // Whether persistent license is supported by the CDM: "true" or "false". |
| const char kCdmPersistentLicenseSupportName[] = |
| "x-cdm-persistent-license-support"; |
| const char kCdmSupportedEncryptionSchemesName[] = |
| "x-cdm-supported-encryption-schemes"; |
| |
| // The following strings are used to specify supported codecs in the |
| // parameter |kCdmCodecsListName|. |
| const char kCdmSupportedCodecVp8[] = "vp8"; |
| const char kCdmSupportedCodecVp9[] = "vp9.0"; |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| const char kCdmSupportedCodecAvc1[] = "avc1"; |
| #endif |
| |
| // The following strings are used to specify supported encryption schemes in |
| // the parameter |kCdmSupportedEncryptionSchemesName|. |
| const char kCdmSupportedEncryptionSchemeCenc[] = "cenc"; |
| const char kCdmSupportedEncryptionSchemeCbcs[] = "cbcs"; |
| |
| // Arguments passed to MakeCdmInfo. |
| struct CdmInfoArgs { |
| base::FilePath path; |
| std::string version; |
| content::CdmCapability capability; |
| }; |
| |
| std::unique_ptr<base::DictionaryValue> ParseManifestFile( |
| const base::FilePath& manifest_path) { |
| CEF_REQUIRE_BLOCKING(); |
| |
| // Manifest file should be < 1kb. Read at most 2kb. |
| std::string manifest_contents; |
| if (!base::ReadFileToStringWithMaxSize(manifest_path, &manifest_contents, |
| 2048)) { |
| return nullptr; |
| } |
| |
| JSONStringValueDeserializer deserializer(manifest_contents); |
| std::unique_ptr<base::Value> manifest( |
| deserializer.Deserialize(nullptr, nullptr)); |
| |
| if (!manifest.get() || !manifest->is_dict()) |
| return nullptr; |
| |
| // Transfer ownership to the caller. |
| return base::WrapUnique( |
| static_cast<base::DictionaryValue*>(manifest.release())); |
| } |
| |
| std::string GetManifestValue(const base::DictionaryValue& manifest, |
| const std::string& key, |
| std::string* error_message) { |
| std::stringstream ss; |
| std::string value; |
| if (!manifest.GetString(key, &value)) { |
| ss << "Manifest missing " << key; |
| *error_message = ss.str(); |
| } else if (value.empty()) { |
| ss << "Manifest has empty " << key; |
| *error_message = ss.str(); |
| } |
| return value; |
| } |
| |
| typedef bool (*VersionCheckFunc)(int version); |
| |
| bool CheckForCompatibleVersion(const base::DictionaryValue& manifest, |
| const std::string version_name, |
| VersionCheckFunc version_check_func, |
| std::string* error_message) { |
| std::string versions_string = |
| GetManifestValue(manifest, version_name, error_message); |
| if (versions_string.empty()) |
| return false; |
| |
| for (const base::StringPiece& ver_str : |
| base::SplitStringPiece(versions_string, kCdmValueDelimiter, |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) { |
| int version = 0; |
| if (base::StringToInt(ver_str, &version)) |
| if (version_check_func(version)) |
| return true; |
| } |
| |
| std::stringstream ss; |
| ss << "Manifest has no supported " << version_name << " in '" |
| << versions_string << "'"; |
| *error_message = ss.str(); |
| return false; |
| } |
| |
| // Returns whether the CDM's OS/platform and module/interface/host API versions, |
| // as specified in the manifest, are compatible with this Chromium binary. |
| bool IsCompatibleWithChrome(const base::DictionaryValue& manifest, |
| std::string* error_message) { |
| return GetManifestValue(manifest, kCdmOsName, error_message) == |
| kWidevineCdmOs && |
| GetManifestValue(manifest, kCdmArchName, error_message) == |
| kWidevineCdmArch && |
| CheckForCompatibleVersion(manifest, kCdmModuleVersionsName, |
| media::IsSupportedCdmModuleVersion, |
| error_message) && |
| CheckForCompatibleVersion(manifest, kCdmInterfaceVersionsName, |
| media::IsSupportedCdmInterfaceVersion, |
| error_message) && |
| CheckForCompatibleVersion(manifest, kCdmHostVersionsName, |
| media::IsSupportedCdmHostVersion, |
| error_message); |
| } |
| |
| // Returns true and updates |video_codecs| if the appropriate manifest entry is |
| // valid. Returns false and does not modify |video_codecs| if the manifest entry |
| // is incorrectly formatted. |
| bool GetCodecs(const base::DictionaryValue& manifest, |
| std::vector<media::VideoCodec>* video_codecs, |
| std::string* error_message) { |
| DCHECK(video_codecs); |
| |
| const base::Value* value = manifest.FindKey(kCdmCodecsListName); |
| if (!value) { |
| std::stringstream ss; |
| ss << "Widevine CDM component manifest is missing codecs."; |
| *error_message = ss.str(); |
| return true; |
| } |
| |
| if (!value->is_string()) { |
| std::stringstream ss; |
| ss << "Manifest entry " << kCdmCodecsListName << " is not a string."; |
| *error_message = ss.str(); |
| return false; |
| } |
| |
| const std::string& codecs = value->GetString(); |
| if (codecs.empty()) { |
| std::stringstream ss; |
| ss << "Widevine CDM component manifest has empty codecs list."; |
| *error_message = ss.str(); |
| return true; |
| } |
| |
| std::vector<media::VideoCodec> result; |
| const std::vector<base::StringPiece> supported_codecs = |
| base::SplitStringPiece(codecs, kCdmValueDelimiter, base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| |
| for (const auto& codec : supported_codecs) { |
| if (codec == kCdmSupportedCodecVp8) |
| result.push_back(media::VideoCodec::kCodecVP8); |
| else if (codec == kCdmSupportedCodecVp9) |
| result.push_back(media::VideoCodec::kCodecVP9); |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| else if (codec == kCdmSupportedCodecAvc1) |
| result.push_back(media::VideoCodec::kCodecH264); |
| #endif // BUILDFLAG(USE_PROPRIETARY_CODECS) |
| } |
| |
| video_codecs->swap(result); |
| return true; |
| } |
| |
| // Returns true and updates |encryption_schemes| if the appropriate manifest |
| // entry is valid. Returns false and does not modify |encryption_schemes| if the |
| // manifest entry is incorrectly formatted. It is assumed that all CDMs support |
| // 'cenc', so if the manifest entry is missing, the result will indicate support |
| // for 'cenc' only. Incorrect types in the manifest entry will log the error and |
| // fail. Unrecognized values will be reported but otherwise ignored. |
| bool GetEncryptionSchemes( |
| const base::DictionaryValue& manifest, |
| base::flat_set<media::EncryptionScheme>* encryption_schemes, |
| std::string* error_message) { |
| DCHECK(encryption_schemes); |
| |
| const base::Value* value = |
| manifest.FindKey(kCdmSupportedEncryptionSchemesName); |
| if (!value) { |
| // No manifest entry found, so assume only 'cenc' supported for backwards |
| // compatibility. |
| encryption_schemes->insert(media::EncryptionScheme::kCenc); |
| return true; |
| } |
| |
| if (!value->is_list()) { |
| std::stringstream ss; |
| ss << "Manifest entry " << kCdmSupportedEncryptionSchemesName |
| << " is not a list."; |
| *error_message = ss.str(); |
| return false; |
| } |
| |
| const base::span<const base::Value> list = value->GetList(); |
| base::flat_set<media::EncryptionScheme> result; |
| for (const auto& item : list) { |
| if (!item.is_string()) { |
| std::stringstream ss; |
| ss << "Unrecognized item type in manifest entry " |
| << kCdmSupportedEncryptionSchemesName; |
| *error_message = ss.str(); |
| return false; |
| } |
| |
| const std::string& scheme = item.GetString(); |
| if (scheme == kCdmSupportedEncryptionSchemeCenc) { |
| result.insert(media::EncryptionScheme::kCenc); |
| } else if (scheme == kCdmSupportedEncryptionSchemeCbcs) { |
| result.insert(media::EncryptionScheme::kCbcs); |
| } else { |
| std::stringstream ss; |
| ss << "Unrecognized encryption scheme " << scheme << " in manifest entry " |
| << kCdmSupportedEncryptionSchemesName; |
| *error_message = ss.str(); |
| } |
| } |
| |
| // As the manifest entry exists, it must specify at least one valid value. |
| if (result.empty()) |
| return false; |
| |
| encryption_schemes->swap(result); |
| return true; |
| } |
| |
| // Returns true and updates |session_types| if the appropriate manifest entry is |
| // valid. Returns false if the manifest entry is incorrectly formatted. |
| bool GetSessionTypes(const base::DictionaryValue& manifest, |
| base::flat_set<media::CdmSessionType>* session_types, |
| std::string* error_message) { |
| DCHECK(session_types); |
| |
| bool is_persistent_license_supported = false; |
| const base::Value* value = manifest.FindKey(kCdmPersistentLicenseSupportName); |
| if (value) { |
| if (!value->is_bool()) |
| return false; |
| is_persistent_license_supported = value->GetBool(); |
| } |
| |
| // Temporary session is always supported. |
| session_types->insert(media::CdmSessionType::kTemporary); |
| if (is_persistent_license_supported) |
| session_types->insert(media::CdmSessionType::kPersistentLicense); |
| |
| return true; |
| } |
| |
| // Verify and load the contents of |base_path|. |
| cef_cdm_registration_error_t LoadWidevineCdmInfo( |
| const base::FilePath& base_path, |
| CdmInfoArgs* args, |
| std::string* error_message) { |
| std::stringstream ss; |
| |
| args->path = base_path.AppendASCII( |
| base::GetNativeLibraryName(kWidevineCdmLibraryName)); |
| if (!base::PathExists(args->path)) { |
| ss << "Missing file " << args->path.value(); |
| *error_message = ss.str(); |
| return CEF_CDM_REGISTRATION_ERROR_INCORRECT_CONTENTS; |
| } |
| |
| base::FilePath manifest_path = base_path.AppendASCII("manifest.json"); |
| if (!base::PathExists(manifest_path)) { |
| ss << "Missing manifest file " << manifest_path.value(); |
| *error_message = ss.str(); |
| return CEF_CDM_REGISTRATION_ERROR_INCORRECT_CONTENTS; |
| } |
| |
| std::unique_ptr<base::DictionaryValue> manifest = |
| ParseManifestFile(manifest_path); |
| if (!manifest) { |
| ss << "Failed to parse manifest file " << manifest_path.value(); |
| *error_message = ss.str(); |
| return CEF_CDM_REGISTRATION_ERROR_INCORRECT_CONTENTS; |
| } |
| |
| if (!IsCompatibleWithChrome(*manifest, error_message)) |
| return CEF_CDM_REGISTRATION_ERROR_INCOMPATIBLE; |
| |
| args->version = GetManifestValue(*manifest, kCdmVersionName, error_message); |
| if (args->version.empty()) |
| return CEF_CDM_REGISTRATION_ERROR_INCORRECT_CONTENTS; |
| |
| if (!GetCodecs(*manifest, &args->capability.video_codecs, error_message) || |
| !GetEncryptionSchemes(*manifest, &args->capability.encryption_schemes, |
| error_message) || |
| !GetSessionTypes(*manifest, &args->capability.session_types, |
| error_message)) { |
| return CEF_CDM_REGISTRATION_ERROR_INCORRECT_CONTENTS; |
| } |
| |
| return CEF_CDM_REGISTRATION_ERROR_NONE; |
| } |
| |
| void DeliverWidevineCdmCallback(cef_cdm_registration_error_t result, |
| const std::string& error_message, |
| CefRefPtr<CefRegisterCdmCallback> callback) { |
| CEF_REQUIRE_UIT(); |
| |
| if (result != CEF_CDM_REGISTRATION_ERROR_NONE) |
| LOG(ERROR) << "Widevine CDM registration failed; " << error_message; |
| else if (!error_message.empty()) |
| LOG(WARNING) << "Widevine CDM registration warning; " << error_message; |
| |
| if (callback) |
| callback->OnCdmRegistrationComplete(result, error_message); |
| } |
| |
| content::CdmInfo MakeCdmInfo(const CdmInfoArgs& args) { |
| return content::CdmInfo(kWidevineCdmDisplayName, kWidevineCdmGuid, |
| base::Version(args.version), args.path, |
| kWidevineCdmFileSystemId, args.capability, |
| kWidevineKeySystem, false); |
| } |
| |
| void RegisterWidevineCdmOnUIThread(std::unique_ptr<CdmInfoArgs> args, |
| CefRefPtr<CefRegisterCdmCallback> callback) { |
| CEF_REQUIRE_UIT(); |
| |
| // Register Widevine with the CdmRegistry. |
| content::CdmRegistry::GetInstance()->RegisterCdm(MakeCdmInfo(*args)); |
| |
| DeliverWidevineCdmCallback(CEF_CDM_REGISTRATION_ERROR_NONE, std::string(), |
| callback); |
| } |
| |
| void LoadWidevineCdmInfoOnBlockingThread( |
| const base::FilePath& base_path, |
| CefRefPtr<CefRegisterCdmCallback> callback) { |
| CEF_REQUIRE_BLOCKING(); |
| |
| std::unique_ptr<CdmInfoArgs> args = std::make_unique<CdmInfoArgs>(); |
| std::string error_message; |
| cef_cdm_registration_error_t result = |
| LoadWidevineCdmInfo(base_path, args.get(), &error_message); |
| if (result != CEF_CDM_REGISTRATION_ERROR_NONE) { |
| CEF_POST_TASK(CEF_UIT, base::BindOnce(DeliverWidevineCdmCallback, result, |
| error_message, callback)); |
| return; |
| } |
| |
| // Continue execution on the UI thread. |
| CEF_POST_TASK(CEF_UIT, base::BindOnce(RegisterWidevineCdmOnUIThread, |
| std::move(args), callback)); |
| } |
| |
| } // namespace |
| |
| // static |
| CefWidevineLoader* CefWidevineLoader::GetInstance() { |
| return &g_widevine_loader.Get(); |
| } |
| |
| void CefWidevineLoader::LoadWidevineCdm( |
| const base::FilePath& path, |
| CefRefPtr<CefRegisterCdmCallback> callback) { |
| if (!CONTEXT_STATE_VALID()) { |
| // Loading will proceed from OnContextInitialized(). |
| load_pending_ = true; |
| path_ = path; |
| callback_ = callback; |
| return; |
| } |
| |
| CEF_POST_USER_VISIBLE_TASK( |
| base::BindOnce(LoadWidevineCdmInfoOnBlockingThread, path, callback)); |
| } |
| |
| void CefWidevineLoader::OnContextInitialized() { |
| CEF_REQUIRE_UIT(); |
| if (load_pending_) { |
| load_pending_ = false; |
| LoadWidevineCdm(path_, callback_); |
| callback_ = nullptr; |
| } |
| } |
| |
| #if defined(OS_LINUX) |
| |
| // static |
| void CefWidevineLoader::AddContentDecryptionModules( |
| std::vector<content::CdmInfo>* cdms, |
| std::vector<media::CdmHostFilePath>* cdm_host_file_paths) { |
| const base::CommandLine& command_line = |
| *base::CommandLine::ForCurrentProcess(); |
| |
| // Perform early plugin registration in the zygote process when the sandbox is |
| // enabled to avoid "cannot open shared object file: Operation not permitted" |
| // errors during plugin loading. This is because the Zygote process must pre- |
| // load all plugins before initializing the sandbox. |
| if (command_line.GetSwitchValueASCII(switches::kProcessType) != |
| service_manager::switches::kZygoteProcess || |
| command_line.HasSwitch(service_manager::switches::kNoSandbox)) { |
| return; |
| } |
| |
| // The Widevine CDM path is passed to the zygote process via |
| // CefContentBrowserClient::AppendExtraCommandLineSwitches. |
| const base::FilePath& base_path = |
| command_line.GetSwitchValuePath(switches::kWidevineCdmPath); |
| if (base_path.empty()) |
| return; |
| |
| // Load contents of the plugin directory synchronously. This only occurs once |
| // on zygote process startup so should not have a huge performance penalty. |
| CdmInfoArgs args; |
| std::string error_message; |
| cef_cdm_registration_error_t result = |
| LoadWidevineCdmInfo(base_path, &args, &error_message); |
| if (result != CEF_CDM_REGISTRATION_ERROR_NONE) { |
| LOG(ERROR) << "Widevine CDM registration failed; " << error_message; |
| return; |
| } |
| |
| cdms->push_back(MakeCdmInfo(args)); |
| } |
| |
| #endif // defined(OS_LINUX) |
| |
| CefWidevineLoader::CefWidevineLoader() {} |
| |
| CefWidevineLoader::~CefWidevineLoader() {} |
| |
| #endif // BUILDFLAG(ENABLE_WIDEVINE) && BUILDFLAG(ENABLE_LIBRARY_CDMS) |