From 94e5264396852ffb6e8cedc227f04e9ff07559f8 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Tue, 6 Oct 2026 13:59:40 -0600 Subject: [PATCH] fix(cdm): send a PlayReady licence without an XML declaration as XML A remote CDM tried base64 first on any licence not starting with "' + "AAAA" + "" +) + + +class Sent(Exception): + """Stops ``parse_license`` after the request body has been captured.""" + + +def decrypt_labs_cdm() -> tuple[Any, str]: + return DecryptLabsRemoteCDM(secret="x", host="https://cdm.test", device_name="SL3"), "decrypt_labs_session_id" + + +def custom_cdm() -> tuple[Any, str]: + cdm = CustomRemoteCDM(host="https://cdm.test", device={"name": "SL3000", "type": "PLAYREADY"}) + return cdm, "remote_session_id" + + +def licence_sent(cdm: Any, remote_id_key: str, licence: str) -> bytes: + """Run ``parse_license`` on a session that has a challenge, and return the licence bytes it posts.""" + session_id = cdm.open() + cdm.set_pssh_b64("UFNTSA==", session_id=session_id) + cdm._sessions[session_id].update({"challenge": b"challenge", remote_id_key: "remote", "pssh": object()}) + + body: dict[str, Any] = {} + + def capture(_url: str, json: dict[str, Any], **_: Any) -> None: + body.update(json) + raise Sent + + cdm._http_session.post = capture + with pytest.raises(Sent): + cdm.parse_license(session_id, licence) + return base64.b64decode(body["license_response"]) + + +@pytest.mark.parametrize("make_cdm", [decrypt_labs_cdm, custom_cdm]) +@pytest.mark.parametrize( + "licence", + [ + ENVELOPE, + '' + ENVELOPE, + "\n " + ENVELOPE, + # Each length makes a different count of base64 characters, so one of them decodes without an error. + *(ENVELOPE.replace("AAAA", "A" * n) for n in range(5, 8)), + ], +) +def test_a_playready_xml_licence_is_sent_as_it_is(make_cdm: Any, licence: str) -> None: + cdm, remote_id_key = make_cdm() + assert licence_sent(cdm, remote_id_key, licence) == licence.encode("utf-8") + + +@pytest.mark.parametrize("make_cdm", [decrypt_labs_cdm, custom_cdm]) +def test_a_base64_licence_is_still_decoded(make_cdm: Any) -> None: + cdm, remote_id_key = make_cdm() + assert licence_sent(cdm, remote_id_key, base64.b64encode(b"binary licence").decode()) == b"binary licence" diff --git a/unshackle/core/cdm/custom_remote_cdm.py b/unshackle/core/cdm/custom_remote_cdm.py index 36431425..d4891a27 100644 --- a/unshackle/core/cdm/custom_remote_cdm.py +++ b/unshackle/core/cdm/custom_remote_cdm.py @@ -940,7 +940,7 @@ class CustomRemoteCDM: raise ValueError("No challenge available - call get_license_challenge first") if isinstance(license_message, str): - if self.is_playready and license_message.strip().startswith("