mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 13:05:09 +00:00
feat(serve)!: add server-held accounts for remote sessions
serve.server_accounts lends region-tagged server credentials to remote sessions (round-robin per region, per-key opt-in, per-account cache). Remote sessions no longer fall back to the server's local credentials when the client sends none.
This commit is contained in:
@@ -44,7 +44,7 @@ All are `GET`.
|
||||
| Path | Returns |
|
||||
|------|---------|
|
||||
| `/api/dashboard/status` | Version, `code_hash`, bind host and port, `mode` (`full`, `api_only`, `remote_only`), uptime, `requests_total`, `requests_rejected` (401s), `requests_by_key` (label → count), loaded service count, session count, `max_sessions`, `session_ttl`, job counts by status. |
|
||||
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the first resolved title's display name), `auth_status`, `auth_error`, `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`. `client` is whatever the remote client sent as `client` in its session create request (the current CLI sends nothing, so it is `{}`). `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
|
||||
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the first resolved title's display name), `auth_status`, `auth_error`, `server_account` (the server profile lent to the remote session, else `null`), `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`. `client` is whatever the remote client sent as `client` in its session create request (the current CLI sends nothing, so it is `{}`). `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
|
||||
| `/api/dashboard/jobs` | Every download job with full detail, regardless of owner. Empty in `--remote-only` mode. |
|
||||
| `/api/dashboard/logs` | `{"seq": N, "records": [...]}`: the last 1000 log records. `?since=<seq>` returns only newer records; `?level=WARNING` sets the minimum level; `?logger=serve` keeps one logger and its children (`aiohttp.access` is the noisiest). `ts` is a Unix epoch in seconds. |
|
||||
| `/api/dashboard/events` | Server-Sent Events stream. With `?since=<seq>` it returns `{"seq", "stats", "events": [...]}` instead: every event after `seq` (the server keeps the last 20000, log records included, so a poll every few seconds is safe even under a busy access log) as one JSON burst, so a UI that polls every few seconds still sees a remote session that opened and closed in between. |
|
||||
|
||||
@@ -120,6 +120,7 @@ Field notes:
|
||||
| `has_search` | Whether the service supports `POST /api/search`. |
|
||||
| `has_drm` | Whether the service overrides a Widevine or PlayReady license hook. |
|
||||
| `pending_update` | Set to `true` while a service repository refresh waits for this service's jobs to finish before it swaps in the new code. Absent otherwise. |
|
||||
| `server_accounts` | Present when the server lends its own accounts for this service: `{regions: ["ca", "gb"], global: bool}`, the regions those accounts cover. A client sends no credentials, cookies, or cache for such a service. |
|
||||
| `auth_methods` | Declared or inferred auth methods, e.g. `["cookies"]`, `["credentials"]`. |
|
||||
|
||||
| Status | Meaning |
|
||||
@@ -833,7 +834,7 @@ DELETE /api/session/{id} → tear down, harvest updated cache
|
||||
|
||||
### `POST /api/session/create`
|
||||
|
||||
Make a remote session for a service and title. Authentication runs asynchronously in the background. This call returns immediately with `status: "authenticating"`, and you then poll the prompt endpoint. The body accepts `service` and `title_id` (both required). It also accepts a broad set of optional keys, because the body allows `additionalProperties`. These are `credentials` (`{username, password, extra?}`), `cookies` (base64 of zlib-compressed Netscape cookie file), `proxy`, `no_proxy`, `profile`, `cache` (map of forwarded cache files), `client_region`, `cdm_type`, `range_`, `vcodec`, `quality`, `best_available`, and any service CLI options.
|
||||
Make a remote session for a service and title. Authentication runs asynchronously in the background. This call returns immediately with `status: "authenticating"`, and you then poll the prompt endpoint. The body accepts `service` and `title_id` (both required). It also accepts a broad set of optional keys, because the body allows `additionalProperties`. These are `credentials` (`{username, password, extra?}`), `cookies` (base64 of zlib-compressed Netscape cookie file), `proxy`, `no_proxy`, `profile`, `cache` (map of forwarded cache files), `client_region`, `proxy_region`, `cdm_type`, `range_`, `vcodec`, `quality`, `best_available`, and any service CLI options. `proxy_region` is the country code the client resolved `proxy` from; the server matches it against its own accounts.
|
||||
|
||||
The `proxy` value must be a full proxy URI, unless the operator gives your API key `server_proxy` in the `serve.users` config. Without it, the server does not resolve country codes with its own proxy providers. It rejects the request with `INVALID_PROXY` when no `proxy` is set and the reported `client_region` differs from the server's region. A request that reports no `client_region` is not blocked. Pass `proxy` with your own proxy, or `no_proxy` to accept the server's own connection. With `server_proxy`, the server resolves a country code and picks a proxy for your `client_region` itself.
|
||||
|
||||
@@ -843,10 +844,13 @@ The `proxy` value must be a full proxy URI, unless the operator gives your API k
|
||||
{
|
||||
"session_id": "d4e5f6a7-...",
|
||||
"service": "EXAMPLE",
|
||||
"status": "authenticating"
|
||||
"status": "authenticating",
|
||||
"server_account": false
|
||||
}
|
||||
```
|
||||
|
||||
`server_account` is `true` when the server authenticated with one of its own accounts (`serve.server_accounts`). Otherwise the server uses only what the client sent, and never falls back to its own credentials.
|
||||
|
||||
| Status | Error code | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `200` | - | Session created; authentication in progress. |
|
||||
|
||||
@@ -126,6 +126,18 @@ hood the client walks a remote session through its lifecycle.
|
||||
the server fetches the right manifests
|
||||
- Your local per-service **cache files** (e.g. refreshed tokens)
|
||||
|
||||
The server uses its own accounts only when the operator lists the service in
|
||||
`serve.server_accounts` and gives your API key `server_accounts` in `serve.users`.
|
||||
For any other service, a client that sends no credentials or cookies authenticates
|
||||
with nothing. For a listed service the client sends no credentials, cookies, or
|
||||
cache at all: the server picks one of its own accounts that works in your region and
|
||||
rotates through them. `--proxy ca` picks a `ca` account.
|
||||
|
||||
`GET /api/services` advertises the regions those accounts cover. When your own
|
||||
region is not one of them and you set no `--proxy`, the client resolves a proxy for
|
||||
the first advertised region itself, so both sides sit in a region the account
|
||||
works in.
|
||||
|
||||
The server responds immediately with a session ID and a status. Authentication
|
||||
runs in the background on the server.
|
||||
|
||||
@@ -247,7 +259,9 @@ serve:
|
||||
- **Namespaced, isolated cache.** Each remote session gets its own cache directory,
|
||||
namespaced by a hash of the API key and the remote session ID, so remote sessions
|
||||
cannot read each other's cached tokens. The server deletes the directory when the
|
||||
remote session ends.
|
||||
remote session ends. A remote session that uses a server account (`serve.server_accounts`)
|
||||
gets a persistent per-account cache instead, so the account's tokens outlive the
|
||||
remote session.
|
||||
- **The server redacts secrets in logs.** It redacts remote session IDs, service tags,
|
||||
and other user values before it writes them to the log.
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ is the [REST API](../../dev/rest-api/index.md) section. These are the config key
|
||||
| `global_speed_limit` | str | *(unlimited)* | Server-wide download speed cap, e.g. `10M`, `1.5G` or plain bytes/sec (same format as `speed_limit`). One shared budget across all concurrent jobs; per-job speed limits are ignored while it is set. |
|
||||
| `cdm_overrides` | list or bool | *(unset)* | Allowed per-request CDM overrides: a list of permitted device names, or `true` for any. Unset rejects every override. |
|
||||
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
|
||||
| `server_accounts` | dict | `{}` | Services whose remote sessions use the server's own `credentials` and cookie files instead of the client's (see below). |
|
||||
| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. |
|
||||
| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. |
|
||||
|
||||
@@ -145,6 +146,54 @@ client that asks anyway gets a `FORBIDDEN` error. Because a download job always
|
||||
an API key without `server_cdm` for that service also cannot submit or retry `/api/download`
|
||||
jobs. Keys that have no `users` entry, such as `api_secret`, keep server CDM access.
|
||||
|
||||
`server_accounts` lists, per service, the accounts the server lends to remote sessions. A remote
|
||||
session for a listed service ignores the credentials, cookies, and cache the client sends and
|
||||
uses one of the server's own profiles from `credentials`. For a service that is not listed the
|
||||
server never uses its own accounts: a client that sends nothing authenticates with nothing.
|
||||
|
||||
```yaml
|
||||
credentials:
|
||||
EXAMPLE1:
|
||||
ca_main: user1@example.com:pw1
|
||||
eu_box: user2@example.com:pw2
|
||||
shared: user3@example.com:pw3
|
||||
|
||||
serve:
|
||||
server_accounts:
|
||||
EXAMPLE1:
|
||||
ca_main: ca # one country
|
||||
eu_box: [gb, fr, de] # a list of countries
|
||||
shared: global # any region
|
||||
EXAMPLE2: true # every profile (or the single credential), any region
|
||||
```
|
||||
|
||||
Each entry under a service names a profile from `credentials`, or a cookie file
|
||||
`cookies/<TAG>/<profile>.txt` for a service that has no credentials. Its value is the region or
|
||||
regions that account works in: a two-letter country code, a list of them, or `global`. Quote
|
||||
`no` (Norway), because YAML reads the bare word as `false`.
|
||||
|
||||
The region of a remote session is the country the client asked for with `--proxy` (`ca`, `ca1`,
|
||||
`provider:ca`), the server's own region with `--no-proxy`, or the client's own region otherwise.
|
||||
A full proxy URI carries no country, so with one only a `global` account matches. The server
|
||||
picks the profiles that cover that region (or are `global`) and rotates through them
|
||||
round-robin, one account per remote session. With no matching account it rejects the remote
|
||||
session. These services ignore the client's `-p/--profile`.
|
||||
|
||||
Cookies come only from the profile's own file (`cookies/EXAMPLE1/ca_main.txt`; the server does
|
||||
not read a bare `cookies/EXAMPLE1.txt`), and each account keeps its own persistent cache under
|
||||
`cache/_accounts/EXAMPLE1/<profile>/`, so refreshed tokens survive the remote session but never
|
||||
cross accounts. `serve` refuses to start when a listed profile has neither an entry under
|
||||
`credentials` nor a cookie file.
|
||||
`/api/search`, `/api/list-titles` and `/api/list-tracks` use the same pool, matched on the
|
||||
client's reported region, and return `FORBIDDEN` to an API key without the opt-in. Rotation is
|
||||
per process and does not track which accounts are in use, so concurrent remote sessions can
|
||||
share one account.
|
||||
|
||||
A `users` entry opts in to those accounts with `server_accounts: true` (every listed service)
|
||||
or a list of service tags. It is off unless set: an API key without it authenticates with the
|
||||
credentials the client sends, as for any other service, and `/api/services` does not advertise
|
||||
the server's accounts to it. API keys with no `users` entry, such as `api_secret`, keep access.
|
||||
|
||||
`server_proxy` decides whether that API key may use the proxy providers the server has
|
||||
configured. There is no implicit access: only the boolean `true` on the API key's `users` entry
|
||||
grants it, and an API key without a `users` entry has no access either. Without the opt-in the
|
||||
@@ -173,6 +222,7 @@ serve:
|
||||
c9d0e1f2:
|
||||
services: [EXAMPLE1, EXAMPLE2]
|
||||
server_cdm: [EXAMPLE1] # the server licenses only EXAMPLE1; EXAMPLE2 needs a local CDM
|
||||
server_accounts: [EXAMPLE1] # may authenticate EXAMPLE1 with one of the server's own accounts
|
||||
server_proxy: true # this key may also use the server's proxy providers
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user