mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
feat(remote): let a remote client license with its own device identity
Relay the service's CDM calls to the client, accept the service's allowed identity config (for example ESN/Kpe/Kph), add the server_vault grant and a lent server-device mode that reads keys only from vaults and returns no login cache. Also fixes the proxied certificate fetch, Base64 Widevine licences, an answered call that stayed pending and a missing default quality.
This commit is contained in:
@@ -84,8 +84,8 @@ Under the integrated server, an API key can do DRM licensing only through a CDM
|
||||
!!! warning "PlayReady access is opt-in per user"
|
||||
If a user entry omits `playready_devices`, the server gives that API key **no** PlayReady access, and logs a warning at startup. You must name the PlayReady devices explicitly for any API key that needs them. Widevine and PlayReady device files themselves are auto-loaded from your WVDs and PRDs directories.
|
||||
|
||||
!!! note "Device lists *are* the server-side-decryption switch"
|
||||
There is no separate tier, capability flag, or permission toggle for whether the server will hand back content keys (`KID:KEY`) for a remote session download. The presence of devices on the calling API key decides it. With **empty** `devices` and `playready_devices`, the server can only proxy CDM challenges, so the client must operate its own CDM. Once those lists are **populated**, the client may request `mode:server_cdm` and receive content keys back. Do not look for a tier setting. The devices you configure are what enables server-side decryption.
|
||||
!!! note "Grants decide whether the server returns content keys"
|
||||
The devices on an API key decide which CDM the server can use. Two grants in the same user entry decide whether the server returns content keys (`KID:KEY`) for a remote session. `server_cdm` lets the server license with those devices. `server_vault` lets a remote session that the client's own device licenses read the server vault, and the server then makes no live licence. Without either grant, the server can only proxy CDM challenges, so the client must operate its own CDM. See [`serve`](../../reference/configuration/services.md) for both grants.
|
||||
|
||||
### Disabling authentication
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ but the defaults hold for the overwhelming majority of responses.
|
||||
| `INVALID_PARAMETERS` | 400 | One or more download or query parameters failed validation (bad codec, bitrate, sort field, and so on). |
|
||||
| `AUTH_FAILED` | 401 | Authentication with the streaming service failed (bad credentials or cookies). |
|
||||
| `FORBIDDEN` | 403 | The action is not allowed. Raised by server-side gates (per-key restrictions on CDM or credential overrides) and by remote session IP binding. |
|
||||
| `SERVER_CDM_CAPPED` | 403 | The server CDM does not license live above the `server_cdm_max_height` of the API key. `details.reason` is `server_cdm_max_height` (with `details.max_height`) or `client_device`, for a remote session where the client's own device licenses. A remote client licenses the track with its own device. |
|
||||
| `SERVER_CDM_CAPPED` | 403 | The server CDM does not license live above the `server_cdm_max_height` of the API key. `details.reason` is `server_cdm_max_height` (with `details.max_height`), `client_device`, for a remote session where the client's own device licenses, or `server_device`, for a remote session that runs on the server's device. A remote client licenses the track with its own device, except for `server_device`: then only its own vaults can supply the key. |
|
||||
| `GEOFENCE` | 403 | The title is not available in the applicable region. |
|
||||
| `NOT_FOUND` | 404 | A requested resource (title, history entry, and so on) does not exist. |
|
||||
| `NO_CONTENT` | 404 | The request was valid but produced nothing: no matching titles, tracks, episodes, or keys. |
|
||||
|
||||
@@ -215,7 +215,8 @@ hood the client walks a remote session through its lifecycle.
|
||||
client saves them locally, so the **next** remote session can skip interactive
|
||||
authentication. The login belongs to the client when the client sent
|
||||
cookies, credentials, or cache files, or answered a login prompt (a device code
|
||||
or an OTP) that led to a successful login. A server-account login never does.
|
||||
or an OTP) that led to a successful login. A server-account login never does,
|
||||
and neither does a remote session on the server's device (see below).
|
||||
The client saves only the returned files that pass the same profile check it
|
||||
applies before it sends them, so a file for another profile cannot overwrite
|
||||
that profile's local file.
|
||||
@@ -245,6 +246,10 @@ There are two ways DRM keys get resolved, chosen by the client's `server_cdm` fl
|
||||
|
||||
- Keeps your CDM local. The server only relays the license request.
|
||||
- Used when `server_cdm` is `false` (the default).
|
||||
- The service on the server sees your device, not the server's: its DRM system,
|
||||
security level, system ID and device type. A CDM call that the service makes
|
||||
(for example a session-key exchange at login) goes to your client as a `cdm_call`,
|
||||
and your local CDM answers it. See [Your own device identity](#your-own-device-identity).
|
||||
|
||||
=== "Server-side CDM (server_cdm: true)"
|
||||
|
||||
@@ -277,6 +282,48 @@ There are two ways DRM keys get resolved, chosen by the client's `server_cdm` fl
|
||||
licenses those tracks in proxy mode with its own local CDM. See
|
||||
[`server_cdm_max_height`](../../reference/configuration/services.md).
|
||||
|
||||
### Your own device identity
|
||||
|
||||
A service can need more than a licence challenge from your device: an ESN, model keys, or a
|
||||
session-key exchange that runs on the CDM. The device never leaves your machine. Only these cross
|
||||
the wire:
|
||||
|
||||
- **Device facts.** When it creates the remote session, the client sends its DRM system, security level,
|
||||
system ID and device type. The service sees a stand-in with these facts.
|
||||
- **CDM calls.** When the service calls the CDM, the server sends a `cdm_call` to the client
|
||||
and waits. The client makes the challenge, or parses the licence, on its own CDM. When
|
||||
the service closes a CDM session, the server sends a `close` call, because a CDM holds
|
||||
only a few sessions. The client polls for calls during login and while one of its own
|
||||
requests runs service code, so an idle remote session sends nothing extra.
|
||||
- **Session keys only.** A `keys` call returns Widevine `OPERATOR_SESSION` and `SIGNING`
|
||||
keys. It never returns a content key, and the client refuses a PlayReady `keys` call.
|
||||
So a server cannot use your device to license titles for itself.
|
||||
- **Identity config.** A service names the config keys a client may send (for example
|
||||
`esn`, `Kpe`, `Kph`, `esn_map`). The server lists them in the service listing as
|
||||
`client_config`. The client sends only those keys, from its own `services: <TAG>:`
|
||||
block in `unshackle.yaml`. When the client sends an identity, the server drops its own
|
||||
value for each of those keys, so the two identities never mix. A mapping (such as
|
||||
`esn_map`) merges the client's entries over the server's. A client entry replaces a
|
||||
server entry with the same key, also when the server's key is a number. The server keeps
|
||||
these values in the remote session's memory only.
|
||||
|
||||
A service can also offer an option that builds the remote session on the server's own
|
||||
device and identity, for the quality that device reaches. The session then makes no live
|
||||
licence at all: each content key comes from the server vault or the client's own vaults,
|
||||
and a content key that no vault holds stops the download with an error. The create
|
||||
response sets `server_device` to `true` for such a remote session. The service then uses
|
||||
the server's own config: the client's identity values do not apply. The client reads the
|
||||
server vault only when the API key has the `server_vault` grant (or `server_cdm`); without
|
||||
it, the client reads only its own vaults. The login tokens of such a remote session hold the
|
||||
server's device identity, so the server keeps them: `DELETE` returns no cache files, and the
|
||||
HTTP session headers and `track.data` lose their secret-looking keys, as for a server login.
|
||||
|
||||
!!! note "The server vault in a session your device licenses"
|
||||
With the `server_vault` grant (or `server_cdm`) on the API key, a remote session that your own
|
||||
device licenses still takes content keys from the server vault. The server tries the
|
||||
vault first. A track that no server vault holds goes back to your device. See
|
||||
[`server_vault`](../../reference/configuration/services.md).
|
||||
|
||||
---
|
||||
|
||||
## Remote session lifecycle and expiry
|
||||
@@ -365,8 +412,10 @@ The server mounts all these routes, even in `--remote-only` mode. Paths use the
|
||||
`POST /api/session/create` requires `service` and `title_id`. It also accepts
|
||||
`credentials`, `cookies` (base64 of zlib-compressed Netscape cookie file), `proxy`,
|
||||
`no_proxy`, `profile`, `cache` (a map of `filename → base64(zlib(bytes))`),
|
||||
`client_region`, `cdm_type`, `client` (a freeform identity object the dashboard
|
||||
shows as sent), and the track-selection hints `range_`, `vcodec`,
|
||||
`client_region`, `cdm_type`, `cdm_security_level`, `cdm_relay` (`true` when the client
|
||||
answers CDM calls), `cdm_system_id` and `cdm_device_type` (Widevine only), `service_config`
|
||||
(the identity config keys the service lists in `client_config`), `client` (a freeform identity
|
||||
object the dashboard shows as sent), and the track-selection hints `range_`, `vcodec`,
|
||||
`quality`, `best_available`, plus arbitrary service CLI options
|
||||
(`additionalProperties: true`).
|
||||
|
||||
@@ -395,9 +444,14 @@ a per-profile ESN.
|
||||
The response returns **before** authentication finishes:
|
||||
|
||||
```json
|
||||
{ "session_id": "...uuid4...", "service": "EXAMPLE", "status": "authenticating" }
|
||||
{ "session_id": "...uuid4...", "service": "EXAMPLE", "status": "authenticating",
|
||||
"server_cdm": false, "server_cdm_max_height": null, "server_vault": true, "server_device": false }
|
||||
```
|
||||
|
||||
`server_vault` tells the client that the server vault serves a remote session its own device
|
||||
licenses. `server_device` tells it that the service runs on the server's device, so every
|
||||
content key comes from a vault.
|
||||
|
||||
Authentication runs on a background thread (`asyncio.to_thread(authenticate, ...)`).
|
||||
The remote session starts in `AUTHENTICATING`. The client must poll the prompt
|
||||
endpoint until it reaches `authenticated` (or `failed`).
|
||||
@@ -415,6 +469,10 @@ layer.
|
||||
- A prompt after authentication (in `get_titles()`, `get_tracks()` or licensing) raises
|
||||
`RuntimeError` at once. The client polls for prompts only during authentication, so
|
||||
no answer can come.
|
||||
- A CDM call (`InputBridge.request_cdm`) works during and after authentication, because the
|
||||
client also polls while its own requests run. A call that no client fetches within 10 seconds
|
||||
fails then, because no client is listening. A fetched call waits 60 seconds for its answer.
|
||||
The call leaves the auth status as it was. An answer never marks the remote session as client-authenticated.
|
||||
- `AUTH_INPUT_TIMEOUT = 600.0` seconds. This is also the TTL granted to
|
||||
`AUTHENTICATING` / `PENDING_INPUT` sessions in the store.
|
||||
|
||||
@@ -424,9 +482,17 @@ layer.
|
||||
{ "status": "authenticated" }
|
||||
{ "status": "authenticating" }
|
||||
{ "status": "pending_input", "prompt": "Enter code: " }
|
||||
{ "status": "pending_input", "prompt": "...", "cdm_call": { "op": "challenge", "drm": "widevine", "init_data": "...", "license_type": "OFFLINE", "privacy_mode": true, "service_certificate": "..." } }
|
||||
{ "status": "failed", "error": "...message..." }
|
||||
```
|
||||
|
||||
A `cdm_call` comes first, in any auth state. The client answers it with a JSON object in
|
||||
`response`: `{"session": "...", "challenge": "<base64>"}` for `challenge`,
|
||||
`{"keys": [{"kid", "type", "key", "permissions"}]}` for `keys`, `{}` for `close`, or
|
||||
`{"error": "..."}` when its CDM refuses. An older client refuses `close`, and the server
|
||||
ignores that refusal. An answered call stops being pending at once, so a client that polls again
|
||||
straight away does not see it twice.
|
||||
|
||||
A missing remote session returns `404 SESSION_NOT_FOUND`. An IP mismatch returns
|
||||
`403 FORBIDDEN`. `POST /api/session/{id}/prompt` takes `{ "response": "..." }` and
|
||||
returns `{ "status": "accepted" }`. A post with no pending prompt is an
|
||||
@@ -477,8 +543,8 @@ _sessions/<pbkdf2_hmac(sha256, X-Secret-Key, "unshackle-session-ns", 100000)[:12
|
||||
```
|
||||
|
||||
The handler writes forwarded `cache` files into that directory before authentication.
|
||||
On `DELETE`, when the login belongs to the client (`client_auth`), the handler
|
||||
harvests updated cache files from the whole directory tree (compressing each
|
||||
On `DELETE`, when the login belongs to the client (`client_auth`) and the remote
|
||||
session does not run on the server's device, the handler harvests updated cache files from the whole directory tree (compressing each
|
||||
with zlib and base64-encoding, **excluding** `titles_*` files) and returns them
|
||||
under a `cache` field, so the client can keep refreshed tokens. Each cache key
|
||||
is the file path relative to that directory, with `/` separators and no `.json`
|
||||
|
||||
Reference in New Issue
Block a user