diff --git a/inputstream.adaptive/resources/language/resource.language.en_gb/strings.po b/inputstream.adaptive/resources/language/resource.language.en_gb/strings.po index 1577ec4d7..c54bc4ea4 100644 --- a/inputstream.adaptive/resources/language/resource.language.en_gb/strings.po +++ b/inputstream.adaptive/resources/language/resource.language.en_gb/strings.po @@ -90,12 +90,12 @@ msgstr "" #empty string with id 30121 msgctxt "#30122" -msgid "Try avoiding the use of secure decoder" +msgid "Disable secure decoder" msgstr "" #. Description of setting with label #30122 msgctxt "#30123" -msgid "Some Android devices defined as Widevine L1, may not work properly, which may result in a black screen during playback. In this case try to enable it." +msgid "Some Android devices defined as Widevine L1, may not work properly, which may result in a black screen during playback. In this case try to enable it. This setting may be overridden by the video add-on used." msgstr "" #empty strings from id 30124 to 30155 diff --git a/lib/jni/jni/src/MediaDrm.cpp b/lib/jni/jni/src/MediaDrm.cpp index 03499e29e..aefcc169c 100644 --- a/lib/jni/jni/src/MediaDrm.cpp +++ b/lib/jni/jni/src/MediaDrm.cpp @@ -139,10 +139,10 @@ CJNIMediaDrmProvisionRequest CJNIMediaDrm::getProvisionRequest() const "getProvisionRequest", "()Landroid/media/MediaDrm$ProvisionRequest;"); } -void CJNIMediaDrm::provideProvisionResponse(const std::vector &response) const +void CJNIMediaDrm::provideProvisionResponse(const std::vector &response) const { call_method(m_object, - "provideProvisionResponse", "([B)V", jcast >(response)); + "provideProvisionResponse", "([B)V", jcast >(response)); } void CJNIMediaDrm::removeKeys(const std::vector &sessionId) const diff --git a/lib/jni/jni/src/MediaDrm.h b/lib/jni/jni/src/MediaDrm.h index 957742cde..ab6a645a6 100644 --- a/lib/jni/jni/src/MediaDrm.h +++ b/lib/jni/jni/src/MediaDrm.h @@ -52,7 +52,7 @@ class CJNIMediaDrm : public CJNIBase std::vector provideKeyResponse(const std::vector &scope, const std::vector &response) const; CJNIMediaDrmProvisionRequest getProvisionRequest() const; - void provideProvisionResponse(const std::vector &response) const; + void provideProvisionResponse(const std::vector &response) const; void removeKeys(const std::vector &sessionId) const; diff --git a/lib/jni/jni/src/MediaDrmKeyRequest.cpp b/lib/jni/jni/src/MediaDrmKeyRequest.cpp index 9e0eeac1a..6e3f00d57 100644 --- a/lib/jni/jni/src/MediaDrmKeyRequest.cpp +++ b/lib/jni/jni/src/MediaDrmKeyRequest.cpp @@ -18,7 +18,7 @@ CJNIMediaDrmKeyRequest::CJNIMediaDrmKeyRequest() m_object.setGlobal(); } -std::vector CJNIMediaDrmKeyRequest::getData() const +std::vector CJNIMediaDrmKeyRequest::getData() const { JNIEnv *env = xbmc_jnienv(); jhbyteArray array = call_method(m_object, @@ -26,7 +26,7 @@ std::vector CJNIMediaDrmKeyRequest::getData() const jsize size = env->GetArrayLength(array.get()); - std::vector result; + std::vector result; result.resize(size); env->GetByteArrayRegion(array.get(), 0, size, (jbyte*)result.data()); diff --git a/lib/jni/jni/src/MediaDrmKeyRequest.h b/lib/jni/jni/src/MediaDrmKeyRequest.h index 1bc614261..8cf18d919 100644 --- a/lib/jni/jni/src/MediaDrmKeyRequest.h +++ b/lib/jni/jni/src/MediaDrmKeyRequest.h @@ -19,7 +19,7 @@ class CJNIMediaDrmKeyRequest : public CJNIBase CJNIMediaDrmKeyRequest(); CJNIMediaDrmKeyRequest(const jni::jhobject &object) : CJNIBase(object) {}; - std::vector getData() const; + std::vector getData() const; int getRequestType() const; }; diff --git a/lib/jni/jni/src/MediaDrmProvisionRequest.cpp b/lib/jni/jni/src/MediaDrmProvisionRequest.cpp index 5b4ff3926..7eb69c5ae 100644 --- a/lib/jni/jni/src/MediaDrmProvisionRequest.cpp +++ b/lib/jni/jni/src/MediaDrmProvisionRequest.cpp @@ -18,7 +18,7 @@ CJNIMediaDrmProvisionRequest::CJNIMediaDrmProvisionRequest() m_object.setGlobal(); } -std::vector CJNIMediaDrmProvisionRequest::getData() const +std::vector CJNIMediaDrmProvisionRequest::getData() const { JNIEnv *env = xbmc_jnienv(); jhbyteArray array = call_method(m_object, @@ -26,7 +26,7 @@ std::vector CJNIMediaDrmProvisionRequest::getData() const jsize size = env->GetArrayLength(array.get()); - std::vector result; + std::vector result; result.resize(size); env->GetByteArrayRegion(array.get(), 0, size, (jbyte*)result.data()); diff --git a/lib/jni/jni/src/MediaDrmProvisionRequest.h b/lib/jni/jni/src/MediaDrmProvisionRequest.h index 57f50f53c..c8d61d803 100644 --- a/lib/jni/jni/src/MediaDrmProvisionRequest.h +++ b/lib/jni/jni/src/MediaDrmProvisionRequest.h @@ -19,7 +19,7 @@ class CJNIMediaDrmProvisionRequest : public CJNIBase CJNIMediaDrmProvisionRequest(); CJNIMediaDrmProvisionRequest(const jni::jhobject &object) : CJNIBase(object) {}; - std::vector getData() const; + std::vector getData() const; std::string getDefaultUrl() const; }; diff --git a/src/CompKodiProps.cpp b/src/CompKodiProps.cpp index 675aa8e80..3de4056bb 100644 --- a/src/CompKodiProps.cpp +++ b/src/CompKodiProps.cpp @@ -24,8 +24,8 @@ using namespace UTILS; namespace { // clang-format off -constexpr std::string_view PROP_LICENSE_TYPE = "inputstream.adaptive.license_type"; -constexpr std::string_view PROP_LICENSE_KEY = "inputstream.adaptive.license_key"; +constexpr std::string_view PROP_LICENSE_TYPE = "inputstream.adaptive.license_type"; //! @todo: to be deprecated +constexpr std::string_view PROP_LICENSE_KEY = "inputstream.adaptive.license_key"; //! @todo: to be deprecated // PROP_LICENSE_URL and PROP_LICENSE_URL_APPEND has been added as workaround for Kodi PVR API bug // where limit property values to max 1024 chars, if exceeds the string is truncated. // Since some services provide license urls that exceeds 1024 chars, @@ -35,9 +35,9 @@ constexpr std::string_view PROP_LICENSE_KEY = "inputstream.adaptive.license_key" // -> this problem has been fixed on Kodi 22 constexpr std::string_view PROP_LICENSE_URL = "inputstream.adaptive.license_url"; //! @todo: deprecated to be removed on Kodi 23 constexpr std::string_view PROP_LICENSE_URL_APPEND = "inputstream.adaptive.license_url_append"; //! @todo: deprecated to be removed on Kodi 23 -constexpr std::string_view PROP_LICENSE_DATA = "inputstream.adaptive.license_data"; -constexpr std::string_view PROP_LICENSE_FLAGS = "inputstream.adaptive.license_flags"; -constexpr std::string_view PROP_SERVER_CERT = "inputstream.adaptive.server_certificate"; +constexpr std::string_view PROP_LICENSE_DATA = "inputstream.adaptive.license_data"; //! @todo: to be deprecated +constexpr std::string_view PROP_LICENSE_FLAGS = "inputstream.adaptive.license_flags"; //! @todo: to be deprecated +constexpr std::string_view PROP_SERVER_CERT = "inputstream.adaptive.server_certificate"; //! @todo: to be deprecated constexpr std::string_view PROP_MANIFEST_PARAMS = "inputstream.adaptive.manifest_params"; constexpr std::string_view PROP_MANIFEST_HEADERS = "inputstream.adaptive.manifest_headers"; @@ -50,7 +50,7 @@ constexpr std::string_view PROP_STREAM_HEADERS = "inputstream.adaptive.stream_he constexpr std::string_view PROP_AUDIO_LANG_ORIG = "inputstream.adaptive.original_audio_language"; constexpr std::string_view PROP_PLAY_TIMESHIFT_BUFFER = "inputstream.adaptive.play_timeshift_buffer"; constexpr std::string_view PROP_LIVE_DELAY = "inputstream.adaptive.live_delay"; //! @todo: deprecated to be removed on Kodi 23 -constexpr std::string_view PROP_PRE_INIT_DATA = "inputstream.adaptive.pre_init_data"; +constexpr std::string_view PROP_PRE_INIT_DATA = "inputstream.adaptive.pre_init_data"; //! @todo: to be deprecated constexpr std::string_view PROP_CONFIG = "inputstream.adaptive.config"; constexpr std::string_view PROP_DRM = "inputstream.adaptive.drm"; @@ -62,6 +62,32 @@ constexpr std::string_view PROP_CHOOSER_BANDWIDTH_MAX = "inputstream.adaptive.ch constexpr std::string_view PROP_CHOOSER_RES_MAX = "inputstream.adaptive.chooser_resolution_max"; constexpr std::string_view PROP_CHOOSER_RES_SECURE_MAX = "inputstream.adaptive.chooser_resolution_secure_max"; // clang-format on + + +void LogProp(std::string_view name, std::string_view value, bool isValueRedacted = false) +{ + LOG::Log(LOGDEBUG, "Property found \"%s\" value: %s", name.data(), + isValueRedacted ? "[redacted]" : value.data()); +} + +void LogDrmJsonDictKeys(std::string_view keyName, + const rapidjson::Value& dictValue, + std::string_view keySystem) +{ + if (dictValue.IsObject()) + { + std::string keys; + for (auto it = dictValue.MemberBegin(); it != dictValue.MemberEnd(); ++it) + { + if (!keys.empty()) + keys += ", "; + keys += it->name.GetString(); + } + LOG::Log(LOGDEBUG, + "Found DRM config for key system: \"%s\" -> Dictionary: \"%s\", Values: \"%s\"", + keySystem.data(), keyName.data(), keys.c_str()); + } +} } // unnamed namespace ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map& props) @@ -72,34 +98,33 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map>>>>>>>>\n" + "A mixed use of DRM properties are not supported.\n" + "Please fix your configuration by using only one of these:\n" + " - Simple method: \"inputstream.adaptive.drm_legacy\"\n" + " - Advanced method (deprecated): \"inputstream.adaptive.license_type\" with optional " + "\"inputstream.adaptive.license_key\"\n" + " - NEW Advanced method: \"inputstream.adaptive.drm\"\n" + "FOR MORE INFO, PLEASE READ THE WIKI PAGE: " + "https://github.com/xbmc/inputstream.adaptive/wiki/Integration-DRM"); return; } + // If a new DRM property is used, ignore old properties + if (!STRING::KeyExists(props, PROP_DRM) && !STRING::KeyExists(props, PROP_DRM_LEGACY)) + { + //! @todo: deprecated DRM properties, all them should be removed + ParseDrmOldProps(props); + } + for (const auto& prop : props) { bool logPropValRedacted{false}; - if (prop.first == PROP_LICENSE_TYPE) - { - if (DRM::IsKeySystemSupported(prop.second)) - m_licenseType = prop.second; - else - LOG::LogF(LOGERROR, "License type \"%s\" is not supported", prop.second.c_str()); - } - else if (prop.first == PROP_LICENSE_KEY) - { - m_licenseKey = prop.second; - logPropValRedacted = true; - } - else if (prop.first == PROP_LICENSE_URL) + if (prop.first == PROP_LICENSE_URL) //! @todo: deprecated to be removed on Kodi 23 { + LogProp(prop.first, prop.second, true); LOG::Log( LOGWARNING, "Warning \"inputstream.adaptive.license_url\" property for PVR API bug is deprecated and " @@ -107,37 +132,20 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map(std::stoi(prop.second)); } else if (prop.first == PROP_CHOOSER_RES_MAX) { + LogProp(prop.first, prop.second); std::pair res; if (STRING::GetMapValue(ADP::SETTINGS::RES_CONV_LIST, prop.second, res)) m_chooserProps.m_resolutionMax = res; @@ -201,6 +214,7 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map res; if (STRING::GetMapValue(ADP::SETTINGS::RES_CONV_LIST, prop.second, res)) m_chooserProps.m_resolutionSecureMax = res; @@ -209,55 +223,57 @@ ADP::KODI_PROPS::CCompKodiProps::CCompKodiProps(const std::map 1) + { + LOG::Log(LOGERROR, "The \"inputstream.adaptive.license_url\" and " + "\"inputstream.adaptive.license_url_append\" properties\n" + "cannot be used with multiple DRM configurations,\n" + "Please set a single DRM configuration."); + } else - m_licenseKey.replace(0, pipePos, licenseUrl); - } - - if (m_licenseType == DRM::KS_CLEARKEY && !m_licenseKey.empty()) - { - LOG::Log( - LOGERROR, - "The \"inputstream.adaptive.license_key\" property cannot be used to configure ClearKey DRM,\n" - "use \"inputstream.adaptive.drm_legacy\" instead.\nSee Wiki integration page for more details."); - m_licenseKey.clear(); + { + auto first = m_drmConfigs.begin(); + first->second.license.serverUrl = licenseUrl; + } } } @@ -348,14 +364,215 @@ void ADP::KODI_PROPS::CCompKodiProps::ParseManifestConfig(const std::string& dat } } -//! @todo: inputstream.adaptive.drm in future will be used to set configs of all DRM's -//! and so will replace old props such as: license_type, license_key, license_data, etc... +void ADP::KODI_PROPS::CCompKodiProps::ParseDrmOldProps( + const std::map& props) +{ + // Translate data from old ISA properties to the new DRM config + + if (!STRING::KeyExists(props, PROP_LICENSE_TYPE) && !STRING::KeyExists(props, PROP_LICENSE_KEY)) + return; + /* + *! @todo: TO UNCOMMENT WHEN DRM AUTO-SELECTION WILL BE FULL IMPLEMENTED + * + LOG::Log(LOGWARNING, "<<<<<<<<< DEPRECATION NOTICE >>>>>>>>>\n" + "DEPRECATED PROPERTIES HAS BEEN USED TO SET THE DRM CONFIGURATION.\n" + "THE FOLLOWING PROPERTIES WILL BE REMOVED FROM FUTURE KODI VERSIONS:\n" + "- inputstream.adaptive.license_type\n" + "- inputstream.adaptive.license_key\n" + "- inputstream.adaptive.license_data\n" + "- inputstream.adaptive.license_flags\n" + "- inputstream.adaptive.server_certificate\n" + "- inputstream.adaptive.pre_init_data\n" + "YOU SHOULD CONSIDER MIGRATING TO THE NEW PROPERTIES:\n" + "- inputstream.adaptive.drm\n" + "- inputstream.adaptive.drm_legacy\n" + "FOR MORE INFO, PLEASE READ THE WIKI PAGE: " + "https://github.com/xbmc/inputstream.adaptive/wiki/Integration-DRM"); + */ + std::string drmKeySystem{DRM::KS_NONE}; + if (STRING::KeyExists(props, PROP_LICENSE_TYPE)) + drmKeySystem = props.at(PROP_LICENSE_TYPE.data()); + + LogProp(PROP_LICENSE_TYPE, drmKeySystem); + + if (!DRM::IsValidKeySystem(drmKeySystem)) + { + LOG::LogF(LOGERROR, + "Cannot parse DRM configuration, unknown key system \"%s\" on license_type property", + drmKeySystem.c_str()); + return; + } + + if (drmKeySystem == DRM::KS_CLEARKEY && STRING::KeyExists(props, PROP_LICENSE_KEY)) + { + LOG::Log(LOGERROR, "The \"inputstream.adaptive.license_key\" property cannot be used to " + "configure ClearKey DRM,\n" + "use \"inputstream.adaptive.drm_legacy\" or \"inputstream.adaptive.drm\" " + "instead.\nSee Wiki integration page for more details."); + return; + } + + // Create a DRM configuration for the specified key system + DrmCfg& drmCfg = m_drmConfigs[drmKeySystem]; + drmCfg.isNewConfig = false; + + // As legacy behaviour its expected to force the unique drm configuration available + drmCfg.priority = 1; + + // Parse DRM properties + std::string propValue; + + if (STRING::GetMapValue(props, PROP_LICENSE_FLAGS, propValue)) + { + LogProp(PROP_LICENSE_FLAGS, propValue); + + if (propValue.find("persistent_storage") != std::string::npos) + drmCfg.isPersistentStorage = true; + if (propValue.find("force_secure_decoder") != std::string::npos) + drmCfg.isSecureDecoderEnabled = false; + } + + if (STRING::GetMapValue(props, PROP_LICENSE_DATA, propValue)) + { + LogProp(PROP_LICENSE_DATA, propValue, true); + drmCfg.initData = propValue; + } + + if (STRING::GetMapValue(props, PROP_PRE_INIT_DATA, propValue)) + { + LogProp(PROP_PRE_INIT_DATA, propValue, true); + drmCfg.preInitData = propValue; + } + + // Parse DRM license properties + + if (STRING::GetMapValue(props, PROP_SERVER_CERT, propValue)) + { + LogProp(PROP_SERVER_CERT, propValue, true); + drmCfg.license.serverCert = propValue; + } + + if (STRING::GetMapValue(props, PROP_LICENSE_KEY, propValue)) + { + LogProp(PROP_LICENSE_KEY, propValue, true); + + std::vector fields = STRING::SplitToVec(propValue, '|'); + size_t fieldCount = fields.size(); + + if (drmKeySystem == DRM::KS_NONE) + { + // We assume its HLS AES-128 encrypted case + // where "inputstream.adaptive.license_key" have different fields + + // Field 1: HTTP request params to append to key URL + if (fieldCount >= 1) + drmCfg.license.reqParams = fields[0]; + + // Field 2: HTTP request headers + if (fieldCount >= 2) + ParseHeaderString(drmCfg.license.reqHeaders, fields[1]); + } + else + { + // Field 1: License server url + if (fieldCount >= 1) + drmCfg.license.serverUrl = fields[0]; + + // Field 2: HTTP request headers + if (fieldCount >= 2) + ParseHeaderString(drmCfg.license.reqHeaders, fields[1]); + + // Field 3: HTTP request data (POST request) + if (fieldCount >= 3) + drmCfg.license.reqData = fields[2]; + + // Field 4: HTTP response data (license wrappers) + if (fieldCount >= 4) + { + bool isJsonWrapper{false}; + std::string jsonWrapperCfg; + std::string_view wrapperPrefix = fields[3]; + + if (wrapperPrefix.empty() || wrapperPrefix == "R") + { + // Raw, no wrapper, no-op + } + else if (wrapperPrefix == "B") + { + drmCfg.license.unwrapper = "base64"; + } + else if (STRING::StartsWith(wrapperPrefix, "BJ")) + { + isJsonWrapper = true; + drmCfg.license.unwrapper = "base64+json"; + jsonWrapperCfg = wrapperPrefix.substr(2); + } + else if (STRING::StartsWith(wrapperPrefix, "JB")) + { + isJsonWrapper = true; + drmCfg.license.unwrapper = "json+base64"; + jsonWrapperCfg = wrapperPrefix.substr(2); + } + else if (STRING::StartsWith(wrapperPrefix, "J")) + { + isJsonWrapper = true; + drmCfg.license.unwrapper = "json"; + jsonWrapperCfg = wrapperPrefix.substr(1); + } + else if (STRING::StartsWith(wrapperPrefix, "HB")) + { + // HB has been removed, we have no info about this use case + // if someone will open an issue we can try get more info for the reimplementation + //! @todo: if no feedbacks in future this can be removed, see also todo on decrypters/Helpers.cpp + LOG::Log(LOGERROR, "The support for \"HB\" parameter in the \"Response data\" field of " + "license_key property has been removed. If this is a requirement for " + "your video service, let us know by opening an issue on GitHub."); + } + else + { + LOG::Log( + LOGERROR, + "Unknown \"%s\" parameter in the \"response data\" field of license_key property", + wrapperPrefix.data()); + } + + // Parse JSON configuration + if (isJsonWrapper) + { + if (jsonWrapperCfg.empty()) + { + LOG::Log(LOGERROR, "Missing JSON dict key names in the \"response data\" field of " + "license_key property"); + } + else + { + // Expected format as "KeyNameForData;KeyNameForHDCP" with exact order + std::vector jPaths = STRING::SplitToVec(jsonWrapperCfg, ';'); + // Position 1: The dict Key name to get license data + if (jPaths.size() >= 1) + { + drmCfg.license.unwrapperParams["path_data_traverse"] = "true"; + drmCfg.license.unwrapperParams["path_data"] = jPaths[0]; + } + + // Position 2: The dict Key name to get HDCP value (optional) + if (jPaths.size() >= 2) + { + drmCfg.license.unwrapperParams["path_hdcp_traverse"] = "true"; + drmCfg.license.unwrapperParams["path_hdcp"] = jPaths[1]; + } + } + } + } + } + } +} + bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data) { /* Expected JSON structure: * { "keysystem_name" : { "persistent_storage" : bool, - * "force_secure_decoder" : bool, - * "streams_pssh_data" : str, + * "init_data" : str, * "pre_init_data" : str, * "priority": int, * "license": dict, @@ -376,15 +593,12 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data) { const char* keySystem = jChildObj.name.GetString(); - if (!DRM::IsKeySystemSupported(keySystem)) + if (!DRM::IsValidKeySystem(keySystem)) { LOG::LogF(LOGERROR, "Ignored unknown key system \"%s\" on DRM property", keySystem); continue; } - //! @todo: m_licenseType temporarily assigned, to remove with the DRM config rework - m_licenseType = keySystem; - DrmCfg& drmCfg = m_drmConfigs[keySystem]; auto& jDictVal = jChildObj.value; @@ -395,16 +609,84 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data) continue; } + // Parse main DRM config + + LogDrmJsonDictKeys("main", jDictVal, keySystem); + + if (jDictVal.HasMember("persistent_storage") && jDictVal["persistent_storage"].IsBool()) + drmCfg.isPersistentStorage = jDictVal["persistent_storage"].GetBool(); + + if (jDictVal.HasMember("secure_decoder") && + jDictVal["secure_decoder"].IsBool()) + drmCfg.isSecureDecoderEnabled = jDictVal["secure_decoder"].GetBool(); + + if (jDictVal.HasMember("init_data") && jDictVal["init_data"].IsString()) + drmCfg.initData = jDictVal["init_data"].GetString(); + + if (jDictVal.HasMember("pre_init_data") && jDictVal["pre_init_data"].IsString()) + drmCfg.preInitData = jDictVal["pre_init_data"].GetString(); + + if (jDictVal.HasMember("optional_key_req_params") && + jDictVal["optional_key_req_params"].IsObject()) + { + for (auto& jPairOptKeyReqParam : + jDictVal["optional_key_req_params"].GetObject()) // Iterate JSON dict + { + if (jPairOptKeyReqParam.name.IsString() && jPairOptKeyReqParam.value.IsString()) + { + drmCfg.optKeyReqParams.emplace(jPairOptKeyReqParam.name.GetString(), + jPairOptKeyReqParam.value.GetString()); + } + } + } + + if (jDictVal.HasMember("priority") && jDictVal["priority"].IsUint()) + drmCfg.priority = jDictVal["priority"].GetUint(); + + // Parse license DRM config + if (jDictVal.HasMember("license") && jDictVal["license"].IsObject()) { auto& jDictLic = jDictVal["license"]; + LogDrmJsonDictKeys("license", jDictLic, keySystem); + + if (jDictLic.HasMember("server_certificate") && jDictLic["server_certificate"].IsString()) + drmCfg.license.serverCert = jDictLic["server_certificate"].GetString(); + if (jDictLic.HasMember("server_url") && jDictLic["server_url"].IsString()) drmCfg.license.serverUrl = jDictLic["server_url"].GetString(); + if (jDictLic.HasMember("use_http_get_request") && jDictLic["use_http_get_request"].IsBool()) + drmCfg.license.isHttpGetRequest = jDictLic["use_http_get_request"].GetBool(); + if (jDictLic.HasMember("req_headers") && jDictLic["req_headers"].IsString()) ParseHeaderString(drmCfg.license.reqHeaders, jDictLic["req_headers"].GetString()); + if (jDictLic.HasMember("req_params") && jDictLic["req_params"].IsString()) + drmCfg.license.reqParams = jDictLic["req_params"].GetString(); + + if (jDictLic.HasMember("req_data") && jDictLic["req_data"].IsString()) + drmCfg.license.reqData = jDictLic["req_data"].GetString(); + + if (jDictLic.HasMember("wrapper") && jDictLic["wrapper"].IsString()) + drmCfg.license.wrapper = STRING::ToLower(jDictLic["wrapper"].GetString()); + + if (jDictLic.HasMember("unwrapper") && jDictLic["unwrapper"].IsString()) + drmCfg.license.unwrapper = STRING::ToLower(jDictLic["unwrapper"].GetString()); + + if (jDictLic.HasMember("unwrapper_params") && jDictLic["unwrapper_params"].IsObject()) + { + for (auto& jPairUnwrap : jDictLic["unwrapper_params"].GetObject()) // Iterate JSON dict + { + if (jPairUnwrap.name.IsString() && jPairUnwrap.value.IsString()) + { + drmCfg.license.unwrapperParams.emplace(jPairUnwrap.name.GetString(), + jPairUnwrap.value.GetString()); + } + } + } + if (jDictLic.HasMember("keyids") && jDictLic["keyids"].IsObject()) { for (auto const& keyid : jDictLic["keyids"].GetObject()) @@ -415,7 +697,7 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmConfig(const std::string& data) } } - //! @todo: temporary support only one DRM config + //! @todo: temporary support only one DRM config, must be reworked the CSession for DRM auto-selection break; } @@ -450,25 +732,25 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmLegacyConfig(const std::string& da if (pipedCfg.size() > 2) licenseHeaders = STRING::Trim(pipedCfg[2]); - if (!DRM::IsKeySystemSupported(keySystem)) + if (!DRM::IsValidKeySystem(keySystem)) { LOG::LogF(LOGERROR, "Unknown key system \"%s\" on DRM legacy property", keySystem.data()); return false; } - m_licenseType = keySystem; - std::string licenseUrl; + DrmCfg drmCfg; + // As legacy behaviour its expected to force the unique drm configuration available + drmCfg.priority = 1; if (!licenseStr.empty()) { if (URL::IsValidUrl(licenseStr)) // License server URL { - licenseUrl = licenseStr; + drmCfg.license.serverUrl = licenseStr; } else // Assume are keyid's for ClearKey DRM { // Expected TEXT structure: "kid1:key1,kid2:key2,..." - DrmCfg& drmCfg = m_drmConfigs[keySystem]; std::vector keyIdPair = STRING::SplitToVec(licenseStr, ','); for (const std::string& keyPairStr : keyIdPair) @@ -484,31 +766,9 @@ bool ADP::KODI_PROPS::CCompKodiProps::ParseDrmLegacyConfig(const std::string& da } } - if (keySystem == DRM::KS_CLEARKEY) - { - DrmCfg& drmCfg = m_drmConfigs[keySystem]; + ParseHeaderString(drmCfg.license.reqHeaders, licenseHeaders); - drmCfg.license.serverUrl = licenseUrl; - ParseHeaderString(drmCfg.license.reqHeaders, licenseHeaders); - // Until the future DRM config rework only the ClearKey DRM use the new properties - // so return now to keep m_licenseKey empty - return true; - } - else if (licenseHeaders.empty()) - { - //! @todo: temporary stored default DRM values here just for convenience - //! since we need to construct the "license key" string - //! these values are stored also on DRM's implementation, - //! they must be placed in an appropriate place with the future DRM config rework - if (keySystem == DRM::KS_WIDEVINE) - licenseHeaders = "Content-Type=application%2Foctet-stream"; - else if (keySystem == DRM::KS_PLAYREADY) - licenseHeaders = "Content-Type=text%2Fxml&SOAPAction=http%3A%2F%2Fschemas.microsoft.com%" - "2FDRM%2F2007%2F03%2Fprotocols%2FAcquireLicense"; - else if (keySystem == DRM::KS_WISEPLAY) - licenseHeaders = "Content-Type=application/json"; - } + m_drmConfigs[keySystem] = drmCfg; - m_licenseKey = licenseUrl + "|" + licenseHeaders + "|R{SSM}|R"; return true; } diff --git a/src/CompKodiProps.h b/src/CompKodiProps.h index d5c8af5ca..0aa43cf08 100644 --- a/src/CompKodiProps.h +++ b/src/CompKodiProps.h @@ -64,15 +64,54 @@ struct ManifestConfig struct DrmCfg { + // Priority over other DRM configurations, a value of 0 means unset + std::optional priority; + // Custom init data encoded as base64 to make the CDM initialization + std::string initData; + // Pre-init data encoded as base64 to make pre-initialization of Widevine CDM + // The data is represented as a string of base64 data splitted by a pipe char + // "{PSSH as base64}|{KID as base64}" + std::string preInitData; + // To enable persistent state CDM behaviour + bool isPersistentStorage{false}; + // To force enable/disable the secure decoder (it could be overrided) + std::optional isSecureDecoderEnabled; + // Optional parameters to make the CDM key request (CDM specific parameters) + std::map optKeyReqParams; + struct License { + // The license server certificate encoded as base64 + std::string serverCert; + // The license server url std::string serverUrl; + // To force an HTTP GET request, instead that POST request + bool isHttpGetRequest{false}; + // HTTP request headers std::map reqHeaders; - - std::map keys; // Clearkeys kid / key + // HTTP parameters to append to the url + std::string reqParams; + // Custom license data encoded as base64 to make the HTTP license request + std::string reqData; + // License data wrappers + // Multiple wrappers supported e.g. "base64,json", the name order defines the order + // in which data will be wrapped, (1) base64 --> (2) url + std::string wrapper; + // License data unwrappers + // Multiple un-wrappers supported e.g. "base64,json", the name order defines the order + // in which data will be unwrapped, (1) base64 --> (2) json + std::string unwrapper; + // License data unwrappers parameters + std::map unwrapperParams; + // Clear key's for ClearKey DRM (KID / KEY pair) + std::map keys; }; - License license; // The license configuration + // The license configuration + License license; + // Specifies if has been parsed the new DRM config ("drm" or "drm_legacy" kodi property) + //! @todo: to remove when deprecated DRM properties will be removed + bool isNewConfig{true}; }; class ATTR_DLL_LOCAL CCompKodiProps @@ -81,16 +120,6 @@ class ATTR_DLL_LOCAL CCompKodiProps CCompKodiProps(const std::map& props); ~CCompKodiProps() = default; - std::string_view GetLicenseType() const { return m_licenseType; } - std::string_view GetLicenseKey() const { return m_licenseKey; } - // \brief Get custom PSSH initialization license data - std::string_view GetLicenseData() const { return m_licenseData; } - - bool IsLicensePersistentStorage() const { return m_isLicensePersistentStorage; } - bool IsLicenseForceSecDecoder() const { return m_isLicenseForceSecureDecoder; } - - std::string_view GetServerCertificate() const { return m_serverCertificate; } - // \brief HTTP parameters used to download manifest updates std::string GetManifestUpdParams() const { return m_manifestUpdParams; } // \brief HTTP parameters used to download manifests @@ -109,12 +138,6 @@ class ATTR_DLL_LOCAL CCompKodiProps // \brief Specify to start playing a LIVE stream from the beginning of the buffer instead of its end bool IsPlayTimeshift() const { return m_playTimeshiftBuffer; } - /* - * \brief Get data to "pre-initialize" the DRM, if set is represented as a string - * of base64 data splitted by a pipe char as: "{PSSH as base64}|{KID as base64}". - */ - std::string_view GetDrmPreInitData() const { return m_drmPreInitData; } - // \brief Specifies the chooser properties that will override XML settings const ChooserProps& GetChooserProps() const { return m_chooserProps; } @@ -124,8 +147,19 @@ class ATTR_DLL_LOCAL CCompKodiProps // \brief Specifies the manifest configuration const ManifestConfig& GetManifestConfig() const { return m_manifestConfig; } + + //! @todo: temporary method, for future rework + const std::string GetDrmKeySystem() + { + return m_drmConfigs.empty() ? "" : m_drmConfigs.begin()->first; + } + //! @todo: temporary method, for future rework + const DrmCfg& GetDrmConfig() { return m_drmConfigs[GetDrmKeySystem()]; } + + // \brief Get DRM configuration for specified keysystem, if not found will return default values const DrmCfg& GetDrmConfig(const std::string& keySystem) { return m_drmConfigs[keySystem]; } + const DrmCfg& GetDrmConfig(std::string_view keySystem) { return m_drmConfigs[std::string(keySystem)]; } const std::map& GetDrmConfigs() const { return m_drmConfigs; } @@ -133,15 +167,10 @@ class ATTR_DLL_LOCAL CCompKodiProps void ParseConfig(const std::string& data); void ParseManifestConfig(const std::string& data); + void ParseDrmOldProps(const std::map& props); bool ParseDrmConfig(const std::string& data); bool ParseDrmLegacyConfig(const std::string& data); - std::string m_licenseType; - std::string m_licenseKey; - std::string m_licenseData; - bool m_isLicensePersistentStorage{false}; - bool m_isLicenseForceSecureDecoder{false}; - std::string m_serverCertificate; std::string m_manifestUpdParams; std::string m_manifestParams; std::map m_manifestHeaders; @@ -149,7 +178,6 @@ class ATTR_DLL_LOCAL CCompKodiProps std::map m_streamHeaders; std::string m_audioLanguageOrig; bool m_playTimeshiftBuffer{false}; - std::string m_drmPreInitData; ChooserProps m_chooserProps; Config m_config; ManifestConfig m_manifestConfig; diff --git a/src/Iaes_decrypter.h b/src/Iaes_decrypter.h index c1bd63452..8f2c2e1e6 100644 --- a/src/Iaes_decrypter.h +++ b/src/Iaes_decrypter.h @@ -28,6 +28,6 @@ class IAESDecrypter bool lastChunk) = 0; virtual std::string convertIV(const std::string& input) = 0; virtual void ivFromSequence(uint8_t* buffer, uint64_t sid) = 0; - virtual const std::string& getLicenseKey() const = 0; - virtual bool RenewLicense(const std::string& pluginUrl) = 0; + // virtual const std::string& getLicenseKey() const = 0; + // virtual bool RenewLicense(const std::string& pluginUrl) = 0; }; diff --git a/src/Session.cpp b/src/Session.cpp index 7166fb42d..a15c8aa6e 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -53,13 +53,6 @@ CSession::CSession(const std::string& manifestUrl) : m_manifestUrl(manifestUrl) default: m_mediaTypeMask = static_cast(~0); } - - std::string_view serverCertificate = CSrvBroker::GetKodiProps().GetServerCertificate(); - - if (!serverCertificate.empty()) - { - m_serverCertificate = BASE64::Decode(serverCertificate); - } } CSession::~CSession() @@ -94,7 +87,8 @@ void CSession::SetSupportedDecrypterURN(std::vector& keySystem return; } - m_decrypter = DRM::FACTORY::GetDecrypter(GetCryptoKeySystem()); + const std::string keySystem = CSrvBroker::GetKodiProps().GetDrmKeySystem(); + m_decrypter = DRM::FACTORY::GetDecrypter(GetCryptoKeySystem(keySystem)); if (!m_decrypter) return; @@ -104,7 +98,7 @@ void CSession::SetSupportedDecrypterURN(std::vector& keySystem return; } - keySystems = m_decrypter->SelectKeySystems(CSrvBroker::GetKodiProps().GetLicenseType()); + keySystems = m_decrypter->SelectKeySystems(keySystem); m_decrypter->SetLibraryPath(decrypterPath); } @@ -132,14 +126,11 @@ void CSession::DisposeDecrypter() bool CSession::Initialize() { - const auto& kodiProps = CSrvBroker::GetKodiProps(); - // Set the DRM configuration flags - if (kodiProps.IsLicensePersistentStorage()) - m_drmConfig |= DRM::IDecrypter::CONFIG_PERSISTENTSTORAGE; + auto& kodiProps = CSrvBroker::GetKodiProps(); // Get URN's wich are supported by this addon std::vector supportedKeySystems; - if (!kodiProps.GetLicenseType().empty()) + if (!kodiProps.GetDrmKeySystem().empty()) { SetSupportedDecrypterURN(supportedKeySystems); for (std::string_view keySystem : supportedKeySystems) @@ -152,7 +143,7 @@ bool CSession::Initialize() bool isSessionOpened{false}; // Preinitialize the DRM, if pre-initialisation data are provided - if (!kodiProps.GetDrmPreInitData().empty()) + if (!kodiProps.GetDrmConfig().preInitData.empty()) { std::string challengeB64; std::string sessionId; @@ -255,15 +246,16 @@ bool CSession::PreInitializeDRM(std::string& challengeB64, std::string& sessionId, bool& isSessionOpened) { - std::string_view preInitData = CSrvBroker::GetKodiProps().GetDrmPreInitData(); - std::string_view psshData; - std::string_view kidData; + auto& drmPropCfg = CSrvBroker::GetKodiProps().GetDrmConfig(); + + std::string psshData; + std::string kidData; // Parse the PSSH/KID data - size_t posSplitter = preInitData.find("|"); + size_t posSplitter = drmPropCfg.preInitData.find("|"); if (posSplitter != std::string::npos) { - psshData = preInitData.substr(0, posSplitter); - kidData = preInitData.substr(posSplitter + 1); + psshData = drmPropCfg.preInitData.substr(0, posSplitter); + kidData = drmPropCfg.preInitData.substr(posSplitter + 1); } if (psshData.empty() || kidData.empty()) @@ -277,14 +269,6 @@ bool CSession::PreInitializeDRM(std::string& challengeB64, // Try to initialize an SingleSampleDecryptor LOG::LogF(LOGDEBUG, "Entering encryption section"); - std::string_view licenseKey = CSrvBroker::GetKodiProps().GetLicenseKey(); - - if (licenseKey.empty()) - { - LOG::LogF(LOGERROR, "Kodi property \"inputstream.adaptive.license_key\" value is not set"); - return false; - } - if (!m_decrypter) { LOG::LogF(LOGERROR, "No decrypter found for encrypted stream"); @@ -293,7 +277,8 @@ bool CSession::PreInitializeDRM(std::string& challengeB64, if (!m_decrypter->IsInitialised()) { - if (!m_decrypter->OpenDRMSystem(licenseKey, m_serverCertificate, m_drmConfig)) + DRM::Config drmCfg = DRM::CreateDRMConfig(DRM::KS_WIDEVINE, drmPropCfg); + if (!m_decrypter->OpenDRMSystem(drmCfg)) { LOG::LogF(LOGERROR, "OpenDRMSystem failed"); return false; @@ -314,7 +299,7 @@ bool CSession::PreInitializeDRM(std::string& challengeB64, LOG::LogF(LOGDEBUG, "Initializing session with KID: %s", hexKid.c_str()); if (m_decrypter && (session.m_cencSingleSampleDecrypter = - m_decrypter->CreateSingleSampleDecrypter(initData, "", decKid, "", true, + m_decrypter->CreateSingleSampleDecrypter(initData, decKid, "", true, CryptoMode::AES_CTR)) != nullptr) { session.m_cdmSessionStr = session.m_cencSingleSampleDecrypter->GetSessionId(); @@ -345,10 +330,13 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) // Try to initialize an SingleSampleDecryptor if (m_adaptiveTree->m_currentPeriod->GetEncryptionState() == EncryptionState::ENCRYPTED_DRM) { - std::string_view licenseKey = CSrvBroker::GetKodiProps().GetLicenseKey(); + const std::string keySystem = CSrvBroker::GetKodiProps().GetDrmKeySystem(); + auto& drmPropCfg = CSrvBroker::GetKodiProps().GetDrmConfig(); + + DRM::Config drmCfg = DRM::CreateDRMConfig(keySystem, drmPropCfg); - if (licenseKey.empty()) - licenseKey = m_adaptiveTree->GetLicenseUrl(); + if (drmCfg.license.serverUrl.empty()) + drmCfg.license.serverUrl = m_adaptiveTree->GetLicenseUrl(); LOG::Log(LOGDEBUG, "Entering encryption section"); @@ -360,15 +348,13 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) if (!m_decrypter->IsInitialised()) { - if (!m_decrypter->OpenDRMSystem(licenseKey, m_serverCertificate, m_drmConfig)) + if (!m_decrypter->OpenDRMSystem(drmCfg)) { LOG::Log(LOGERROR, "OpenDRMSystem failed"); return false; } } - std::string_view licenseType = CSrvBroker::GetKodiProps().GetLicenseType(); - // cdmSession 0 is reserved for unencrypted streams for (size_t ses{1}; ses < m_cdmSessions.size(); ++ses) { @@ -390,55 +376,32 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) std::vector initData = sessionPsshset.pssh_; std::string defaultKidStr = sessionPsshset.defaultKID_; - std::string drmOptionalKeyParam; - std::string_view licenseDataStr = CSrvBroker::GetKodiProps().GetLicenseData(); + std::vector customInitData = BASE64::Decode(drmPropCfg.initData); - if (m_adaptiveTree->GetTreeType() == adaptive::TreeType::SMOOTH_STREAMING) + if (m_adaptiveTree->GetTreeType() == adaptive::TreeType::SMOOTH_STREAMING && + keySystem == DRM::KS_WIDEVINE) { - if (licenseType == "com.widevine.alpha") + if (DRM::IsValidPsshHeader(customInitData)) { - // Create SmoothStreaming Widevine PSSH data - //! @todo: CreateISMlicense accept placeholders {KID} and {UUID} but its not wiki documented - //! we should continue allow create custom pssh with placeholders? - //! see also todo's below - std::vector licenseData = BASE64::Decode(licenseDataStr); - - if (DRM::IsValidPsshHeader(licenseData)) - { - initData = licenseData; - } - else - { - LOG::Log(LOGDEBUG, "License data: Create Widevine PSSH for SmoothStreaming %s", - licenseData.empty() ? "" : "(with custom data)"); - - initData = - DRM::PSSH::MakeWidevine({DRM::ConvertKidStrToBytes(defaultKidStr)}, licenseData); - } + initData = customInitData; } - else if (licenseType == "com.microsoft.playready") + else { - // Use licenseData property to set data for the "PRCustomData" PlayReady DRM parameter - //! @todo: we are allowing to send custom data to the DRM for the license request - //! but drmOptionalKeyParam is used only for the specific "PRCustomData" DRM parameter - //! and limited to android only, ISM manifest only, also not wiki documented. - - //! @todo: The current "inputstream.adaptive.license_data" ISA property its a lot confusing, too much - //! multi purpose with specific and/or limited behaviours, this property need to be reworked/changed. - //! As first decoupling things and allowing to have a way to set DRM optional parameters in a extensible way - //! for future other use cases, and not limited to Playready only. - //! To take in account that license_data property is also used on DASH parser to bypass ContentProtection tags. - drmOptionalKeyParam = licenseDataStr; + LOG::Log(LOGDEBUG, "License data: Create Widevine PSSH for SmoothStreaming %s", + customInitData.empty() ? "" : "(with custom data)"); + + initData = + DRM::PSSH::MakeWidevine({DRM::ConvertKidStrToBytes(defaultKidStr)}, customInitData); } } - else if (!licenseDataStr.empty()) + else if (!customInitData.empty()) { // Custom license PSSH data provided from property // This can allow to initialize a DRM that could be also not specified // as supported in the manifest (e.g. missing DASH ContentProtection tags) LOG::Log(LOGDEBUG, "License data: Use PSSH data provided by the license data property"); - initData = BASE64::Decode(licenseDataStr); + initData = customInitData; } // If no KID, but init data, extract the KID from init data @@ -454,7 +417,7 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) //! @todo: as is implemented InitializeDRM will initialize all PSSHSet's also when are not used, //! therefore ExtractStreamProtectionData can perform many (not needed) downloads of mp4 init files - if ((initData.empty() && licenseType != DRM::KS_CLEARKEY) || defaultKidStr.empty()) + if ((initData.empty() && keySystem != DRM::KS_CLEARKEY) || defaultKidStr.empty()) { // Try extract the PSSH/KID from the stream ExtractStreamProtectionData(sessionPsshset, defaultKidStr, initData, @@ -504,7 +467,7 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) if (session.m_cencSingleSampleDecrypter || (session.m_cencSingleSampleDecrypter = m_decrypter->CreateSingleSampleDecrypter( - initData, drmOptionalKeyParam, defaultKid, sessionPsshset.m_licenseUrl, false, + initData, defaultKid, sessionPsshset.m_licenseUrl, false, sessionPsshset.m_cryptoMode == CryptoMode::NONE ? CryptoMode::AES_CTR : sessionPsshset.m_cryptoMode)) != nullptr) @@ -522,13 +485,17 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) { isSecureVideoSession = true; - bool isDisableSecureDecoder = CSrvBroker::GetSettings().IsDisableSecureDecoder(); - if (isDisableSecureDecoder) - LOG::Log(LOGDEBUG, "Secure video session, with setting configured to try disable secure decoder"); - - if (isDisableSecureDecoder && !CSrvBroker::GetKodiProps().IsLicenseForceSecDecoder() && - !m_adaptiveTree->m_currentPeriod->IsSecureDecodeNeeded()) + // Allow to disable the secure decoder + bool disableSecureDecoder = CSrvBroker::GetSettings().IsDisableSecureDecoder(); + // but, DRM config can override it + if (drmPropCfg.isSecureDecoderEnabled.has_value()) + disableSecureDecoder = !*drmPropCfg.isSecureDecoderEnabled; + // but, manifest config can override all others + if (m_adaptiveTree->m_currentPeriod->IsSecureDecodeNeeded().has_value()) + disableSecureDecoder = !*m_adaptiveTree->m_currentPeriod->IsSecureDecodeNeeded(); + if (disableSecureDecoder) { + LOG::Log(LOGDEBUG, "Initialize DRM: Configured with secure decoder disabled"); session.m_decrypterCaps.flags &= ~DRM::DecrypterCapabilites::SSD_SECURE_DECODER; } } @@ -1325,16 +1292,15 @@ uint32_t CSession::GetIncludedStreamMask() const return res; } -STREAM_CRYPTO_KEY_SYSTEM CSession::GetCryptoKeySystem() const +STREAM_CRYPTO_KEY_SYSTEM CSession::GetCryptoKeySystem(std::string_view keySystem) const { - std::string_view licenseType = CSrvBroker::GetKodiProps().GetLicenseType(); - if (licenseType == "com.widevine.alpha") + if (keySystem == DRM::KS_WIDEVINE) return STREAM_CRYPTO_KEY_SYSTEM_WIDEVINE; - else if (licenseType == "com.huawei.wiseplay") + else if (keySystem == DRM::KS_WISEPLAY) return STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY; - else if (licenseType == "com.microsoft.playready") + else if (keySystem == DRM::KS_PLAYREADY) return STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY; - else if (licenseType == "org.w3.clearkey") + else if (keySystem == DRM::KS_CLEARKEY) return STREAM_CRYPTO_KEY_SYSTEM_CLEARKEY; else return STREAM_CRYPTO_KEY_SYSTEM_NONE; diff --git a/src/Session.h b/src/Session.h index 78a5ff162..cc93dcb1c 100644 --- a/src/Session.h +++ b/src/Session.h @@ -236,7 +236,7 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver /*! \brief Get the type crypto key system in use * \return enum of crypto key system */ - STREAM_CRYPTO_KEY_SYSTEM GetCryptoKeySystem() const; + STREAM_CRYPTO_KEY_SYSTEM GetCryptoKeySystem(std::string_view keySystem) const; /*! \brief Check if there is an initial discontinuity sequence number * \return True if there is an initial discontinuity sequence number @@ -343,7 +343,6 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver private: std::string m_manifestUrl; - std::vector m_serverCertificate; std::unique_ptr m_dllHelper; std::shared_ptr m_decrypter; @@ -366,6 +365,5 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver uint64_t m_chapterStartTime{0}; // In STREAM_TIME_BASE double m_chapterSeekTime{0.0}; // In seconds uint8_t m_mediaTypeMask{0}; - uint8_t m_drmConfig{0}; }; } // namespace SESSION diff --git a/src/aes_decrypter.cpp b/src/aes_decrypter.cpp index 89d92cf2f..99ad2710a 100644 --- a/src/aes_decrypter.cpp +++ b/src/aes_decrypter.cpp @@ -62,7 +62,7 @@ void AESDecrypter::ivFromSequence(uint8_t *buffer, uint64_t sid) memset(buffer, 0, 16); AP4_BytesFromUInt64BE(buffer + 8, sid); } - +/* bool AESDecrypter::RenewLicense(const std::string &pluginUrl) { std::vector items; @@ -73,3 +73,4 @@ bool AESDecrypter::RenewLicense(const std::string &pluginUrl) } return false; } +*/ diff --git a/src/aes_decrypter.h b/src/aes_decrypter.h index 0632c87c3..d5b8c1e9f 100644 --- a/src/aes_decrypter.h +++ b/src/aes_decrypter.h @@ -24,7 +24,8 @@ class ATTR_DLL_LOCAL AESDecrypter : public IAESDecrypter { public: - AESDecrypter(std::string_view licenseKey) : m_licenseKey(licenseKey) {} + AESDecrypter() = default; + // AESDecrypter(std::string_view licenseKey) : m_licenseKey(licenseKey) {} virtual ~AESDecrypter() = default; void decrypt(const AP4_UI08* aes_key, @@ -36,9 +37,9 @@ class ATTR_DLL_LOCAL AESDecrypter : public IAESDecrypter bool lastChunk); std::string convertIV(const std::string& input); void ivFromSequence(uint8_t* buffer, uint64_t sid); - const std::string& getLicenseKey() const { return m_licenseKey; }; - bool RenewLicense(const std::string& pluginUrl); + // const std::string& getLicenseKey() const { return m_licenseKey; }; + // bool RenewLicense(const std::string& pluginUrl); -private: - std::string m_licenseKey; + // private: + // std::string m_licenseKey; }; diff --git a/src/common/Period.h b/src/common/Period.h index f885fffc0..c3368522b 100644 --- a/src/common/Period.h +++ b/src/common/Period.h @@ -82,8 +82,8 @@ class ATTR_DLL_LOCAL CPeriod : public CCommonSegAttribs void SetEncryptionState(EncryptionState encryptState) { m_encryptionState = encryptState; } // Force the use of secure decoder only when parsed manifest specify it - uint64_t IsSecureDecodeNeeded() const { return m_isSecureDecoderNeeded; } - void SetSecureDecodeNeeded(uint64_t isSecureDecoderNeeded) + std::optional IsSecureDecodeNeeded() const { return m_isSecureDecoderNeeded; } + void SetSecureDecodeNeeded(std::optional isSecureDecoderNeeded) { m_isSecureDecoderNeeded = isSecureDecoderNeeded; }; @@ -141,7 +141,7 @@ class ATTR_DLL_LOCAL CPeriod : public CCommonSegAttribs uint64_t m_start{NO_VALUE}; uint64_t m_duration{0}; EncryptionState m_encryptionState{EncryptionState::UNENCRYPTED}; - bool m_isSecureDecoderNeeded{false}; + std::optional m_isSecureDecoderNeeded; std::vector m_segmentTimelineDuration; }; diff --git a/src/decrypters/DrmFactory.cpp b/src/decrypters/DrmFactory.cpp index 583ea9981..d0103145e 100644 --- a/src/decrypters/DrmFactory.cpp +++ b/src/decrypters/DrmFactory.cpp @@ -8,8 +8,12 @@ #include "DrmFactory.h" -#include +#include "CompKodiProps.h" +#include "Helpers.h" #include "clearkey/ClearKeyDecrypter.h" +#include "utils/Base64Utils.h" +#include "utils/log.h" + #if ANDROID #include "widevineandroid/WVDecrypter.h" #else @@ -18,7 +22,84 @@ #endif #endif -using namespace DRM; +#include + +using namespace UTILS; + +namespace +{ +// \brief Fill in missing drm configuration info with defaults +void FillDrmConfigDefaults(std::string_view keySystem, DRM::Config& cfg) +{ + auto& licCfg = cfg.license; + + if (keySystem == DRM::KS_WIDEVINE) + { + if (!licCfg.isHttpGetRequest) + { + if (licCfg.reqHeaders.empty()) + licCfg.reqHeaders["Content-Type"] = "application/octet-stream"; + } + } + else if (keySystem == DRM::KS_PLAYREADY) + { + if (!licCfg.isHttpGetRequest) + { + if (licCfg.reqHeaders.empty()) + { + licCfg.reqHeaders["Content-Type"] = "text/xml"; + licCfg.reqHeaders["SOAPAction"] = + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"; + } + } + } + else if (keySystem == DRM::KS_WISEPLAY) + { + if (!licCfg.isHttpGetRequest) + { + if (licCfg.reqHeaders.empty()) + licCfg.reqHeaders["Content-Type"] = "application/json"; + } + } +} +} // unnamed namespace + +DRM::Config DRM::CreateDRMConfig(std::string_view keySystem, const ADP::KODI_PROPS::DrmCfg& propCfg) +{ + DRM::Config cfg; + + cfg.isPersistentStorage = propCfg.isPersistentStorage; + cfg.optKeyReqParams = propCfg.optKeyReqParams; + cfg.isNewConfig = propCfg.isNewConfig; + + auto& propLicCfg = propCfg.license; + auto& licCfg = cfg.license; + + licCfg.serverCert = BASE64::Decode(propLicCfg.serverCert); + licCfg.serverUrl = propLicCfg.serverUrl; + licCfg.isHttpGetRequest = propLicCfg.isHttpGetRequest; + + if (!propLicCfg.reqData.empty() && !BASE64::IsValidBase64(propLicCfg.reqData) && + propCfg.isNewConfig) + { + LOG::LogF(LOGERROR, "The license \"req_data\" parameter must have data encoded as base 64."); + } + else + { + licCfg.reqData = propLicCfg.reqData; + } + + licCfg.reqHeaders = propLicCfg.reqHeaders; + licCfg.reqParams = propLicCfg.reqParams; + licCfg.wrapper = propLicCfg.wrapper; + licCfg.unwrapper = propLicCfg.unwrapper; + licCfg.unwrapperParams = propLicCfg.unwrapperParams; + licCfg.keys = propLicCfg.keys; + + FillDrmConfigDefaults(keySystem, cfg); + + return cfg; +} std::shared_ptr DRM::FACTORY::GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem) { diff --git a/src/decrypters/DrmFactory.h b/src/decrypters/DrmFactory.h index 5124436c3..dbb3d7884 100644 --- a/src/decrypters/DrmFactory.h +++ b/src/decrypters/DrmFactory.h @@ -12,8 +12,18 @@ #include +namespace ADP +{ +namespace KODI_PROPS +{ +struct DrmCfg; +} +} + namespace DRM { +DRM::Config CreateDRMConfig(std::string_view keySystem, const ADP::KODI_PROPS::DrmCfg& propCfg); + namespace FACTORY { std::shared_ptr GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem); diff --git a/src/decrypters/HelperWv.cpp b/src/decrypters/HelperWv.cpp index 846dc87c3..2e6bfcdd2 100644 --- a/src/decrypters/HelperWv.cpp +++ b/src/decrypters/HelperWv.cpp @@ -7,9 +7,22 @@ */ #include "HelperWv.h" +#include "Helpers.h" +#include "utils/Base64Utils.h" +#include "utils/DigestMD5Utils.h" +#include "utils/JsonUtils.h" +#include "utils/StringUtils.h" +#include "utils/UrlUtils.h" +#include "utils/XMLUtils.h" #include "utils/log.h" +#include + #include +#include + +using namespace pugi; +using namespace UTILS; namespace { @@ -112,6 +125,114 @@ std::vector ConvertKidToUUIDVec(const std::vector& kid) return uuid; } + +// Supported wrappers +enum class Wrapper +{ + AUTO, // Try auto-detect wrappers + NONE, // Implicit for raw binary data + BASE64, + JSON, + XML, + URL_ENC, // URL encode +}; + +// \brief Translate a wrapper string in to relative vector of enum values. +// e.g. "json+base64" --> JSON, BASE64 +std::vector TranslateWrapper(std::string_view wrapper) +{ + const std::vector wrappers = STRING::SplitToVec(wrapper, ','); + + if (wrappers.empty()) + return {}; + + std::vector result; + // Here we have to keep the order because + // defines the order in which data will be unwrapped + for (const std::string& wrapper : wrappers) + { + if (wrapper == "auto") + result.emplace_back(Wrapper::AUTO); + else if (wrapper == "none") + result.emplace_back(Wrapper::NONE); + else if (wrapper == "base64") + result.emplace_back(Wrapper::BASE64); + else if (wrapper == "json") + result.emplace_back(Wrapper::JSON); + else if (wrapper == "xml") + result.emplace_back(Wrapper::XML); + else if (wrapper == "urlenc") + result.emplace_back(Wrapper::URL_ENC); + else + { + LOG::LogF(LOGERROR, "Cannot translate license wrapper, unknown type \"%s\"", wrapper.c_str()); + return {Wrapper::AUTO}; + } + } + return result; +} + +//! @todo: to be removed in future when the old DRM properties will be removed +void ConvertDeprecatedPlaceholders(std::string& data) +{ + if (data.empty()) + return; + + if (STRING::Contains(data, "R{SSM}", false)) // Raw data + { + STRING::ReplaceFirst(data, "R{SSM}", "{CHA-RAW}"); + } + else if (STRING::Contains(data, "b{SSM}", false)) // Base64 encoded + { + STRING::ReplaceFirst(data, "b{SSM}", "{CHA-B64}"); + } + else if (STRING::Contains(data, "B{SSM}", false)) // Base64 and URL encoded + { + STRING::ReplaceFirst(data, "B{SSM}", "{CHA-B64U}"); + } + else if (STRING::Contains(data, "D{SSM}", false)) // Decimal converted + { + STRING::ReplaceFirst(data, "D{SSM}", "{CHA-DEC}"); + } + + // SESSION ID - Placeholder {SID-?} + + if (STRING::Contains(data, "R{SID}", false)) // Raw + { + STRING::ReplaceFirst(data, "R{SID}", "{SID-RAW}"); + } + else if (STRING::Contains(data, "b{SID}", false)) // Base64 encoded + { + STRING::ReplaceFirst(data, "b{SID}", "{SID-B64}"); + } + else if (STRING::Contains(data, "B{SID}", false)) // Base64 and URL encoded + { + STRING::ReplaceFirst(data, "B{SID}", "{SID-B64U}"); + } + + // KEY ID - Placeholder {KID-?} + + if (STRING::Contains(data, "R{KID}", false)) // KID converted to UUID format + { + STRING::ReplaceFirst(data, "R{KID}", "{KID-UUID}"); + } + else if (STRING::Contains(data, "H{KID}", false)) // Hexadecimal converted + { + STRING::ReplaceFirst(data, "H{KID}", "{KID-HEX}"); + } + + // PSSH - Placeholder {PSSH-?} + + if (STRING::Contains(data, "b{PSSH}", false)) // Base64 encoded + { + STRING::ReplaceFirst(data, "b{PSSH}", "{PSSH-B64}"); + } + else if (STRING::Contains(data, "B{PSSH}", false)) // Base64 and URL encoded + { + STRING::ReplaceFirst(data, "B{PSSH}", "{PSSH-B64U}"); + } +} + } // unnamed namespace std::vector DRM::MakeWidevinePsshData(const std::vector>& keyIds, @@ -196,3 +317,335 @@ void DRM::ParseWidevinePssh(const std::vector& wvPsshData, } } } + +bool DRM::WvWrapLicense(std::string& data, + const std::vector& challenge, + std::string_view sessionId, + const std::vector& kid, + const std::vector& pssh, + std::string_view wrapper, + const bool isNewConfig) +{ + //! @todo: to be removed in future when the old DRM properties will be removed + if (!isNewConfig) + ConvertDeprecatedPlaceholders(data); + + // By default raw key request (challenge) data + if (data.empty()) + data = "{CHA-RAW}"; + + // KEY REQUEST (CHALLENGE) - Placeholder {CHA-?} + + if (STRING::Contains(data, "{CHA-RAW}", false)) // Raw data + { + std::string_view krStr{reinterpret_cast(challenge.data()), challenge.size()}; + STRING::ReplaceFirst(data, "{CHA-RAW}", krStr); + } + else if (STRING::Contains(data, "{CHA-B64}", false)) // Base64 encoded + { + STRING::ReplaceFirst(data, "{CHA-B64}", BASE64::Encode(challenge)); + } + else if (STRING::Contains(data, "{CHA-B64U}", false)) // Base64 and URL encoded + { + const std::string krEnc = STRING::URLEncode(BASE64::Encode(challenge)); + STRING::ReplaceFirst(data, "{CHA-B64U}", krEnc); + } + else if (STRING::Contains(data, "{CHA-DEC}", false)) // Decimal converted + { + const std::string krDec = STRING::ToDecimal(challenge.data(), challenge.size()); + STRING::ReplaceFirst(data, "{CHA-DEC}", krDec); + } + + // SESSION ID - Placeholder {SID-?} + + if (STRING::Contains(data, "{SID-RAW}", false)) // Raw + { + STRING::ReplaceFirst(data, "{SID-RAW}", sessionId); + } + else if (STRING::Contains(data, "{SID-B64}", false)) // Base64 encoded + { + STRING::ReplaceFirst(data, "{SID-B64}", BASE64::Encode(sessionId.data())); + } + else if (STRING::Contains(data, "{SID-B64U}", false)) // Base64 and URL encoded + { + const std::string sidEnc = STRING::URLEncode(BASE64::Encode(sessionId.data())); + STRING::ReplaceFirst(data, "{SID-B64U}", sidEnc); + } + + // KEY ID - Placeholder {KID-?} + + if (STRING::Contains(data, "{KID-UUID}", false)) // KID converted to UUID format + { + STRING::ReplaceFirst(data, "{KID-UUID}", DRM::ConvertKidBytesToUUID(kid)); + } + else if (STRING::Contains(data, "{KID-HEX}", false)) // Hexadecimal converted + { + STRING::ReplaceFirst(data, "{KID-HEX}", STRING::ToHexadecimal(kid)); + } + + // PSSH - Placeholder {PSSH-?} + + if (STRING::Contains(data, "{PSSH-B64}", false)) // Base64 encoded + { + STRING::ReplaceFirst(data, "{PSSH-B64}", BASE64::Encode(pssh)); + } + else if (STRING::Contains(data, "{PSSH-B64U}", false)) // Base64 and URL encoded + { + std::string sidEnc = STRING::URLEncode(BASE64::Encode(pssh)); + STRING::ReplaceFirst(data, "{PSSH-B64U}", sidEnc); + } + + const std::vector wrappers = TranslateWrapper(wrapper); + + for (size_t i = 0; i < wrappers.size(); ++i) + { + const auto& wrapper = wrappers[i]; + + if (wrapper == Wrapper::NONE) + { + break; + } + else if (wrapper == Wrapper::BASE64) + { + data = BASE64::Encode(data); + } + else if (wrapper == Wrapper::URL_ENC) + { + data = STRING::URLEncode(data); + } + else + { + LOG::LogF(LOGERROR, "Specified an unsupported wrapper type"); + return false; + } + } + + return true; +} + +bool DRM::WvUnwrapLicense(std::string_view wrapper, + const std::map& params, + std::string_view contentType, + std::string data, + std::string& dataOut, + int& hdcpLimit) +{ + // The license response must be in binary data format + // but many services have a proprietary implementations therefore + // the license data could be wrapped by using other formats (such as base64, json, etc...) + // here we provide the support for some common wrappers, + // for more complex requirements the audio/video add-on must implement a proxy + // where it can request and process the license in a custom way and return the binary data + + std::vector wrappers = TranslateWrapper(wrapper); + + const bool isAuto = wrappers.empty() || wrappers.front() == Wrapper::AUTO; + + bool isAllowedFallbacks{false}; + + if (isAuto) + { + wrappers.clear(); + // Check mime types to try detect the wrapper + if (contentType == "application/octet-stream") + { + // its binary + } + else if (contentType == "application/json") + { + if (BASE64::IsValidBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + + wrappers.emplace_back(Wrapper::JSON); + } + else if (contentType == "application/xml" || contentType == "text/xml") + { + wrappers.emplace_back(Wrapper::XML); + } + else if (contentType == "text/plain") + { + // Some service use text mime type for XML + isAllowedFallbacks = true; + wrappers.emplace_back(Wrapper::XML); + } + else // Unknown + { + // Assumed to be binary with a possible base64 wrap + if (BASE64::IsValidBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + } + } + + // Process multiple wrappers with sequential order + + for (size_t i = 0; i < wrappers.size(); ++i) + { + const auto& wrapper = wrappers[i]; + + if (wrapper == Wrapper::NONE) + { + break; + } + else if (wrapper == Wrapper::BASE64) + { + data = BASE64::DecodeToStr(data); + } + else if (wrapper == Wrapper::JSON) + { + if (!STRING::KeyExists(params, "path_data")) + { + LOG::LogF(LOGERROR, + "Cannot parse JSON license data, missing unwrapper parameter \"path_data\""); + return false; + } + + rapidjson::Document jDoc; + jDoc.Parse(data.c_str(), data.size()); + + if (!jDoc.IsObject()) + { + LOG::LogF(LOGERROR, + "Unable to parse license data as JSON format, malformed data or wrong wrapper"); + return false; + } + + bool isJsonDataTraverse{false}; + if (STRING::KeyExists(params, "path_data_traverse")) + isJsonDataTraverse = STRING::ToLower(params.at("path_data_traverse")) == "true"; + + const rapidjson::Value* jDataObjValue; + if (isJsonDataTraverse) + jDataObjValue = JSON::GetValueTraversePaths(jDoc, params.at("path_data")); + else + jDataObjValue = JSON::GetValueAtPath(jDoc, params.at("path_data")); + + if (!jDataObjValue || !jDataObjValue->IsString()) + { + LOG::LogF(LOGERROR, "Unable to get license data from JSON path, possible wrong path on " + "\"path_data\" parameter"); + return false; + } + + data = jDataObjValue->GetString(); + + if (STRING::KeyExists(params, "path_hdcp")) + { + bool isJsonHdcpTraverse{false}; + if (STRING::KeyExists(params, "path_hdcp_traverse")) + isJsonHdcpTraverse = STRING::ToLower(params.at("path_hdcp_traverse")) == "true"; + + const rapidjson::Value* jHdcpObjValue; + if (isJsonHdcpTraverse) + jHdcpObjValue = JSON::GetValueTraversePaths(jDoc, params.at("path_hdcp")); + else + jHdcpObjValue = JSON::GetValueAtPath(jDoc, params.at("path_hdcp")); + + if (!jHdcpObjValue) + { + LOG::LogF(LOGERROR, "Unable to parse JSON HDCP value, path \"%s\" not found", + params.at("path_hdcp").c_str()); + } + else if (!jHdcpObjValue->IsInt()) + { + LOG::LogF(LOGERROR, + "Unable to parse JSON HDCP value, value with wrong data type on path \"%s\"", + params.at("path_hdcp").c_str()); + } + else + hdcpLimit = jHdcpObjValue->GetInt(); + } + + if (isAuto && BASE64::IsValidBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + } + else if (wrapper == Wrapper::XML) + { + if (!STRING::KeyExists(params, "path_data")) + { + LOG::LogF(LOGERROR, + "Cannot parse XML license data, missing unwrapper parameter \"path_data\""); + return false; + } + + xml_document doc; + xml_parse_result parseRes = doc.load_buffer(data.c_str(), data.size()); + + if (parseRes.status != status_ok) + { + if (isAllowedFallbacks) + { + LOG::LogF(LOGDEBUG, "License data not in XML format, fallback to binary"); + wrappers.emplace_back(Wrapper::NONE); + continue; + } + else + { + LOG::LogF(LOGERROR, "Unable to parse XML license data, malformed data or wrong wrapper"); + return false; + } + } + + pugi::xml_node node = doc.select_node(params.at("path_data").c_str()).node(); + if (!node) + { + LOG::LogF(LOGERROR, "Unable to get license data from XML path \"%s\"", + params.at("path_data").c_str()); + return false; + } + data = node.child_value(); + + if (isAuto && BASE64::IsValidBase64(data)) + wrappers.emplace_back(Wrapper::BASE64); + } + else + { + LOG::LogF(LOGERROR, "Specified an unsupported unwrapper type"); + return false; + } + } + + if (data.empty()) + { + LOG::LogF(LOGERROR, "No license data, a problem occurred while processing license wrappers"); + return false; + } + + //! @todo: the support to binary license data (with HB) that start with "\r\n\r\n" has not been reintroduced with the + //! rework of this code, this is a old unclear addition, seem there are no info about this on web, + //! and seem no addons use it, so for now is removed, if in the future someone complain about this lack + //! will be possible reintroduce it and include more clear info about this use case. + // if (data.compare(0, 4, "\r\n\r\n") == 0) + // data.erase(0, 4); + + dataOut = data; + + return true; +} + +void DRM::TranslateLicenseUrlPh(std::string& url, + const std::vector& challenge, + const bool isNewConfig) +{ + if (!isNewConfig) + { + // Replace deprecated placeholders + //! @todo: to be removed in future when the old DRM properties will be removed + if (STRING::Contains(url, "B{SSM}", false)) // Base64 and URL encoded + STRING::ReplaceFirst(url, "B{SSM}", "{CHA-B64U}"); + if (STRING::Contains(url, "{HASH}", false)) // MD5 hash + STRING::ReplaceFirst(url, "{HASH}", "{CHA-MD5}"); + } + + if (STRING::Contains(url, "{CHA-B64U}", false)) // Base64 and URL encoded + { + const std::string krEnc = STRING::URLEncode(BASE64::Encode(challenge)); + STRING::ReplaceFirst(url, "{CHA-B64U}", krEnc); + } + else if (STRING::Contains(url, "{CHA-MD5}", false)) // MD5 hash + { + DIGEST::MD5 md5; + md5.Update(challenge.data(), static_cast(challenge.size())); + md5.Finalize(); + STRING::ReplaceFirst(url, "{CHA-MD5}", md5.HexDigest()); + } +} diff --git a/src/decrypters/HelperWv.h b/src/decrypters/HelperWv.h index 83b22825f..5c71ada8b 100644 --- a/src/decrypters/HelperWv.h +++ b/src/decrypters/HelperWv.h @@ -15,9 +15,18 @@ #endif #include +#include #include +#include +#include #include +// forward +namespace DRM +{ +struct Config; +} + enum class CdmMessageType { UNKNOWN, @@ -58,7 +67,7 @@ class ATTR_DLL_LOCAL IWVCdmAdapter : public IWVSubject virtual std::shared_ptr GetCDM() = 0; - virtual const std::string& GetLicenseUrl() = 0; + virtual const DRM::Config& GetConfig() = 0; virtual void SetCodecInstance(void* instance) {} virtual void ResetCodecInstance() {} @@ -102,4 +111,23 @@ std::vector MakeWidevinePsshData(const std::vector void ParseWidevinePssh(const std::vector& wvPsshData, std::vector>& keyIds); +bool WvWrapLicense(std::string& data, + const std::vector& challenge, + std::string_view sessionId, + const std::vector& kid, + const std::vector& pssh, + std::string_view wrapper, + const bool isNewConfig); + +bool WvUnwrapLicense(std::string_view wrapper, + const std::map& params, + std::string_view contentType, + std::string data, + std::string& dataOut, + int& hdcpLimit); + +void TranslateLicenseUrlPh(std::string& url, + const std::vector& challenge, + const bool isNewConfig); + } // namespace DRM diff --git a/src/decrypters/Helpers.cpp b/src/decrypters/Helpers.cpp index f85977ad9..48123ac71 100644 --- a/src/decrypters/Helpers.cpp +++ b/src/decrypters/Helpers.cpp @@ -122,7 +122,21 @@ const uint8_t* DRM::KeySystemToUUID(std::string_view ks) return nullptr; } -bool DRM::IsKeySystemSupported(std::string_view keySystem) +std::string DRM::KeySystemToUUIDstr(std::string_view ks) +{ + if (ks == KS_WIDEVINE) + return UUID_WIDEVINE.data(); + else if (ks == KS_PLAYREADY) + return UUID_PLAYREADY.data(); + else if (ks == KS_WISEPLAY) + return UUID_WISEPLAY.data(); + else if (ks == KS_CLEARKEY) + return UUID_CLEARKEY.data(); + else + return "unknown"; +} + +bool DRM::IsValidKeySystem(std::string_view keySystem) { return keySystem == DRM::KS_NONE || keySystem == DRM::KS_WIDEVINE || keySystem == DRM::KS_PLAYREADY || keySystem == DRM::KS_WISEPLAY || diff --git a/src/decrypters/Helpers.h b/src/decrypters/Helpers.h index d20920783..619bc2ca2 100644 --- a/src/decrypters/Helpers.h +++ b/src/decrypters/Helpers.h @@ -25,6 +25,12 @@ constexpr std::string_view KS_CLEARKEY = "org.w3.clearkey"; // DRM UUIDs +constexpr std::string_view UUID_WIDEVINE = "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; +constexpr std::string_view UUID_PLAYREADY = "9a04f079-9840-4286-ab92-e65be0885f95"; +constexpr std::string_view UUID_WISEPLAY = "3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c"; +constexpr std::string_view UUID_CLEARKEY = "e2719d58-a985-b3c9-781a-b030af78d30e"; +constexpr std::string_view UUID_COMMON = "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"; + constexpr std::string_view URN_WIDEVINE = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; constexpr std::string_view URN_PLAYREADY = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95"; constexpr std::string_view URN_WISEPLAY = "urn:uuid:3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c"; @@ -46,7 +52,9 @@ std::string KeySystemToDrmName(std::string_view ks); const uint8_t* KeySystemToUUID(std::string_view ks); -bool IsKeySystemSupported(std::string_view keySystem); +std::string KeySystemToUUIDstr(std::string_view ks); + +bool IsValidKeySystem(std::string_view keySystem); /*! * \brief Generate an hash by using the base domain of an URL. diff --git a/src/decrypters/IDecrypter.h b/src/decrypters/IDecrypter.h index 903835543..d877191b6 100644 --- a/src/decrypters/IDecrypter.h +++ b/src/decrypters/IDecrypter.h @@ -9,7 +9,9 @@ #pragma once #include +#include #include +#include #include #include @@ -44,6 +46,48 @@ struct DecrypterCapabilites int hdcpLimit{0}; // If set (> 0) streams that are greater than the multiplication of "Width x Height" cannot be played. }; +struct Config +{ + // To enable persistent state CDM behaviour + bool isPersistentStorage{false}; + // Optional parameters to make the CDM key request (CDM specific parameters) + std::map optKeyReqParams; + + struct License + { + // The license server certificate + std::vector serverCert; + // The license server url + std::string serverUrl; + // To force an HTTP GET request, instead that POST request + bool isHttpGetRequest{false}; + // HTTP request headers + std::map reqHeaders; + // HTTP parameters to append to the url + std::string reqParams; + // Custom license data encoded as base64 to make the HTTP license request + std::string reqData; + // License data wrappers + // Multiple wrappers supported e.g. "base64,json", the name order defines the order + // in which data will be wrapped, (1) base64 --> (2) url + std::string wrapper; + // License data unwrappers + // Multiple un-wrappers supported e.g. "base64,json", the name order defines the order + // in which data will be unwrapped, (1) base64 --> (2) json + std::string unwrapper; + // License data unwrappers parameters + std::map unwrapperParams; + // Clear key's for ClearKey DRM (KID / KEY pair) + std::map keys; + }; + + // The license configuration + License license; + // Specifies if has been parsed the new DRM config ("drm" or "drm_legacy" kodi property) + //! @todo: to remove when deprecated DRM properties will be removed + bool isNewConfig{true}; +}; + class IDecrypter { public: @@ -66,19 +110,14 @@ class IDecrypter /** * \brief Initialise the DRM system - * \param licenseURL The license URL to contact if applicable - * \param serverCertificate Server certificate to supply if applicable - * \param config Flags to be passed to the decrypter + * \param config The DRM configuration * \return true on success */ - virtual bool OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) = 0; + virtual bool OpenDRMSystem(const DRM::Config& config) = 0; /** * \brief Creates a Single Sample Decrypter for decrypting content * \param initData The data for initialising the decrypter (e.g. PSSH) - * \param optionalKeyParameter License key data passed into IA as parameter * \param defaultkeyid The default KeyID to initialise with * \param licenseUrl The license server URL * \param skipSessionMessage False for preinitialisation case @@ -87,7 +126,6 @@ class IDecrypter */ virtual std::shared_ptr CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId, std::string_view licenseUrl, bool skipSessionMessage, diff --git a/src/decrypters/clearkey/ClearKeyDecrypter.cpp b/src/decrypters/clearkey/ClearKeyDecrypter.cpp index b98643309..934bf3011 100644 --- a/src/decrypters/clearkey/ClearKeyDecrypter.cpp +++ b/src/decrypters/clearkey/ClearKeyDecrypter.cpp @@ -9,8 +9,6 @@ #include "ClearKeyDecrypter.h" #include "ClearKeyCencSingleSampleDecrypter.h" -#include "CompKodiProps.h" -#include "SrvBroker.h" #include "decrypters/Helpers.h" #include "utils/log.h" @@ -25,16 +23,15 @@ std::vector CClearKeyDecrypter::SelectKeySystems(std::string_v return keySystems; } -bool CClearKeyDecrypter::OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) +bool CClearKeyDecrypter::OpenDRMSystem(const DRM::Config& config) { + m_config = config; + m_isInitialized = true; return true; } std::shared_ptr CClearKeyDecrypter::CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultkeyid, std::string_view licenseUrl, bool skipSessionMessage, @@ -47,21 +44,21 @@ std::shared_ptr CClearKeyDecrypter::CreateSi } std::shared_ptr decrypter; - auto& cfgLic = CSrvBroker::GetKodiProps().GetDrmConfig(std::string(DRM::KS_CLEARKEY)).license; + const DRM::Config::License& licConfig = m_config.license; // If keys / license url are provided by Kodi property, those of the manifest will be overwritten - if (!cfgLic.serverUrl.empty()) - licenseUrl = cfgLic.serverUrl; + if (!licConfig.serverUrl.empty()) + licenseUrl = licConfig.serverUrl; - if ((!cfgLic.keys.empty() || !initData.empty()) && cfgLic.serverUrl.empty()) // Keys provided from manifest or Kodi property + if ((!licConfig.keys.empty() || !initData.empty()) && licConfig.serverUrl.empty()) // Keys provided from manifest or Kodi property { decrypter = std::make_shared(initData, defaultkeyid, - cfgLic.keys, this); + licConfig.keys, this); } else // Clearkey license server URL provided { - decrypter = std::make_shared(licenseUrl, cfgLic.reqHeaders, + decrypter = std::make_shared(licenseUrl, licConfig.reqHeaders, defaultkeyid, this); } diff --git a/src/decrypters/clearkey/ClearKeyDecrypter.h b/src/decrypters/clearkey/ClearKeyDecrypter.h index a88f334e4..08ae4a6ce 100644 --- a/src/decrypters/clearkey/ClearKeyDecrypter.h +++ b/src/decrypters/clearkey/ClearKeyDecrypter.h @@ -9,8 +9,6 @@ #pragma once #include "decrypters/IDecrypter.h" -#include - using namespace DRM; class CClearKeyDecrypter : public IDecrypter @@ -19,12 +17,9 @@ class CClearKeyDecrypter : public IDecrypter CClearKeyDecrypter(){}; virtual ~CClearKeyDecrypter() override{}; virtual std::vector SelectKeySystems(std::string_view keySystem) override; - virtual bool OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) override; + virtual bool OpenDRMSystem(const DRM::Config& config) override; virtual std::shared_ptr CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultkeyid, std::string_view licenseUrl, bool skipSessionMessage, @@ -38,7 +33,7 @@ class CClearKeyDecrypter : public IDecrypter } virtual bool HasLicenseKey(std::shared_ptr decrypter, const std::vector& keyid) override; - virtual bool IsInitialised() override { return true; } + virtual bool IsInitialised() override { return m_isInitialized; } virtual std::string GetChallengeB64Data(std::shared_ptr decrypter) override { return ""; @@ -68,16 +63,8 @@ class CClearKeyDecrypter : public IDecrypter virtual void ReleaseBuffer(void* instance, void* buffer) {} virtual std::string_view GetLibraryPath() const override { return m_libraryPath; } - void insertssd(AP4_CencSingleSampleDecrypter* ssd) { ssds.push_back(ssd); }; - void removessd(AP4_CencSingleSampleDecrypter* ssd) - { - std::vector::iterator res( - std::find(ssds.begin(), ssds.end(), ssd)); - if (res != ssds.end()) - ssds.erase(res); - }; - private: - std::vector ssds; + bool m_isInitialized{false}; + DRM::Config m_config; std::string m_libraryPath; }; diff --git a/src/decrypters/widevine/CMakeLists.txt b/src/decrypters/widevine/CMakeLists.txt index 46594f0c8..3dc4ec540 100644 --- a/src/decrypters/widevine/CMakeLists.txt +++ b/src/decrypters/widevine/CMakeLists.txt @@ -3,7 +3,6 @@ set(SOURCES WVCdmAdapter.cpp CdmFixedBuffer.cpp WVDecrypter.cpp - jsmn.c CdmTypeConversion.cpp ) @@ -12,7 +11,6 @@ set(HEADERS WVCdmAdapter.h CdmFixedBuffer.h WVDecrypter.h - jsmn.h CdmTypeConversion.h ) diff --git a/src/decrypters/widevine/WVCdmAdapter.cpp b/src/decrypters/widevine/WVCdmAdapter.cpp index 12589eb07..8a295077f 100644 --- a/src/decrypters/widevine/WVCdmAdapter.cpp +++ b/src/decrypters/widevine/WVCdmAdapter.cpp @@ -30,11 +30,8 @@ constexpr const char* LIBRARY_FILENAME = "libwidevinecdm.so"; #endif } // unnamed namespace -CWVCdmAdapter::CWVCdmAdapter(std::string_view licenseURL, - const std::vector& serverCert, - const uint8_t config, - CWVDecrypter* host) - : m_licenseUrl(licenseURL), m_host(host) +CWVCdmAdapter::CWVCdmAdapter(const DRM::Config& config, CWVDecrypter* host) + : m_config(config), m_host(host) { if (m_host->GetLibraryPath().empty()) { @@ -43,15 +40,9 @@ CWVCdmAdapter::CWVCdmAdapter(std::string_view licenseURL, } std::string cdmPath = FILESYS::PathCombine(m_host->GetLibraryPath(), LIBRARY_FILENAME); - if (licenseURL.empty()) - { - LOG::LogF(LOGERROR, "No license URL path specified"); - return; - } - // The license url come from license_key kodi property // we have to kept only the url without the parameters specified after pipe "|" char - std::string licUrl = m_licenseUrl; + std::string licUrl = m_config.license.serverUrl; const size_t urlPipePos = licUrl.find('|'); if (urlPipePos != std::string::npos) licUrl.erase(urlPipePos); @@ -62,10 +53,10 @@ CWVCdmAdapter::CWVCdmAdapter(std::string_view licenseURL, basePath = FILESYS::PathCombine(basePath, DRM::GenerateUrlDomainHash(licUrl)); basePath += FILESYS::SEPARATOR; - m_cdmAdapter = std::make_shared( - "com.widevine.alpha", cdmPath, basePath, - media::CdmConfig(false, (config & DRM::IDecrypter::CONFIG_PERSISTENTSTORAGE) != 0), - dynamic_cast(this)); + m_cdmAdapter = + std::make_shared("com.widevine.alpha", cdmPath, basePath, + media::CdmConfig(false, m_config.isPersistentStorage), + dynamic_cast(this)); if (!m_cdmAdapter->valid()) { @@ -74,12 +65,11 @@ CWVCdmAdapter::CWVCdmAdapter(std::string_view licenseURL, return; } - if (!serverCert.empty()) - m_cdmAdapter->SetServerCertificate(0, serverCert.data(), serverCert.size()); - - // For backward compatibility: If no | is found in URL, use the most common working config - if (m_licenseUrl.find('|') == std::string::npos) - m_licenseUrl += "|Content-Type=application%2Foctet-stream|R{SSM}|"; + const std::vector& cert = m_config.license.serverCert; + if (!cert.empty()) + { + m_cdmAdapter->SetServerCertificate(0, cert.data(), cert.size()); + } // m_cdmAdapter->GetStatusForPolicy(); // m_cdmAdapter->QueryOutputProtectionStatus(); @@ -135,6 +125,11 @@ cdm::Buffer* CWVCdmAdapter::AllocateBuffer(size_t sz) return nullptr; } +const DRM::Config& CWVCdmAdapter::GetConfig() +{ + return m_config; +} + void CWVCdmAdapter::SetCodecInstance(void* instance) { m_codecInstance = reinterpret_cast(instance); diff --git a/src/decrypters/widevine/WVCdmAdapter.h b/src/decrypters/widevine/WVCdmAdapter.h index c6ffffc9a..7d3f5be40 100644 --- a/src/decrypters/widevine/WVCdmAdapter.h +++ b/src/decrypters/widevine/WVCdmAdapter.h @@ -11,6 +11,7 @@ #include "CdmBuffer.h" #include "cdm/media/cdm/cdm_adapter.h" #include "decrypters/HelperWv.h" +#include "decrypters/IDecrypter.h" #include #include @@ -25,10 +26,7 @@ class ATTR_DLL_LOCAL CWVCdmAdapter : public media::CdmAdapterClient, public IWVCdmAdapter { public: - CWVCdmAdapter(std::string_view licenseURL, - const std::vector& serverCert, - const uint8_t config, - CWVDecrypter* host); + CWVCdmAdapter(const DRM::Config& config, CWVDecrypter* host); virtual ~CWVCdmAdapter(); // media::CdmAdapterClient interface methods @@ -44,7 +42,7 @@ class ATTR_DLL_LOCAL CWVCdmAdapter : public media::CdmAdapterClient, // IWVCdmAdapter interface methods std::shared_ptr GetCDM() override { return m_cdmAdapter; } - const std::string& GetLicenseUrl() override { return m_licenseUrl; } + const DRM::Config& GetConfig() override; void SetCodecInstance(void* instance) override; void ResetCodecInstance() override; std::string_view GetKeySystem() override; @@ -57,8 +55,8 @@ class ATTR_DLL_LOCAL CWVCdmAdapter : public media::CdmAdapterClient, void NotifyObservers(const CdmMessage& message) override; private: + DRM::Config m_config; std::shared_ptr m_cdmAdapter; - std::string m_licenseUrl; kodi::addon::CInstanceVideoCodec* m_codecInstance{nullptr}; CWVDecrypter* m_host; std::list m_observers; diff --git a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp index a93741b54..aec1f86da 100644 --- a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp +++ b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp @@ -15,7 +15,6 @@ #include "CdmTypeConversion.h" #include "WVCdmAdapter.h" #include "cdm/media/cdm/cdm_adapter.h" -#include "jsmn.h" #include "decrypters/Helpers.h" #include "utils/Base64Utils.h" #include "utils/CurlUtils.h" @@ -238,15 +237,6 @@ void CWVCencSingleSampleDecrypter::CheckLicenseRenewal() bool CWVCencSingleSampleDecrypter::SendSessionMessage() { - std::vector blocks{STRING::SplitToVec(m_cdmAdapter->GetLicenseUrl(), '|')}; - - if (blocks.size() != 4) - { - LOG::LogF(LOGERROR, "Wrong \"|\" blocks in license URL. Four blocks (req | header | body | " - "response) are expected in license URL"); - return false; - } - if (CSrvBroker::GetSettings().IsDebugLicense()) { std::string debugFilePath = FILESYS::PathCombine( @@ -256,344 +246,113 @@ bool CWVCencSingleSampleDecrypter::SendSessionMessage() UTILS::FILESYS::SaveFile(debugFilePath, data, true); } - //Process placeholder in GET String - std::string::size_type insPos(blocks[0].find("{SSM}")); - if (insPos != std::string::npos) + const DRM::Config drmCfg = m_cdmAdapter->GetConfig(); + const DRM::Config::License& licConfig = drmCfg.license; + //! @todo: cleanup this var + std::vector challenge(reinterpret_cast(m_challenge.GetData()), + reinterpret_cast(m_challenge.GetData()) + + m_challenge.GetDataSize()); + std::string reqData; + + if (!licConfig.isHttpGetRequest) // Make HTTP POST request { - if (insPos > 0 && blocks[0][insPos - 1] == 'B') + if (licConfig.reqData.empty()) // By default add raw challenge { - std::string msgEncoded{BASE64::Encode(m_challenge.GetData(), m_challenge.GetDataSize())}; - msgEncoded = STRING::URLEncode(msgEncoded); - blocks[0].replace(insPos - 1, 6, msgEncoded); + reqData.assign(challenge.cbegin(), challenge.cend()); } else { - LOG::Log(LOGERROR, "Unsupported License request template (command)"); - return false; - } - } - - insPos = blocks[0].find("{HASH}"); - if (insPos != std::string::npos) - { - DIGEST::MD5 md5; - md5.Update(m_challenge.GetData(), m_challenge.GetDataSize()); - md5.Finalize(); - blocks[0].replace(insPos, 6, md5.HexDigest()); - } - - CURL::CUrl file{blocks[0].c_str()}; - file.AddHeader("Expect", ""); - - std::string response; - std::string resLimit; - std::string contentType; - char buf[2048]; - bool serverCertRequest; - - //Process headers - std::vector headers{STRING::SplitToVec(blocks[1], '&')}; - for (std::string& headerStr : headers) - { - std::vector header{STRING::SplitToVec(headerStr, '=')}; - if (!header.empty()) - { - STRING::Trim(header[0]); - std::string value; - if (header.size() > 1) + if (BASE64::IsValidBase64(licConfig.reqData)) + reqData = BASE64::DecodeToStr(licConfig.reqData); + else //! @todo: this fallback as plain text must be removed when the deprecated DRM properties are removed, and so replace it to return error + reqData = licConfig.reqData; + + // Some services have a customized license server that require data to be wrapped with their formats (e.g. JSON). + // Here we provide a built-in way to customize the license data to be sent, this avoid force add-ons to integrate + // an HTTP server proxy to manage the license data request/response, and so use Kodi properties to set wrappers. + if (m_cdmAdapter->GetKeySystem() == DRM::KS_WIDEVINE && + !DRM::WvWrapLicense(reqData, challenge, m_strSession, m_defaultKeyId, m_pssh, + licConfig.wrapper, drmCfg.isNewConfig)) { - STRING::Trim(header[1]); - value = STRING::URLDecode(header[1]); + return false; } - file.AddHeader(header[0].c_str(), value.c_str()); } } - //Process body - if (!blocks[2].empty()) + if (CSrvBroker::GetSettings().IsDebugLicense()) { - if (blocks[2][0] == '%') - blocks[2] = STRING::URLDecode(blocks[2]); - - insPos = blocks[2].find("{SSM}"); - if (insPos != std::string::npos) - { - std::string::size_type sidPos(blocks[2].find("{SID}")); - std::string::size_type kidPos(blocks[2].find("{KID}")); - - char fullDecode = 0; - if (insPos > 1 && sidPos > 1 && kidPos > 1 && (blocks[2][0] == 'b' || blocks[2][0] == 'B') && - blocks[2][1] == '{') - { - fullDecode = blocks[2][0]; - blocks[2] = blocks[2].substr(2, blocks[2].size() - 3); - insPos -= 2; - if (kidPos != std::string::npos) - kidPos -= 2; - if (sidPos != std::string::npos) - sidPos -= 2; - } - - size_t size_written(0); - - if (insPos > 0) - { - if (blocks[2][insPos - 1] == 'B' || blocks[2][insPos - 1] == 'b') - { - std::string msgEncoded{BASE64::Encode(m_challenge.GetData(), m_challenge.GetDataSize())}; - if (blocks[2][insPos - 1] == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2].replace(insPos - 1, 6, msgEncoded); - size_written = msgEncoded.size(); - } - else if (blocks[2][insPos - 1] == 'D') - { - std::string msgEncoded{ - STRING::ToDecimal(m_challenge.GetData(), m_challenge.GetDataSize())}; - blocks[2].replace(insPos - 1, 6, msgEncoded); - size_written = msgEncoded.size(); - } - else - { - blocks[2].replace(insPos - 1, 6, reinterpret_cast(m_challenge.GetData()), - m_challenge.GetDataSize()); - size_written = m_challenge.GetDataSize(); - } - } - else - { - LOG::Log(LOGERROR, "Unsupported License request template (body / ?{SSM})"); - return false; - } - - if (sidPos != std::string::npos && insPos < sidPos) - sidPos += size_written, sidPos -= 6; - - if (kidPos != std::string::npos && insPos < kidPos) - kidPos += size_written, kidPos -= 6; - - size_written = 0; - - if (sidPos != std::string::npos) - { - if (sidPos > 0) - { - if (blocks[2][sidPos - 1] == 'B' || blocks[2][sidPos - 1] == 'b') - { - std::string msgEncoded{BASE64::Encode(m_strSession)}; - - if (blocks[2][sidPos - 1] == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - - blocks[2].replace(sidPos - 1, 6, msgEncoded); - size_written = msgEncoded.size(); - } - else - { - blocks[2].replace(sidPos - 1, 6, m_strSession.data(), m_strSession.size()); - size_written = m_strSession.size(); - } - } - else - { - LOG::LogF(LOGERROR, "Unsupported License request template (body / ?{SID})"); - return false; - } - } - - if (kidPos != std::string::npos) - { - if (sidPos < kidPos) - kidPos += size_written, kidPos -= 6; - - if (blocks[2][kidPos - 1] == 'H') - { - std::string keyIdUUID{STRING::ToHexadecimal(m_defaultKeyId)}; - blocks[2].replace(kidPos - 1, 6, keyIdUUID.c_str(), 32); - } - else - { - std::string kidUUID{DRM::ConvertKidBytesToUUID(m_defaultKeyId)}; - blocks[2].replace(kidPos, 5, kidUUID.c_str(), 36); - } - } + std::string debugFilePath = FILESYS::PathCombine( + m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.request"); + UTILS::FILESYS::SaveFile(debugFilePath, reqData, true); + } - if (fullDecode) - { - std::string msgEncoded{BASE64::Encode(blocks[2])}; - if (fullDecode == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2] = msgEncoded; - } - } + std::string url = licConfig.serverUrl; + DRM::TranslateLicenseUrlPh(url, challenge, drmCfg.isNewConfig); - std::string encData{BASE64::Encode(blocks[2])}; - //! @todo: inappropriate use of "postdata" header, use CURL::CUrl for post request - file.AddHeader("postdata", encData.c_str()); - } + CURL::CUrl cUrl{url, reqData}; + cUrl.AddHeaders(licConfig.reqHeaders); - serverCertRequest = m_challenge.GetDataSize() == 2; - m_challenge.SetDataSize(0); + const int statusCode = cUrl.Open(); - int statusCode = file.Open(); if (statusCode == -1 || statusCode >= 400) { LOG::Log(LOGERROR, "License server returned failure (HTTP error %i)", statusCode); return false; } - CURL::ReadStatus downloadStatus = CURL::ReadStatus::CHUNK_READ; - while (downloadStatus == CURL::ReadStatus::CHUNK_READ) + std::string respData; + if (cUrl.Read(respData) == CURL::ReadStatus::ERROR) { - downloadStatus = file.Read(response); + LOG::LogF(LOGERROR, "Cannot read license server response"); + return false; } - resLimit = file.GetResponseHeader("X-Limit-Video"); - contentType = file.GetResponseHeader("Content-Type"); + const std::string resLimit = cUrl.GetResponseHeader("X-Limit-Video"); // Custom header + const std::string respContentType = cUrl.GetResponseHeader("Content-Type"); if (!resLimit.empty()) { - std::string::size_type posMax = resLimit.find("max="); // log/check this + // To force limit playable streams resolutions + size_t posMax = resLimit.find("max="); if (posMax != std::string::npos) m_resolutionLimit = std::atoi(resLimit.data() + (posMax + 4)); } - if (downloadStatus == CURL::ReadStatus::ERROR) - { - LOG::LogF(LOGERROR, "Could not read full SessionMessage response"); - return false; - } - - if (CSrvBroker::GetSettings().IsDebugLicense()) - { - std::string debugFilePath = FILESYS::PathCombine( - m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"); - FILESYS::SaveFile(debugFilePath, response, true); - } + // The first request could be the license certificate request + // this request is done by sending a challenge of 2 bytes, 0x08 0x04 (CAQ=) + //! @todo: compare data not only by size but also by bytes + const bool isCertRequest = + m_challenge.GetDataSize() == 2 && respContentType == "application/octet-stream"; + m_challenge.SetDataSize(0); - if (serverCertRequest && contentType.find("application/octet-stream") == std::string::npos) - serverCertRequest = false; + int hdcpLimit{0}; - if (!blocks[3].empty() && blocks[3][0] != 'R' && !serverCertRequest) + // Unwrap license response + if (!isCertRequest && m_cdmAdapter->GetKeySystem() == DRM::KS_WIDEVINE) { - if (blocks[3][0] == 'J' || (blocks[3].size() > 1 && blocks[3][0] == 'B' && blocks[3][1] == 'J')) - { - int dataPos = 2; - - if (response.size() >= 3 && blocks[3][0] == 'B') - { - response = BASE64::DecodeToStr(response); - dataPos = 3; - } - - jsmn_parser jsn; - jsmntok_t tokens[256]; - - jsmn_init(&jsn); - int i(0), numTokens = jsmn_parse(&jsn, response.c_str(), response.size(), tokens, 256); - - std::vector jsonVals{STRING::SplitToVec(blocks[3].substr(dataPos), ';')}; - - // Find HDCP limit - if (jsonVals.size() > 1) - { - for (; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && - jsonVals[1].size() == static_cast(tokens[i].end - tokens[i].start) && - strncmp(response.c_str() + tokens[i].start, jsonVals[1].c_str(), - tokens[i].end - tokens[i].start) == 0) - break; - if (i < numTokens) - m_hdcpLimit = std::atoi((response.c_str() + tokens[i + 1].start)); - } - // Find license key - if (jsonVals.size() > 0) - { - for (i = 0; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && - jsonVals[0].size() == static_cast(tokens[i].end - tokens[i].start) && - strncmp(response.c_str() + tokens[i].start, jsonVals[0].c_str(), - tokens[i].end - tokens[i].start) == 0) - { - if (i + 1 < numTokens && tokens[i + 1].type == JSMN_ARRAY && tokens[i + 1].size == 1) - ++i; - break; - } - } - else - i = numTokens; - - if (i < numTokens) - { - std::string respData{ - response.substr(tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start)}; - - if (blocks[3][dataPos - 1] == 'B') - { - respData = BASE64::DecodeToStr(respData); - } - - m_cdmAdapter->GetCDM()->UpdateSession( - ++m_promiseId, m_strSession.data(), m_strSession.size(), - reinterpret_cast(respData.c_str()), respData.size()); - } - else - { - LOG::LogF(LOGERROR, "Unable to find %s in JSON string", blocks[3].c_str() + 2); - return false; - } - } - else if (blocks[3][0] == 'H' && blocks[3].size() >= 2) + std::string unwrappedData; + // Some services have a customized license server that require data to be wrapped with their formats (e.g. JSON). + // Here we provide a built-in way to unwrap the license data received, this avoid force add-ons to integrate + // a HTTP server proxy to manage the license data request/response, and so use Kodi properties to set wrappers. + if (!DRM::WvUnwrapLicense(licConfig.unwrapper, licConfig.unwrapperParams, respContentType, + respData, unwrappedData, hdcpLimit)) { - //Find the payload - std::string::size_type payloadPos = response.find("\r\n\r\n"); - if (payloadPos != std::string::npos) - { - payloadPos += 4; - if (blocks[3][1] == 'B') - m_cdmAdapter->GetCDM()->UpdateSession( - ++m_promiseId, m_strSession.data(), m_strSession.size(), - reinterpret_cast(response.c_str() + payloadPos), - response.size() - payloadPos); - else - { - LOG::LogF(LOGERROR, "Unsupported HTTP payload data type definition"); - return false; - } - } - else - { - LOG::LogF(LOGERROR, "Unable to find HTTP payload in response"); - return false; - } - } - else if (blocks[3][0] == 'B' && blocks[3].size() == 1) - { - std::string decRespData{BASE64::DecodeToStr(response)}; - - m_cdmAdapter->GetCDM()->UpdateSession( - ++m_promiseId, m_strSession.data(), m_strSession.size(), - reinterpret_cast(decRespData.c_str()), decRespData.size()); - } - else - { - LOG::LogF(LOGERROR, "Unsupported License request template (response)"); return false; } + respData = unwrappedData; } - else // its binary - simply push the returned data as update + + if (!isCertRequest && CSrvBroker::GetSettings().IsDebugLicense()) { - m_cdmAdapter->GetCDM()->UpdateSession( - ++m_promiseId, m_strSession.data(), m_strSession.size(), - reinterpret_cast(response.data()), response.size()); + std::string debugFilePath = FILESYS::PathCombine( + m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"); + FILESYS::SaveFile(debugFilePath, respData, true); } + m_cdmAdapter->GetCDM()->UpdateSession(++m_promiseId, m_strSession.data(), m_strSession.size(), + reinterpret_cast(respData.c_str()), + respData.size()); + if (m_keys.empty()) { LOG::LogF(LOGERROR, "License update not successful (no keys)"); diff --git a/src/decrypters/widevine/WVDecrypter.cpp b/src/decrypters/widevine/WVDecrypter.cpp index 3f9c5deeb..18c74b4c1 100644 --- a/src/decrypters/widevine/WVDecrypter.cpp +++ b/src/decrypters/widevine/WVDecrypter.cpp @@ -83,23 +83,20 @@ std::vector CWVDecrypter::SelectKeySystems(std::string_view ke return keySystems; } -bool CWVDecrypter::OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) +bool CWVDecrypter::OpenDRMSystem(const DRM::Config& config) { - if (licenseURL.empty()) + if (config.license.serverUrl.empty()) { - LOG::LogF(LOGERROR, "License Key property cannot be empty"); + LOG::LogF(LOGERROR, "The DRM license server url has not been specified"); return false; } - m_WVCdmAdapter = std::make_shared(licenseURL, serverCertificate, config, this); + m_WVCdmAdapter = std::make_shared(config, this); return m_WVCdmAdapter->GetCDM() != nullptr; } std::shared_ptr CWVDecrypter::CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId, std::string_view licenseUrl, bool skipSessionMessage, diff --git a/src/decrypters/widevine/WVDecrypter.h b/src/decrypters/widevine/WVDecrypter.h index 6281c159d..f18964305 100644 --- a/src/decrypters/widevine/WVDecrypter.h +++ b/src/decrypters/widevine/WVDecrypter.h @@ -6,7 +6,7 @@ * See LICENSES/README.md for more information. */ -#include "../IDecrypter.h" +#include "decrypters/IDecrypter.h" class CWVCdmAdapter; class CWVCencSingleSampleDecrypter; @@ -20,12 +20,9 @@ class ATTR_DLL_LOCAL CWVDecrypter : public DRM::IDecrypter virtual bool Initialize() override; virtual std::vector SelectKeySystems(std::string_view keySystem) override; - virtual bool OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) override; + virtual bool OpenDRMSystem(const DRM::Config& config) override; virtual std::shared_ptr CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId, std::string_view licenseUrl, bool skipSessionMessage, diff --git a/src/decrypters/widevine/jsmn.c b/src/decrypters/widevine/jsmn.c deleted file mode 100644 index 77c9b1f08..000000000 --- a/src/decrypters/widevine/jsmn.c +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2017 peak3d (http://www.peak3d.de) - * This file is part of Kodi - https://kodi.tv - * - * SPDX-License-Identifier: GPL-2.0-or-later - * See LICENSES/README.md for more information. - */ - -#include "jsmn.h" - -/** - * Allocates a fresh unused token from the token pull. - */ -static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, - jsmntok_t *tokens, size_t num_tokens) { - jsmntok_t *tok; - if (parser->toknext >= num_tokens) { - return NULL; - } - tok = &tokens[parser->toknext++]; - tok->start = tok->end = -1; - tok->size = 0; -#ifdef JSMN_PARENT_LINKS - tok->parent = -1; -#endif - return tok; -} - -/** - * Fills token type and boundaries. - */ -static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, - int start, int end) { - token->type = type; - token->start = start; - token->end = end; - token->size = 0; -} - -/** - * Fills next available token with JSON primitive. - */ -static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, - size_t len, jsmntok_t *tokens, size_t num_tokens) { - jsmntok_t *token; - int start; - - start = parser->pos; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - switch (js[parser->pos]) { -#ifndef JSMN_STRICT - /* In strict mode primitive must be followed by "," or "}" or "]" */ - case ':': -#endif - case '\t' : case '\r' : case '\n' : case ' ' : - case ',' : case ']' : case '}' : - goto found; - } - if (js[parser->pos] < 32 || js[parser->pos] >= 127) { - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } -#ifdef JSMN_STRICT - /* In strict mode primitive must be followed by a comma/object/array */ - parser->pos = start; - return JSMN_ERROR_PART; -#endif - -found: - if (tokens == NULL) { - parser->pos--; - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - parser->pos--; - return 0; -} - -/** - * Fills next token with JSON string. - */ -static int jsmn_parse_string(jsmn_parser *parser, const char *js, - size_t len, jsmntok_t *tokens, size_t num_tokens) { - jsmntok_t *token; - - int start = parser->pos; - - parser->pos++; - - /* Skip starting quote */ - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c = js[parser->pos]; - - /* Quote: end of string */ - if (c == '\"') { - if (tokens == NULL) { - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - return 0; - } - - /* Backslash: Quoted symbol expected */ - if (c == '\\' && parser->pos + 1 < len) { - int i; - parser->pos++; - switch (js[parser->pos]) { - /* Allowed escaped symbols */ - case '\"': case '/' : case '\\' : case 'b' : - case 'f' : case 'r' : case 'n' : case 't' : - break; - /* Allows escaped symbol \uXXXX */ - case 'u': - parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { - /* If it isn't a hex character we have an error */ - if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ - (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ - (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - parser->pos++; - } - parser->pos--; - break; - /* Unexpected symbol */ - default: - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } - } - parser->pos = start; - return JSMN_ERROR_PART; -} - -/** - * Parse JSON string and fill tokens. - */ -int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, - jsmntok_t *tokens, unsigned int num_tokens) { - int r; - int i; - jsmntok_t *token; - int count = parser->toknext; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c; - jsmntype_t type; - - c = js[parser->pos]; - switch (c) { - case '{': case '[': - count++; - if (tokens == NULL) { - break; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) - return JSMN_ERROR_NOMEM; - if (parser->toksuper != -1) { - tokens[parser->toksuper].size++; -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - } - token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); - token->start = parser->pos; - parser->toksuper = parser->toknext - 1; - break; - case '}': case ']': - if (tokens == NULL) - break; - type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); -#ifdef JSMN_PARENT_LINKS - if (parser->toknext < 1) { - return JSMN_ERROR_INVAL; - } - token = &tokens[parser->toknext - 1]; - for (;;) { - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - token->end = parser->pos + 1; - parser->toksuper = token->parent; - break; - } - if (token->parent == -1) { - break; - } - token = &tokens[token->parent]; - } -#else - for (i = parser->toknext - 1; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - parser->toksuper = -1; - token->end = parser->pos + 1; - break; - } - } - /* Error if unmatched closing bracket */ - if (i == -1) return JSMN_ERROR_INVAL; - for (; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - parser->toksuper = i; - break; - } - } -#endif - break; - case '\"': - r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if (r < 0) return r; - count++; - if (parser->toksuper != -1 && tokens != NULL) - tokens[parser->toksuper].size++; - break; - case '\t' : case '\r' : case '\n' : case ' ': - break; - case ':': - parser->toksuper = parser->toknext - 1; - break; - case ',': - if (tokens != NULL && parser->toksuper != -1 && - tokens[parser->toksuper].type != JSMN_ARRAY && - tokens[parser->toksuper].type != JSMN_OBJECT) { -#ifdef JSMN_PARENT_LINKS - parser->toksuper = tokens[parser->toksuper].parent; -#else - for (i = parser->toknext - 1; i >= 0; i--) { - if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { - if (tokens[i].start != -1 && tokens[i].end == -1) { - parser->toksuper = i; - break; - } - } - } -#endif - } - break; -#ifdef JSMN_STRICT - /* In strict mode primitives are: numbers and booleans */ - case '-': case '0': case '1' : case '2': case '3' : case '4': - case '5': case '6': case '7' : case '8': case '9': - case 't': case 'f': case 'n' : - /* And they must not be keys of the object */ - if (tokens != NULL && parser->toksuper != -1) { - jsmntok_t *t = &tokens[parser->toksuper]; - if (t->type == JSMN_OBJECT || - (t->type == JSMN_STRING && t->size != 0)) { - return JSMN_ERROR_INVAL; - } - } -#else - /* In non-strict mode every unquoted value is a primitive */ - default: -#endif - r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if (r < 0) return r; - count++; - if (parser->toksuper != -1 && tokens != NULL) - tokens[parser->toksuper].size++; - break; - -#ifdef JSMN_STRICT - /* Unexpected char in strict mode */ - default: - return JSMN_ERROR_INVAL; -#endif - } - } - - if (tokens != NULL) { - for (i = parser->toknext - 1; i >= 0; i--) { - /* Unmatched opened object or array */ - if (tokens[i].start != -1 && tokens[i].end == -1) { - return JSMN_ERROR_PART; - } - } - } - - return count; -} - -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -void jsmn_init(jsmn_parser *parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - diff --git a/src/decrypters/widevine/jsmn.h b/src/decrypters/widevine/jsmn.h deleted file mode 100644 index d187da53c..000000000 --- a/src/decrypters/widevine/jsmn.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2017 peak3d (http://www.peak3d.de) - * This file is part of Kodi - https://kodi.tv - * - * SPDX-License-Identifier: GPL-2.0-or-later - * See LICENSES/README.md for more information. - */ - -#ifndef __JSMN_H_ -#define __JSMN_H_ - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1, - JSMN_ARRAY = 2, - JSMN_STRING = 3, - JSMN_PRIMITIVE = 4 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * @param type type (object, array, string etc.) - * @param start start position in JSON data string - * @param end end position in JSON data string - */ -typedef struct { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string - */ -typedef struct { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -void jsmn_init(jsmn_parser *parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each describing - * a single JSON object. - */ -int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, - jsmntok_t *tokens, unsigned int num_tokens); - -#ifdef __cplusplus -} -#endif - -#endif /* __JSMN_H_ */ diff --git a/src/decrypters/widevineandroid/CMakeLists.txt b/src/decrypters/widevineandroid/CMakeLists.txt index a0fa971c8..e97b0a445 100644 --- a/src/decrypters/widevineandroid/CMakeLists.txt +++ b/src/decrypters/widevineandroid/CMakeLists.txt @@ -2,14 +2,12 @@ set(SOURCES WVCencSingleSampleDecrypter.cpp WVCdmAdapter.cpp WVDecrypter.cpp - jsmn.c ) set(HEADERS WVCencSingleSampleDecrypter.h WVCdmAdapter.h WVDecrypter.h - jsmn.h ) add_dir_sources(SOURCES HEADERS) diff --git a/src/decrypters/widevineandroid/WVCdmAdapter.cpp b/src/decrypters/widevineandroid/WVCdmAdapter.cpp index 21c57a7ac..acc1d6b4d 100644 --- a/src/decrypters/widevineandroid/WVCdmAdapter.cpp +++ b/src/decrypters/widevineandroid/WVCdmAdapter.cpp @@ -37,21 +37,14 @@ void CMediaDrmOnEventListener::onEvent(const jni::CJNIMediaDrm& mediaDrm, } CWVCdmAdapterA::CWVCdmAdapterA(std::string_view keySystem, - std::string_view licenseURL, - const std::vector& serverCert, + const DRM::Config& config, std::shared_ptr jniClassLoader, CWVDecrypterA* host) - : m_keySystem(keySystem), m_licenseUrl(licenseURL), m_host(host) + : m_keySystem(keySystem), m_config(config), m_host(host) { - if (licenseURL.empty()) - { - LOG::LogF(LOGERROR, "No license URL path specified"); - return; - } - // The license url come from license_key kodi property // we have to kept only the url without the parameters specified after pipe "|" char - std::string licUrl = m_licenseUrl; + std::string licUrl = m_config.license.serverUrl; const size_t urlPipePos = licUrl.find('|'); if (urlPipePos != std::string::npos) licUrl.erase(urlPipePos); @@ -107,9 +100,9 @@ CWVCdmAdapterA::CWVCdmAdapterA(std::string_view keySystem, if (m_keySystem == DRM::KS_WIDEVINE) { //m_cdmAdapter->setPropertyString("sessionSharing", "enable"); - if (!serverCert.empty()) + if (!m_config.license.serverCert.empty()) { - m_cdmAdapter->setPropertyByteArray("serviceCertificate", serverCert); + m_cdmAdapter->setPropertyByteArray("serviceCertificate", m_config.license.serverCert); } else LoadServiceCertificate(); @@ -126,17 +119,6 @@ CWVCdmAdapterA::CWVCdmAdapterA(std::string_view keySystem, LOG::Log(LOGDEBUG, "MediaDrm initialized (Device unique ID size: %zu, System ID: %s, Security level: %s)", strDeviceId.size(), strSystemId.c_str(), strSecurityLevel.c_str()); - - if (m_licenseUrl.find('|') == std::string::npos) - { - if (m_keySystem == DRM::KS_WIDEVINE) - m_licenseUrl += "|Content-Type=application%2Foctet-stream|R{SSM}|"; - else if (m_keySystem == DRM::KS_PLAYREADY) - m_licenseUrl += "|Content-Type=text%2Fxml&SOAPAction=http%3A%2F%2Fschemas.microsoft.com%" - "2FDRM%2F2007%2F03%2Fprotocols%2FAcquireLicense|R{SSM}|"; - else - m_licenseUrl += "|Content-Type=application/json|R{SSM}|"; - } } CWVCdmAdapterA::~CWVCdmAdapterA() @@ -246,6 +228,11 @@ void CWVCdmAdapterA::SaveServiceCertificate() } } +const DRM::Config& CWVCdmAdapterA::GetConfig() +{ + return m_config; +} + std::string_view CWVCdmAdapterA::GetKeySystem() { return m_keySystem; diff --git a/src/decrypters/widevineandroid/WVCdmAdapter.h b/src/decrypters/widevineandroid/WVCdmAdapter.h index 7b1682f2f..fb77199a8 100644 --- a/src/decrypters/widevineandroid/WVCdmAdapter.h +++ b/src/decrypters/widevineandroid/WVCdmAdapter.h @@ -9,6 +9,7 @@ #pragma once #include "decrypters/HelperWv.h" +#include "decrypters/IDecrypter.h" #include #include @@ -66,8 +67,7 @@ class ATTR_DLL_LOCAL CWVCdmAdapterA : public CMediaDrmOnEventCallback, { public: CWVCdmAdapterA(std::string_view keySystem, - std::string_view licenseURL, - const std::vector& serverCert, + const DRM::Config& config, std::shared_ptr jniClassLoader, CWVDecrypterA* host); ~CWVCdmAdapterA(); @@ -75,7 +75,7 @@ class ATTR_DLL_LOCAL CWVCdmAdapterA : public CMediaDrmOnEventCallback, // IWVCdmAdapter interface methods std::shared_ptr GetCDM() override { return m_cdmAdapter; } - const std::string& GetLicenseUrl() override { return m_licenseUrl; } + const DRM::Config& GetConfig() override; std::string_view GetKeySystem() override; std::string_view GetLibraryPath() const override; void SaveServiceCertificate() override; @@ -96,13 +96,13 @@ class ATTR_DLL_LOCAL CWVCdmAdapterA : public CMediaDrmOnEventCallback, int extra, const std::vector& data) override; + DRM::Config m_config; std::shared_ptr m_cdmAdapter; std::list m_observers; std::mutex m_observer_mutex; std::string m_keySystem; std::unique_ptr m_mediaDrmEventListener; - std::string m_licenseUrl; std::string m_strBasePath; CWVDecrypterA* m_host; }; diff --git a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp index 0545afb58..f682329e1 100644 --- a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp +++ b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp @@ -12,7 +12,6 @@ #include "SrvBroker.h" #include "decrypters/HelperWv.h" #include "decrypters/Helpers.h" -#include "jsmn.h" #include "utils/Base64Utils.h" #include "utils/CurlUtils.h" #include "utils/DigestMD5Utils.h" @@ -30,7 +29,6 @@ using namespace UTILS; CWVCencSingleSampleDecrypterA::CWVCencSingleSampleDecrypterA( IWVCdmAdapter* cdmAdapter, std::vector& pssh, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId) : m_cdmAdapter(cdmAdapter), m_pssh(pssh), @@ -53,16 +51,23 @@ CWVCencSingleSampleDecrypterA::CWVCencSingleSampleDecrypterA( if (CSrvBroker::GetSettings().IsDebugLicense()) { - std::string debugFilePath = - FILESYS::PathCombine(m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.init"); + std::string fileName = + STRING::ToUpper(DRM::KeySystemToUUIDstr(m_cdmAdapter->GetKeySystem())) + ".init"; + std::string debugFilePath = FILESYS::PathCombine(m_cdmAdapter->GetLibraryPath(), fileName); std::string data{reinterpret_cast(m_pssh.data()), m_pssh.size()}; FILESYS::SaveFile(debugFilePath, data, true); } m_initialPssh = m_pssh; - if (!optionalKeyParameter.empty()) - m_optParams["PRCustomData"] = optionalKeyParameter; + if (m_cdmAdapter->GetKeySystem() == DRM::KS_PLAYREADY) + { + for (auto& [keyName, keyValue] : m_cdmAdapter->GetConfig().optKeyReqParams) + { + if (keyName == "custom_data") + m_optParams["PRCustomData"] = keyValue; + } + } /* std::vector pui = m_cdmAdapter->GetCDM()->getPropertyByteArray("provisioningUniqueId"); @@ -159,7 +164,7 @@ const char* CWVCencSingleSampleDecrypterA::GetSessionId() return m_sessionId.c_str(); } -std::vector CWVCencSingleSampleDecrypterA::GetChallengeData() +std::vector CWVCencSingleSampleDecrypterA::GetChallengeData() { return m_keyRequestData; } @@ -218,13 +223,13 @@ bool CWVCencSingleSampleDecrypterA::ProvisionRequest() return false; } - std::vector provData = request.getData(); + std::vector provData = request.getData(); std::string url = request.getDefaultUrl(); LOG::Log(LOGDEBUG, "Provision data size: %lu, url: %s", provData.size(), url.c_str()); std::string reqData("{\"signedRequest\":\""); - reqData += std::string(provData.data(), provData.size()); + reqData += std::string(provData.cbegin(), provData.cend()); reqData += "\"}"; reqData = BASE64::Encode(reqData); @@ -259,7 +264,7 @@ bool CWVCencSingleSampleDecrypterA::ProvisionRequest() return true; } -bool CWVCencSingleSampleDecrypterA::GetKeyRequest(std::vector& keyRequestData) +bool CWVCencSingleSampleDecrypterA::GetKeyRequest(std::vector& keyRequestData) { jni::CJNIMediaDrmKeyRequest keyRequest = m_cdmAdapter->GetCDM()->getKeyRequest( m_sessionIdVec, m_pssh, "video/mp4", jni::CJNIMediaDrm::KEY_TYPE_STREAMING, m_optParams); @@ -330,394 +335,142 @@ bool CWVCencSingleSampleDecrypterA::KeyUpdateRequest(bool waitKeys, bool skipSes return true; } -bool CWVCencSingleSampleDecrypterA::SendSessionMessage(const std::vector& keyRequestData) +bool CWVCencSingleSampleDecrypterA::SendSessionMessage(const std::vector& challenge) { - std::vector blocks{STRING::SplitToVec(m_cdmAdapter->GetLicenseUrl(), '|')}; - - if (blocks.size() != 4) - { - LOG::LogF(LOGERROR, "Wrong \"|\" blocks in license URL. Four blocks (req | header | body | " - "response) are expected in license URL"); - return false; - } - if (CSrvBroker::GetSettings().IsDebugLicense()) { - std::string debugFilePath = FILESYS::PathCombine( - m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.challenge"); - FILESYS::SaveFile(debugFilePath, keyRequestData.data(), true); + std::string fileName = + STRING::ToUpper(DRM::KeySystemToUUIDstr(m_cdmAdapter->GetKeySystem())) + ".challenge"; + std::string debugFilePath = FILESYS::PathCombine(m_cdmAdapter->GetLibraryPath(), fileName); + UTILS::FILESYS::SaveFile(debugFilePath, reinterpret_cast(challenge.data()), true); } - //Process placeholder in GET String - std::string::size_type insPos(blocks[0].find("{SSM}")); - if (insPos != std::string::npos) + const DRM::Config drmCfg = m_cdmAdapter->GetConfig(); + const DRM::Config::License& licConfig = drmCfg.license; + std::string reqData; + + if (!licConfig.isHttpGetRequest) // Make HTTP POST request { - if (insPos > 0 && blocks[0][insPos - 1] == 'B') + if (licConfig.reqData.empty()) // By default add raw challenge { - std::string msgEncoded = BASE64::Encode(keyRequestData); - msgEncoded = STRING::URLEncode(msgEncoded); - blocks[0].replace(insPos - 1, 6, msgEncoded); + reqData.assign(challenge.cbegin(), challenge.cend()); } else { - LOG::Log(LOGERROR, "Unsupported License request template (command)"); - return false; - } - } - - insPos = blocks[0].find("{HASH}"); - if (insPos != std::string::npos) - { - DIGEST::MD5 md5; - md5.Update(keyRequestData.data(), static_cast(keyRequestData.size())); - md5.Finalize(); - blocks[0].replace(insPos, 6, md5.HexDigest()); - } - - CURL::CUrl file(blocks[0]); - std::string response; - std::string resLimit; - std::string contentType; - - //Process headers - std::vector headers{STRING::SplitToVec(blocks[1], '&')}; - for (std::string& headerStr : headers) - { - std::vector header{STRING::SplitToVec(headerStr, '=')}; - if (!header.empty()) - { - STRING::Trim(header[0]); - std::string value; - if (header.size() > 1) + if (BASE64::IsValidBase64(licConfig.reqData)) + reqData = BASE64::DecodeToStr(licConfig.reqData); + else //! @todo: this fallback as plain text must be removed when the deprecated DRM properties are removed, and so replace it to return error + reqData = licConfig.reqData; + + // Some services have a customized license server that require data to be wrapped with their formats (e.g. JSON). + // Here we provide a built-in way to customize the license data to be sent, this avoid force add-ons to integrate + // an HTTP server proxy to manage the license data request/response, and so use Kodi properties to set wrappers. + if (m_cdmAdapter->GetKeySystem() == DRM::KS_WIDEVINE && + !DRM::WvWrapLicense(reqData, challenge, m_sessionId, m_defaultKeyId, m_pssh, + licConfig.wrapper, drmCfg.isNewConfig)) { - STRING::Trim(header[1]); - value = STRING::URLDecode(header[1]); + return false; } - file.AddHeader(header[0], value); } } - //Process body - if (!blocks[2].empty()) + if (CSrvBroker::GetSettings().IsDebugLicense()) { - if (blocks[2][0] == '%') - blocks[2] = STRING::URLDecode(blocks[2]); - - insPos = blocks[2].find("{SSM}"); - if (insPos != std::string::npos) - { - std::string::size_type sidPos(blocks[2].find("{SID}")); - std::string::size_type kidPos(blocks[2].find("{KID}")); - std::string::size_type psshPos(blocks[2].find("{PSSH}")); - - char fullDecode = 0; - if (insPos > 1 && sidPos > 1 && kidPos > 1 && (blocks[2][0] == 'b' || blocks[2][0] == 'B') && - blocks[2][1] == '{') - { - fullDecode = blocks[2][0]; - blocks[2] = blocks[2].substr(2, blocks[2].size() - 3); - insPos -= 2; - if (kidPos != std::string::npos) - kidPos -= 2; - if (sidPos != std::string::npos) - sidPos -= 2; - if (psshPos != std::string::npos) - psshPos -= 2; - } - - size_t sizeWritten(0); - - if (insPos > 0) - { - if (blocks[2][insPos - 1] == 'B' || blocks[2][insPos - 1] == 'b') - { - std::string msgEncoded = BASE64::Encode(keyRequestData); - if (blocks[2][insPos - 1] == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2].replace(insPos - 1, 6, msgEncoded); - sizeWritten = msgEncoded.size(); - } - else if (blocks[2][insPos - 1] == 'D') - { - std::string msgEncoded{STRING::ToDecimal( - reinterpret_cast(keyRequestData.data()), keyRequestData.size())}; - blocks[2].replace(insPos - 1, 6, msgEncoded); - sizeWritten = msgEncoded.size(); - } - else - { - blocks[2].replace(insPos - 1, 6, keyRequestData.data(), keyRequestData.size()); - sizeWritten = keyRequestData.size(); - } - } - else - { - LOG::Log(LOGERROR, "Unsupported License request template (body / ?{SSM})"); - return false; - } - - if (sidPos != std::string::npos && insPos < sidPos) - sidPos += sizeWritten, sidPos -= 6; - - if (kidPos != std::string::npos && insPos < kidPos) - kidPos += sizeWritten, kidPos -= 6; - - if (psshPos != std::string::npos && insPos < psshPos) - psshPos += sizeWritten, psshPos -= 6; - - sizeWritten = 0; - - if (sidPos != std::string::npos) - { - if (sidPos > 0) - { - if (blocks[2][sidPos - 1] == 'B' || blocks[2][sidPos - 1] == 'b') - { - std::string msgEncoded = BASE64::Encode(m_sessionId); - if (blocks[2][sidPos - 1] == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2].replace(sidPos - 1, 6, msgEncoded); - sizeWritten = msgEncoded.size(); - } - else - { - blocks[2].replace(sidPos - 1, 6, m_sessionId.data(), m_sessionId.size()); - sizeWritten = m_sessionId.size(); - } - } - else - { - LOG::Log(LOGERROR, "Unsupported License request template (body / ?{SID})"); - return false; - } - } - - if (kidPos != std::string::npos && sidPos < kidPos) - kidPos += sizeWritten, kidPos -= 6; - - if (psshPos != std::string::npos && sidPos < psshPos) - psshPos += sizeWritten, psshPos -= 6; - - size_t kidPlaceholderLen = 6; - if (kidPos != std::string::npos) - { - if (blocks[2][kidPos - 1] == 'H') - { - std::string keyIdUUID{STRING::ToHexadecimal(m_defaultKeyId)}; - blocks[2].replace(kidPos - 1, 6, keyIdUUID.c_str(), 32); - } - else - { - std::string kidUUID{DRM::ConvertKidBytesToUUID(m_defaultKeyId)}; - blocks[2].replace(kidPos, 5, kidUUID.c_str(), 36); - kidPlaceholderLen = 5; - } - } - - if (psshPos != std::string::npos && kidPos < psshPos) - psshPos += sizeWritten, psshPos -= kidPlaceholderLen; - - if (psshPos != std::string::npos) - { - std::string msgEncoded = BASE64::Encode(m_initialPssh); - if (blocks[2][psshPos - 1] == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2].replace(psshPos - 1, 7, msgEncoded); - sizeWritten = msgEncoded.size(); - } + std::string fileName = + STRING::ToUpper(DRM::KeySystemToUUIDstr(m_cdmAdapter->GetKeySystem())) + ".request"; + std::string debugFilePath = FILESYS::PathCombine(m_cdmAdapter->GetLibraryPath(), fileName); + UTILS::FILESYS::SaveFile(debugFilePath, reqData, true); + } - if (fullDecode) - { - std::string msgEncoded = BASE64::Encode(blocks[2]); - if (fullDecode == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2] = msgEncoded; - } + std::string url = licConfig.serverUrl; + DRM::TranslateLicenseUrlPh(url, challenge, drmCfg.isNewConfig); - if (CSrvBroker::GetSettings().IsDebugLicense()) - { - std::string debugFilePath = FILESYS::PathCombine( - m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.postdata"); - FILESYS::SaveFile(debugFilePath, blocks[2], true); - } - } + CURL::CUrl cUrl{url, reqData}; + cUrl.AddHeaders(licConfig.reqHeaders); - std::string encData{BASE64::Encode(blocks[2])}; - file.AddHeader("postdata", encData.c_str()); - } + const int statusCode = cUrl.Open(); - int statusCode = file.Open(); if (statusCode == -1 || statusCode >= 400) { LOG::Log(LOGERROR, "License server returned failure (HTTP error %i)", statusCode); return false; } - CURL::ReadStatus downloadStatus = CURL::ReadStatus::CHUNK_READ; - while (downloadStatus == CURL::ReadStatus::CHUNK_READ) + std::string respData; + if (cUrl.Read(respData) == CURL::ReadStatus::ERROR) { - downloadStatus = file.Read(response); + LOG::LogF(LOGERROR, "Cannot read license server response"); + return false; } - resLimit = file.GetResponseHeader("X-Limit-Video"); - contentType = file.GetResponseHeader("Content-Type"); + const std::string resLimit = cUrl.GetResponseHeader("X-Limit-Video"); // Custom header + const std::string respContentType = cUrl.GetResponseHeader("Content-Type"); if (!resLimit.empty()) { - std::string::size_type posMax = resLimit.find("max="); + // To force limit playable streams resolutions + size_t posMax = resLimit.find("max="); if (posMax != std::string::npos) m_resolutionLimit = std::atoi(resLimit.data() + (posMax + 4)); } - if (downloadStatus == CURL::ReadStatus::ERROR) - { - LOG::LogF(LOGERROR, "Could not read full SessionMessage response"); - return false; - } - else if (response.empty()) - { - LOG::LogF(LOGERROR, "Empty SessionMessage response - invalid"); - return false; - } + // The first request could be the license certificate request + // this request is done by sending a challenge of 2 bytes, 0x08 0x04 (CAQ=) + const bool isCertRequest = challenge.size() == 2 && challenge[0] == 0x08 && challenge[1] == 0x04; - if (m_cdmAdapter->GetKeySystem() == DRM::KS_PLAYREADY && - response.find("") == std::string::npos) - { - std::string::size_type dstPos(response.find("")); - std::string challenge(keyRequestData.data(), keyRequestData.size()); - std::string::size_type srcPosS(challenge.find("")); - if (dstPos != std::string::npos && srcPosS != std::string::npos) - { - LOG::Log(LOGDEBUG, "Inserting "); - std::string::size_type srcPosE(challenge.find("", srcPosS)); - if (srcPosE != std::string::npos) - response.insert(dstPos + 11, challenge.c_str() + srcPosS, srcPosE - srcPosS + 15); - } - } - - if (CSrvBroker::GetSettings().IsDebugLicense()) - { - std::string debugFilePath = FILESYS::PathCombine( - m_cdmAdapter->GetLibraryPath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"); - FILESYS::SaveFile(debugFilePath, response, true); - } + int hdcpLimit{0}; - if (!blocks[3].empty() && blocks[3][0] != 'R' && - (keyRequestData.size() > 2 || - contentType.find("application/octet-stream") == std::string::npos)) + if (!isCertRequest) { - if (blocks[3][0] == 'J' || (blocks[3].size() > 1 && blocks[3][0] == 'B' && blocks[3][1] == 'J')) + // Unwrap license response + if (m_cdmAdapter->GetKeySystem() == DRM::KS_WIDEVINE) { - int dataPos = 2; - - if (response.size() >= 3 && blocks[3][0] == 'B') + std::string unwrappedData; + // Some services have a customized license server that require data to be wrapped with their formats (e.g. JSON). + // Here we provide a built-in way to unwrap the license data received, this avoid force add-ons to integrate + // a HTTP server proxy to manage the license data request/response, and so use Kodi properties to set wrappers. + if (!DRM::WvUnwrapLicense(licConfig.unwrapper, licConfig.unwrapperParams, respContentType, + respData, unwrappedData, hdcpLimit)) { - response = BASE64::DecodeToStr(response); - dataPos = 3; - } - - jsmn_parser jsn; - jsmntok_t tokens[256]; - - jsmn_init(&jsn); - int i(0), numTokens = jsmn_parse(&jsn, response.c_str(), response.size(), tokens, 256); - - std::vector jsonVals{STRING::SplitToVec(blocks[3].substr(dataPos), ';')}; - - // Find HDCP limit - if (jsonVals.size() > 1) - { - for (; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && - jsonVals[1].size() == static_cast(tokens[i].end - tokens[i].start) && - strncmp(response.c_str() + tokens[i].start, jsonVals[1].c_str(), - tokens[i].end - tokens[i].start) == 0) - break; - if (i < numTokens) - m_hdcpLimit = atoi((response.c_str() + tokens[i + 1].start)); - } - // Find license key - if (jsonVals.size() > 0) - { - for (i = 0; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && - jsonVals[0].size() == static_cast(tokens[i].end - tokens[i].start) && - strncmp(response.c_str() + tokens[i].start, jsonVals[0].c_str(), - tokens[i].end - tokens[i].start) == 0) - { - if (i + 1 < numTokens && tokens[i + 1].type == JSMN_ARRAY && tokens[i + 1].size == 1) - ++i; - break; - } - } - else - i = numTokens; - - if (i < numTokens) - { - response = response.substr(tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - - if (blocks[3][dataPos - 1] == 'B') - { - response = BASE64::DecodeToStr(response); - } - } - else - { - LOG::LogF(LOGERROR, "Unable to find %s in JSON string", blocks[3].c_str() + 2); return false; } + respData = unwrappedData; } - else if (blocks[3][0] == 'H' && blocks[3].size() >= 2) + + if (m_cdmAdapter->GetKeySystem() == DRM::KS_PLAYREADY && + respData.find("") == std::string::npos) { - //Find the payload - std::string::size_type payloadPos = response.find("\r\n\r\n"); - if (payloadPos != std::string::npos) + size_t dstPos = respData.find(""); + std::string challengeStr(challenge.cbegin(), challenge.cend()); + size_t srcPosS = challengeStr.find(""); + if (dstPos != std::string::npos && srcPosS != std::string::npos) { - payloadPos += 4; - if (blocks[3][1] == 'B') - response = std::string(response.c_str() + payloadPos, response.c_str() + response.size()); - else - { - LOG::LogF(LOGERROR, "Unsupported HTTP payload data type definition"); - return false; - } - } - else - { - LOG::LogF(LOGERROR, "Unable to find HTTP payload in response"); - return false; + LOG::Log(LOGDEBUG, "Injecting missing PlayReady tag to license response"); + size_t srcPosE = challengeStr.find("", srcPosS); + if (srcPosE != std::string::npos) + respData.insert(dstPos + 11, challengeStr.c_str() + srcPosS, srcPosE - srcPosS + 15); } } - else if (blocks[3][0] == 'B' && blocks[3].size() == 1) - { - response = BASE64::DecodeToStr(response); - } - else - { - LOG::LogF(LOGERROR, "Unsupported License request template (response)"); - return false; - } + } + + if (!isCertRequest && CSrvBroker::GetSettings().IsDebugLicense()) + { + std::string fileName = + STRING::ToUpper(DRM::KeySystemToUUIDstr(m_cdmAdapter->GetKeySystem())) + ".response"; + std::string debugFilePath = FILESYS::PathCombine(m_cdmAdapter->GetLibraryPath(), fileName); + FILESYS::SaveFile(debugFilePath, respData, true); } m_keySetId = m_cdmAdapter->GetCDM()->provideKeyResponse( - m_sessionIdVec, std::vector(response.data(), response.data() + response.size())); + m_sessionIdVec, std::vector(respData.data(), respData.data() + respData.size())); if (xbmc_jnienv()->ExceptionCheck()) { - LOG::LogF(LOGERROR, "provideKeyResponse has raised an exception"); + LOG::LogF(LOGERROR, "MediaDrm: provideKeyResponse has raised an exception"); xbmc_jnienv()->ExceptionClear(); return false; } - if (keyRequestData.size() == 2) + if (isCertRequest) m_cdmAdapter->SaveServiceCertificate(); LOG::Log(LOGDEBUG, "License update successful"); diff --git a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h index 5f8f0d833..81469f0bd 100644 --- a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h +++ b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h @@ -26,14 +26,13 @@ class ATTR_DLL_LOCAL CWVCencSingleSampleDecrypterA : public Adaptive_CencSingleS public: CWVCencSingleSampleDecrypterA(IWVCdmAdapter* cdmAdapter, std::vector& pssh, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId); virtual ~CWVCencSingleSampleDecrypterA(); bool StartSession(bool skipSessionMessage) { return KeyUpdateRequest(true, skipSessionMessage); }; virtual const char* GetSessionId() override; - std::vector GetChallengeData(); + std::vector GetChallengeData(); virtual bool HasLicenseKey(const std::vector& keyId); virtual AP4_Result SetFragmentInfo(AP4_UI32 poolId, @@ -73,9 +72,9 @@ class ATTR_DLL_LOCAL CWVCencSingleSampleDecrypterA : public Adaptive_CencSingleS private: bool ProvisionRequest(); - bool GetKeyRequest(std::vector& keyRequestData); + bool GetKeyRequest(std::vector& keyRequestData); bool KeyUpdateRequest(bool waitForKeys, bool skipSessionMessage); - bool SendSessionMessage(const std::vector& keyRequestData); + bool SendSessionMessage(const std::vector& challenge); IWVCdmAdapter* m_cdmAdapter; @@ -86,7 +85,7 @@ class ATTR_DLL_LOCAL CWVCencSingleSampleDecrypterA : public Adaptive_CencSingleS std::string m_sessionId; std::vector m_sessionIdVec; std::vector m_keySetId; - std::vector m_keyRequestData; + std::vector m_keyRequestData; bool m_isProvisioningRequested; bool m_isKeyUpdateRequested; diff --git a/src/decrypters/widevineandroid/WVDecrypter.cpp b/src/decrypters/widevineandroid/WVDecrypter.cpp index 0dce31a55..8f3160dd4 100644 --- a/src/decrypters/widevineandroid/WVDecrypter.cpp +++ b/src/decrypters/widevineandroid/WVDecrypter.cpp @@ -12,7 +12,6 @@ #include "WVCencSingleSampleDecrypter.h" #include "common/AdaptiveDecrypter.h" #include "decrypters/Helpers.h" -#include "jsmn.h" #include "utils/Base64Utils.h" #include "utils/log.h" @@ -77,36 +76,28 @@ std::vector CWVDecrypterA::SelectKeySystems(std::string_view k return {}; } -bool CWVDecrypterA::OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) +bool CWVDecrypterA::OpenDRMSystem(const DRM::Config& config) { - if (m_keySystem.empty()) - return false; - - if (licenseURL.empty()) + if (config.license.serverUrl.empty()) { - LOG::LogF(LOGERROR, "License Key property cannot be empty"); + LOG::LogF(LOGERROR, "The DRM license server url has not been specified"); return false; } - m_WVCdmAdapter = std::make_shared(m_keySystem, licenseURL, serverCertificate, - m_classLoader, this); + m_WVCdmAdapter = std::make_shared(m_keySystem, config, m_classLoader, this); return m_WVCdmAdapter->GetCDM() != nullptr; } std::shared_ptr CWVDecrypterA::CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId, std::string_view licenseUrl, bool skipSessionMessage, CryptoMode cryptoMode) { std::shared_ptr decrypter = - std::make_shared(m_WVCdmAdapter.get(), initData, - optionalKeyParameter, defaultKeyId); + std::make_shared(m_WVCdmAdapter.get(), initData, defaultKeyId); if (!(*decrypter->GetSessionId() && decrypter->StartSession(skipSessionMessage))) { @@ -154,7 +145,7 @@ std::string CWVDecrypterA::GetChallengeB64Data(std::shared_ptr(decrypter); if (wvDecrypter) { - const std::vector challengeData = wvDecrypter->GetChallengeData(); + const std::vector challengeData = wvDecrypter->GetChallengeData(); return BASE64::Encode(challengeData); } else diff --git a/src/decrypters/widevineandroid/WVDecrypter.h b/src/decrypters/widevineandroid/WVDecrypter.h index fdd398994..3682fc600 100644 --- a/src/decrypters/widevineandroid/WVDecrypter.h +++ b/src/decrypters/widevineandroid/WVDecrypter.h @@ -46,13 +46,10 @@ class ATTR_DLL_LOCAL CWVDecrypterA : public DRM::IDecrypter virtual std::vector SelectKeySystems(std::string_view keySystem) override; - virtual bool OpenDRMSystem(std::string_view licenseURL, - const std::vector& serverCertificate, - const uint8_t config) override; + virtual bool OpenDRMSystem(const DRM::Config& config) override; virtual std::shared_ptr CreateSingleSampleDecrypter( std::vector& initData, - std::string_view optionalKeyParameter, const std::vector& defaultKeyId, std::string_view licenseUrl, bool skipSessionMessage, diff --git a/src/decrypters/widevineandroid/jsmn.c b/src/decrypters/widevineandroid/jsmn.c deleted file mode 100644 index 77c9b1f08..000000000 --- a/src/decrypters/widevineandroid/jsmn.c +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2017 peak3d (http://www.peak3d.de) - * This file is part of Kodi - https://kodi.tv - * - * SPDX-License-Identifier: GPL-2.0-or-later - * See LICENSES/README.md for more information. - */ - -#include "jsmn.h" - -/** - * Allocates a fresh unused token from the token pull. - */ -static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, - jsmntok_t *tokens, size_t num_tokens) { - jsmntok_t *tok; - if (parser->toknext >= num_tokens) { - return NULL; - } - tok = &tokens[parser->toknext++]; - tok->start = tok->end = -1; - tok->size = 0; -#ifdef JSMN_PARENT_LINKS - tok->parent = -1; -#endif - return tok; -} - -/** - * Fills token type and boundaries. - */ -static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, - int start, int end) { - token->type = type; - token->start = start; - token->end = end; - token->size = 0; -} - -/** - * Fills next available token with JSON primitive. - */ -static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, - size_t len, jsmntok_t *tokens, size_t num_tokens) { - jsmntok_t *token; - int start; - - start = parser->pos; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - switch (js[parser->pos]) { -#ifndef JSMN_STRICT - /* In strict mode primitive must be followed by "," or "}" or "]" */ - case ':': -#endif - case '\t' : case '\r' : case '\n' : case ' ' : - case ',' : case ']' : case '}' : - goto found; - } - if (js[parser->pos] < 32 || js[parser->pos] >= 127) { - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } -#ifdef JSMN_STRICT - /* In strict mode primitive must be followed by a comma/object/array */ - parser->pos = start; - return JSMN_ERROR_PART; -#endif - -found: - if (tokens == NULL) { - parser->pos--; - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - parser->pos--; - return 0; -} - -/** - * Fills next token with JSON string. - */ -static int jsmn_parse_string(jsmn_parser *parser, const char *js, - size_t len, jsmntok_t *tokens, size_t num_tokens) { - jsmntok_t *token; - - int start = parser->pos; - - parser->pos++; - - /* Skip starting quote */ - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c = js[parser->pos]; - - /* Quote: end of string */ - if (c == '\"') { - if (tokens == NULL) { - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - return 0; - } - - /* Backslash: Quoted symbol expected */ - if (c == '\\' && parser->pos + 1 < len) { - int i; - parser->pos++; - switch (js[parser->pos]) { - /* Allowed escaped symbols */ - case '\"': case '/' : case '\\' : case 'b' : - case 'f' : case 'r' : case 'n' : case 't' : - break; - /* Allows escaped symbol \uXXXX */ - case 'u': - parser->pos++; - for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { - /* If it isn't a hex character we have an error */ - if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ - (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ - (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - parser->pos++; - } - parser->pos--; - break; - /* Unexpected symbol */ - default: - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } - } - parser->pos = start; - return JSMN_ERROR_PART; -} - -/** - * Parse JSON string and fill tokens. - */ -int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, - jsmntok_t *tokens, unsigned int num_tokens) { - int r; - int i; - jsmntok_t *token; - int count = parser->toknext; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c; - jsmntype_t type; - - c = js[parser->pos]; - switch (c) { - case '{': case '[': - count++; - if (tokens == NULL) { - break; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) - return JSMN_ERROR_NOMEM; - if (parser->toksuper != -1) { - tokens[parser->toksuper].size++; -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - } - token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); - token->start = parser->pos; - parser->toksuper = parser->toknext - 1; - break; - case '}': case ']': - if (tokens == NULL) - break; - type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); -#ifdef JSMN_PARENT_LINKS - if (parser->toknext < 1) { - return JSMN_ERROR_INVAL; - } - token = &tokens[parser->toknext - 1]; - for (;;) { - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - token->end = parser->pos + 1; - parser->toksuper = token->parent; - break; - } - if (token->parent == -1) { - break; - } - token = &tokens[token->parent]; - } -#else - for (i = parser->toknext - 1; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - parser->toksuper = -1; - token->end = parser->pos + 1; - break; - } - } - /* Error if unmatched closing bracket */ - if (i == -1) return JSMN_ERROR_INVAL; - for (; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - parser->toksuper = i; - break; - } - } -#endif - break; - case '\"': - r = jsmn_parse_string(parser, js, len, tokens, num_tokens); - if (r < 0) return r; - count++; - if (parser->toksuper != -1 && tokens != NULL) - tokens[parser->toksuper].size++; - break; - case '\t' : case '\r' : case '\n' : case ' ': - break; - case ':': - parser->toksuper = parser->toknext - 1; - break; - case ',': - if (tokens != NULL && parser->toksuper != -1 && - tokens[parser->toksuper].type != JSMN_ARRAY && - tokens[parser->toksuper].type != JSMN_OBJECT) { -#ifdef JSMN_PARENT_LINKS - parser->toksuper = tokens[parser->toksuper].parent; -#else - for (i = parser->toknext - 1; i >= 0; i--) { - if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { - if (tokens[i].start != -1 && tokens[i].end == -1) { - parser->toksuper = i; - break; - } - } - } -#endif - } - break; -#ifdef JSMN_STRICT - /* In strict mode primitives are: numbers and booleans */ - case '-': case '0': case '1' : case '2': case '3' : case '4': - case '5': case '6': case '7' : case '8': case '9': - case 't': case 'f': case 'n' : - /* And they must not be keys of the object */ - if (tokens != NULL && parser->toksuper != -1) { - jsmntok_t *t = &tokens[parser->toksuper]; - if (t->type == JSMN_OBJECT || - (t->type == JSMN_STRING && t->size != 0)) { - return JSMN_ERROR_INVAL; - } - } -#else - /* In non-strict mode every unquoted value is a primitive */ - default: -#endif - r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if (r < 0) return r; - count++; - if (parser->toksuper != -1 && tokens != NULL) - tokens[parser->toksuper].size++; - break; - -#ifdef JSMN_STRICT - /* Unexpected char in strict mode */ - default: - return JSMN_ERROR_INVAL; -#endif - } - } - - if (tokens != NULL) { - for (i = parser->toknext - 1; i >= 0; i--) { - /* Unmatched opened object or array */ - if (tokens[i].start != -1 && tokens[i].end == -1) { - return JSMN_ERROR_PART; - } - } - } - - return count; -} - -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -void jsmn_init(jsmn_parser *parser) { - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - diff --git a/src/decrypters/widevineandroid/jsmn.h b/src/decrypters/widevineandroid/jsmn.h deleted file mode 100644 index d187da53c..000000000 --- a/src/decrypters/widevineandroid/jsmn.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2017 peak3d (http://www.peak3d.de) - * This file is part of Kodi - https://kodi.tv - * - * SPDX-License-Identifier: GPL-2.0-or-later - * See LICENSES/README.md for more information. - */ - -#ifndef __JSMN_H_ -#define __JSMN_H_ - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1, - JSMN_ARRAY = 2, - JSMN_STRING = 3, - JSMN_PRIMITIVE = 4 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * @param type type (object, array, string etc.) - * @param start start position in JSON data string - * @param end end position in JSON data string - */ -typedef struct { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string - */ -typedef struct { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -void jsmn_init(jsmn_parser *parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each describing - * a single JSON object. - */ -int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, - jsmntok_t *tokens, unsigned int num_tokens); - -#ifdef __cplusplus -} -#endif - -#endif /* __JSMN_H_ */ diff --git a/src/main.cpp b/src/main.cpp index 1ed55f60d..207a62fe9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -330,7 +330,9 @@ bool CInputStreamAdaptive::OpenStream(int streamid) // StreamCryptoSession enable the use of ISA VideoCodecAdaptive decoder kodi::addon::StreamCryptoSession cryptoSession; - cryptoSession.SetKeySystem(m_session->GetCryptoKeySystem()); + + const std::string keySystem = CSrvBroker::GetKodiProps().GetDrmKeySystem(); + cryptoSession.SetKeySystem(m_session->GetCryptoKeySystem(keySystem)); const char* sessionId(m_session->GetCDMSession(cdmSessionIndex)); cryptoSession.SetSessionId(sessionId); diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index 0870d0c84..f4d2e186e 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -110,7 +110,7 @@ void adaptive::CDashTree::Configure(CHOOSER::IRepresentationChooser* reprChooser std::string_view manifestUpdParams) { AdaptiveTree::Configure(reprChooser, supportedKeySystems, manifestUpdParams); - m_isCustomInitPssh = !CSrvBroker::GetKodiProps().GetLicenseData().empty(); + m_isCustomInitPssh = !CSrvBroker::GetKodiProps().GetDrmConfig().initData.empty(); } bool adaptive::CDashTree::Open(std::string_view url, @@ -959,7 +959,7 @@ void adaptive::CDashTree::ParseTagRepresentation(pugi::xml_node nodeRepr, } repr->m_psshSetPos = psshSetPos; - if (ParseTagContentProtectionSecDec(nodeRepr)) + if (ParseTagContentProtectionSecDec(nodeRepr).has_value()) { LOG::LogF(LOGERROR, "The tag must be child of " "the tag."); @@ -1350,7 +1350,7 @@ bool adaptive::CDashTree::GetProtectionData( //! @todo: this should not be a task of parser, moreover missing an appropriate KID extraction from mp4 box auto& kodiProps = CSrvBroker::GetKodiProps(); ProtectionScheme ckProtScheme; - if (kodiProps.GetLicenseType() == DRM::KS_CLEARKEY) + if (kodiProps.GetDrmKeySystem() == DRM::KS_CLEARKEY) { std::string_view defaultKid; if (protSelected) @@ -1421,7 +1421,7 @@ bool adaptive::CDashTree::GetProtectionData( return isEncrypted; } -bool adaptive::CDashTree::ParseTagContentProtectionSecDec(pugi::xml_node nodeParent) +std::optional adaptive::CDashTree::ParseTagContentProtectionSecDec(pugi::xml_node nodeParent) { // Try to find ISA custom tag/attrib: // @@ -1446,11 +1446,14 @@ bool adaptive::CDashTree::ParseTagContentProtectionSecDec(pugi::xml_node nodePar "You must change it to \"HW_SECURE_CODECS_REQUIRED\"."); robustnessLevel = "HW_SECURE_CODECS_REQUIRED"; } - return robustnessLevel == "HW_SECURE_CODECS_REQUIRED"; + if (robustnessLevel == "HW_SECURE_CODECS_NOT_ALLOWED") + return false; + else if (robustnessLevel == "HW_SECURE_CODECS_REQUIRED") + return true; } } } - return false; + return std::nullopt; } uint32_t adaptive::CDashTree::ParseAudioChannelConfig(pugi::xml_node node) diff --git a/src/parser/DASHTree.h b/src/parser/DASHTree.h index 89cfe1901..44b339641 100644 --- a/src/parser/DASHTree.h +++ b/src/parser/DASHTree.h @@ -90,7 +90,7 @@ class ATTR_DLL_LOCAL CDashTree : public adaptive::AdaptiveTree std::string& kid, std::string& licenseUrl); - bool ParseTagContentProtectionSecDec(pugi::xml_node nodeParent); + std::optional ParseTagContentProtectionSecDec(pugi::xml_node nodeParent); uint32_t ParseAudioChannelConfig(pugi::xml_node node); diff --git a/src/parser/HLSTree.cpp b/src/parser/HLSTree.cpp index fb6f9c87f..80699d2a7 100644 --- a/src/parser/HLSTree.cpp +++ b/src/parser/HLSTree.cpp @@ -146,7 +146,7 @@ adaptive::CHLSTree::CHLSTree() : AdaptiveTree() adaptive::CHLSTree::CHLSTree(const CHLSTree& left) : AdaptiveTree(left) { - m_decrypter = std::make_unique(left.m_decrypter->getLicenseKey()); + m_decrypter = std::make_unique(); } void adaptive::CHLSTree::Configure(CHOOSER::IRepresentationChooser* reprChooser, @@ -154,7 +154,7 @@ void adaptive::CHLSTree::Configure(CHOOSER::IRepresentationChooser* reprChooser, std::string_view manifestUpdateParam) { AdaptiveTree::Configure(reprChooser, supportedKeySystems, manifestUpdateParam); - m_decrypter = std::make_unique(CSrvBroker::GetKodiProps().GetLicenseKey()); + m_decrypter = std::make_unique(); } bool adaptive::CHLSTree::Open(std::string_view url, @@ -1001,24 +1001,19 @@ void adaptive::CHLSTree::OnDataArrived(uint64_t segNum, if (pssh.defaultKID_.empty()) { - RETRY: - std::map headers; - std::vector keyParts = STRING::SplitToVec(m_decrypter->getLicenseKey(), '|'); - std::string url = pssh.m_licenseUrl; - - if (keyParts.size() > 0) - { - URL::AppendParameters(url, keyParts[0]); - } - if (keyParts.size() > 1) - ParseHeaderString(headers, keyParts[1]); + // RETRY: + auto& drmCfgProp = CSrvBroker::GetKodiProps().GetDrmConfig(DRM::KS_NONE); CURL::HTTPResponse resp; - if (DownloadKey(url, headers, {}, resp)) + if (DownloadKey(pssh.m_licenseUrl, drmCfgProp.license.reqHeaders, {}, resp)) { pssh.defaultKID_ = resp.data; } + /* + *! @todo: unclear if could be used by some old addon, + *! for now all related code has been commented for a future removal + * else if (pssh.defaultKID_ != "0") { //! @todo: RenewLicense (addon) callback is not wiki documented, there are addons that could use this? @@ -1029,14 +1024,18 @@ void adaptive::CHLSTree::OnDataArrived(uint64_t segNum, m_decrypter->RenewLicense(keyParts[4])) goto RETRY; } + */ } } + /* if (pssh.defaultKID_ == "0") { segBuffer.resize(segBufferSize + srcDataSize, 0); return; } else if (!segBufferSize) + */ + if (!segBufferSize) { if (pssh.iv.empty()) m_decrypter->ivFromSequence(iv, segNum); diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index b0d3180e0..2ed234956 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -46,6 +46,7 @@ add_executable(${BINARY} ../utils/CurlUtils.cpp ../utils/DigestMD5Utils.cpp ../utils/FileUtils.cpp + ../utils/JsonUtils.cpp ../utils/StringUtils.cpp ../utils/UrlUtils.cpp ../utils/Utils.cpp diff --git a/src/test/TestHelper.cpp b/src/test/TestHelper.cpp index cc828c0a7..021815700 100644 --- a/src/test/TestHelper.cpp +++ b/src/test/TestHelper.cpp @@ -159,8 +159,6 @@ std::string AESDecrypter::convertIV(const std::string& input) void AESDecrypter::ivFromSequence(uint8_t* buffer, uint64_t sid){} -bool AESDecrypter::RenewLicense(const std::string& pluginUrl){return false;} - std::string DASHTestTree::RunManifestUpdate(std::string manifestUpdFile) { m_manifestUpdUrl.clear(); diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index a77b80958..9b0d1abe9 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES CurlUtils.cpp DigestMD5Utils.cpp FileUtils.cpp + JsonUtils.cpp StringUtils.cpp UrlUtils.cpp Utils.cpp @@ -17,6 +18,7 @@ set(HEADERS CurlUtils.h DigestMD5Utils.h FileUtils.h + JsonUtils.h log.h StringUtils.h UrlUtils.h diff --git a/src/utils/JsonUtils.cpp b/src/utils/JsonUtils.cpp new file mode 100644 index 000000000..2b39a7402 --- /dev/null +++ b/src/utils/JsonUtils.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "JsonUtils.h" + +namespace +{ +const rapidjson::Value* TraversePaths(const rapidjson::Value& node, const std::string& keyName) +{ + if (node.IsObject()) + { + for (auto itr = node.MemberBegin(); itr != node.MemberEnd(); ++itr) + { + if (itr->name.GetString() == keyName) + { + return &itr->value; + } + else if (itr->value.IsObject()) + { + const rapidjson::Value* ret = TraversePaths(itr->value, keyName); + if (ret) + return ret; + } + } + } + + return nullptr; +} + +} // unnamed namespace + +const rapidjson::Value* UTILS::JSON::GetValueAtPath(const rapidjson::Value& node, + const std::string& path) +{ + size_t pos = path.find('/'); + std::string current_level = path.substr(0, pos); + + if (node.IsObject() && node.HasMember(current_level.c_str())) + { + if (pos == std::string::npos) + { + return &node[current_level.c_str()]; + } + else + { + return GetValueAtPath(node[current_level.c_str()], path.substr(pos + 1)); + } + } + + return nullptr; +} + +const rapidjson::Value* UTILS::JSON::GetValueTraversePaths(const rapidjson::Value& node, + const std::string& keyName) +{ + return TraversePaths(node, keyName); +} diff --git a/src/utils/JsonUtils.h b/src/utils/JsonUtils.h new file mode 100644 index 000000000..5598ea891 --- /dev/null +++ b/src/utils/JsonUtils.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include + +#include + +namespace UTILS +{ +namespace JSON +{ + +/*! + * \brief Get value from a JSON path e.g. "a/b/c" + * \param node The json object where get the value + * \param path The path where the value is contained + * \return The json object if found, otherwise nullptr. + */ +const rapidjson::Value* GetValueAtPath(const rapidjson::Value& node, const std::string& path); + +/*! + * \brief Get value from an unknown JSON path, + * then traverse all even nested dictionaries to search for the specified key name. + * \param node The json object where find the path/value + * \param keyName The key name to search for + * \return The json object if found, otherwise nullptr. + */ +const rapidjson::Value* GetValueTraversePaths(const rapidjson::Value& node, + const std::string& keyName); + +} // namespace JSON +} // namespace UTILS diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index 7dfd9344c..e15756c9b 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -265,6 +265,12 @@ std::string UTILS::STRING::ToLower(std::string str) return str; } +std::string UTILS::STRING::ToUpper(std::string str) +{ + StringUtils::ToUpper(str); + return str; +} + uint32_t UTILS::STRING::HexStrToUint(std::string_view hexValue) { uint32_t val; diff --git a/src/utils/StringUtils.h b/src/utils/StringUtils.h index aac37c2db..caa334f7b 100644 --- a/src/utils/StringUtils.h +++ b/src/utils/StringUtils.h @@ -52,6 +52,25 @@ bool GetMapValue(const std::map& map, const T& key, TValue& val) return false; } +/*! + * \brief Get map value of the specified key + * \param map The map where find the value + * \param key The key to find + * \param val[OUT] The value that match to the specified key, if found + * \return True if found, otherwise false. + */ +template +bool GetMapValue(const std::map& map, const std::string_view& key, TValue& val) +{ + auto mapIt = map.find(key.data()); + if (mapIt != map.cend()) + { + val = mapIt->second; + return true; + } + return false; +} + /*! * \brief Replace the first string occurrence in a string * \param inputStr String to perform the replace @@ -193,12 +212,19 @@ bool CompareNoCase(std::string_view str1, std::string_view str2); bool GetLine(std::stringstream& ss, std::string& line); /*! - * \brief Convert a string to lower. + * \brief Convert a string to lower case. * \param str The string to be converted * \return The string in lowercase. */ std::string ToLower(std::string str); +/*! + * \brief Convert a string to upper case. + * \param str The string to be converted + * \return The string in uppercase. + */ +std::string ToUpper(std::string str); + /*! * \brief Convert a hex value as string to unsigned integer. * \param hexValue The hex value as string to be converted