Files
unshackle/tests/remote/unit/test_server_cdm_multikey.py
T
imSp4rky 91db1fa0b1 feat(remote): keep slow remote session requests alive with a heartbeat
Titles, tracks and licence stream a newline every 30s until the body is ready, so a slow service outlasts the client timeout and proxy idle limits; add a per-server timeout.
2026-09-24 10:18:03 -06:00

163 lines
6.8 KiB
Python

"""Server-CDM licensing must survive multi-key PlayReady titles.
A PlayReady WRMHEADER can advertise fewer KIDs than the license returns (the real
per-track KID is in the init segment, not the PSSH), so the server must license
instead of short-circuiting on a vault hit for the PSSH KID alone, and the batch
path must give each track the full bundle without borrowing another PSSH's keys.
"""
import asyncio
import base64
import json
from types import SimpleNamespace
from uuid import UUID
import pyplayready.system.pssh as pr_pssh_mod
import pytest
import pywidevine.pssh as wv_pssh_mod
import unshackle.core.cdm as cdm_mod
import unshackle.core.cdm.detect as detect_mod
import unshackle.core.drm as drm_mod
from unshackle.core.api import handlers
pytestmark = pytest.mark.unit
REAL = UUID("161219ec3df64e0eadbb3827c0ccc46d") # real per-track KID, only in the init segment
PSSH_KID = UUID("75982fac2cd8412a990a59c0ef2d4bb1") # the single KID the WRMHEADER advertises
OTHER = UUID("43de497c6d1945f49acc2951696ec628") # a sibling track's KID from the same license
PSSH_B64 = base64.b64encode(b"pssh").decode()
class _FakePRPSSH:
def __init__(self, data):
self.data = data
class _FakePlayReady:
"""PSSH reports one KID; the license returns the full multi-key bundle."""
def __init__(self, pssh=None, pssh_b64=None, **kwargs):
self.kids = [PSSH_KID]
self.content_keys: dict = {}
self.pssh_b64 = pssh_b64
def get_content_keys(self, cdm, certificate, licence):
self.content_keys = {REAL: "k_real", PSSH_KID: "k_pssh", OTHER: "k_other"}
@pytest.fixture
def playready_env(monkeypatch):
monkeypatch.setattr(pr_pssh_mod, "PSSH", _FakePRPSSH)
monkeypatch.setattr(drm_mod, "PlayReady", _FakePlayReady)
monkeypatch.setitem(cdm_mod.__dict__, "load_cdm", lambda *a, **k: object())
monkeypatch.setattr(detect_mod, "is_playready_cdm", lambda cdm: True)
monkeypatch.setattr(handlers, "ensure_track_drm", lambda track, session=None, init_data=None: None)
monkeypatch.setattr(handlers, "resolve_device_name", lambda *a, **k: "dev")
monkeypatch.setattr(handlers.config, "serve", {"users": {}}, raising=False)
def test_playready_multikey_not_shortcircuited_by_vault(playready_env, monkeypatch):
# The vault holds only the PSSH KID (a stale subset). The old code returned it and skipped
# the license, hiding REAL and OTHER; the fix licenses and returns the whole bundle.
monkeypatch.setattr(handlers, "check_vaults", lambda kids, name: {PSSH_KID.hex: "STALE"})
cached: dict = {}
monkeypatch.setattr(handlers, "cache_to_vaults", lambda keys, name: cached.update(keys))
keys = handlers.handle_single_server_cdm(
service=SimpleNamespace(),
title=SimpleNamespace(),
track=SimpleNamespace(),
pssh_b64=PSSH_B64,
drm_type="playready",
request=None,
)
assert set(keys) == {REAL.hex, PSSH_KID.hex, OTHER.hex}
assert keys[REAL.hex] == "k_real"
assert cached == keys # the full bundle is cached for other consumers
def test_widevine_still_uses_vault_shortcut(monkeypatch):
# Widevine PSSH KIDs are authoritative, so a vault hit stays a valid shortcut (no license).
kid = UUID("11111111111111111111111111111111")
class _FakeWvPSSH:
def __init__(self, b64):
self.b64 = b64
class _FakeWidevine:
def __init__(self, pssh=None, **kwargs):
self.kids = [kid]
self.content_keys: dict = {}
def get_content_keys(self, **kwargs):
raise AssertionError("Widevine must not license when the vault already has every KID")
monkeypatch.setattr(wv_pssh_mod, "PSSH", _FakeWvPSSH)
monkeypatch.setattr(drm_mod, "Widevine", _FakeWidevine)
monkeypatch.setattr(handlers, "ensure_track_drm", lambda track, session=None, init_data=None: None)
monkeypatch.setattr(handlers, "resolve_device_name", lambda *a, **k: "dev")
monkeypatch.setattr(handlers, "check_vaults", lambda kids, name: ({kid.hex: "vaultkey"}, {kid.hex: "sqlite"}))
monkeypatch.setattr(handlers.config, "serve", {"users": {}}, raising=False)
sources: dict = {}
keys = handlers.handle_single_server_cdm(
SimpleNamespace(), SimpleNamespace(), SimpleNamespace(), PSSH_B64, "widevine", None, sources
)
assert keys == {kid.hex: "vaultkey"}
assert sources == {kid.hex: "sqlite"} # the server keeps the name; the client only learns it is vault output
async def test_batch_shares_full_bundle_per_pssh(monkeypatch):
# Two tracks share PSSH-A (one license, both get the full bundle); a third uses PSSH-B and must
# get PSSH-B's own keys, never PSSH-A's.
pssh_a = base64.b64encode(b"A").decode()
pssh_b = base64.b64encode(b"B").decode()
track_pssh = {"vid": pssh_a, "aud": pssh_a, "vid2": pssh_b}
tracks = {tid: SimpleNamespace(id=tid, drm=[object()]) for tid in track_pssh}
session = SimpleNamespace(
lock=asyncio.Lock(),
service_tag="EXAMPLE",
service_instance=SimpleNamespace(),
served_keys={},
tracks=SimpleNamespace(get=tracks.get),
)
async def fake_get_session(session_id, request):
return session
calls: list = []
bundle_a = {REAL.hex: "k_real", PSSH_KID.hex: "k_pssh", OTHER.hex: "k_other"}
bundle_b = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "k_b"}
def fake_single(service, title, track, pssh_str, drm_type, request, sources=None):
calls.append(pssh_str)
return dict(bundle_a) if pssh_str == pssh_a else dict(bundle_b)
monkeypatch.setattr(handlers, "get_validated_session", fake_get_session)
monkeypatch.setattr(handlers, "server_cdm_allowed", lambda request, tag: True)
monkeypatch.setattr(handlers, "ensure_track_drm", lambda track, session=None, init_data=None: None)
monkeypatch.setattr(handlers, "find_title_for_track", lambda tid, session: SimpleNamespace())
monkeypatch.setattr(handlers, "detect_cdm_type_for_service", lambda tag, cfg: "playready")
monkeypatch.setattr(handlers, "resolve_device_name", lambda *a, **k: "dev")
monkeypatch.setattr(handlers, "drm_preference_name", lambda track: None)
monkeypatch.setattr(handlers, "extract_pssh_from_track", lambda track, drm: track_pssh[track.id])
monkeypatch.setattr(handlers, "handle_single_server_cdm", fake_single)
monkeypatch.setattr(handlers.config, "serve", {"users": {}}, raising=False)
resp = await handlers.session_license_handler(
{"mode": "server_cdm", "track_ids": ["vid", "aud", "vid2"], "drm_type": "playready"},
"sess",
None,
)
payload = json.loads(resp.body)
keys = payload["keys"]
assert calls == [pssh_a, pssh_b] # licensed once per unique PSSH, not per track
assert keys["vid"] == bundle_a
assert keys["aud"] == bundle_a # the shared-PSSH sibling reuses the full bundle
assert keys["vid2"] == bundle_b # the other PSSH group keeps its own keys, no cross-borrow