mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
fix(api)!: key the health route and keep server internals from user keys
/api/health needs X-Secret-Key but stays outside the rate limit. Only the admin key gets load_errors from /api/services, default error details no longer name the exception class, and a prepare signal replaces aiohttp's Server banner on every response while the CDM routes keep their own.
This commit is contained in:
@@ -16,7 +16,7 @@ You start the API with the [`unshackle serve`](../../guide/cli-reference.md) com
|
||||
|
||||
1. Add an `api_secret` to the `serve` section of your `unshackle.yaml`.
|
||||
2. Start the server with `unshackle serve`.
|
||||
3. Send that secret in the `X-Secret-Key` header on every request except `GET /api/health`.
|
||||
3. Send that secret in the `X-Secret-Key` header on every request, `GET /api/health` included.
|
||||
|
||||
```bash title="A minimal authenticated request"
|
||||
curl -H "X-Secret-Key: your-secret-key-here" \
|
||||
@@ -152,12 +152,12 @@ The value must match one of the configured API keys exactly (`api_secret`, or on
|
||||
!!! tip "The reference client sets it for you"
|
||||
unshackle's own [remote download](../../guide/downloading.md) client (`RemoteService`) sends `X-Secret-Key` automatically, reading the API key from your `remote_services` config. It also sends a `User-Agent` of `unshackle/<version>`. You only deal with the header directly when writing your own client.
|
||||
|
||||
### The health check is always open
|
||||
### The health check needs an API key too
|
||||
|
||||
One endpoint is exempt from authentication: `GET /api/health`. It never requires an API key, so you can use it for liveness and version probes without embedding a secret in your monitoring:
|
||||
`GET /api/health` reports the running version and commit, so it sits behind the same `X-Secret-Key` check as every other route. A monitor that probes it sends an API key; a request without one gets the usual `401` body, which still proves the server is up:
|
||||
|
||||
```bash title="No key needed"
|
||||
curl http://127.0.0.1:8786/api/health
|
||||
```bash title="Liveness probe"
|
||||
curl -H "X-Secret-Key: your-secret-key-here" http://127.0.0.1:8786/api/health
|
||||
```
|
||||
|
||||
```json title="Response"
|
||||
@@ -307,7 +307,6 @@ With `--caddy`, unshackle also starts `caddy run` using the `Caddyfile` located
|
||||
| Auth header | `X-Secret-Key` |
|
||||
| Query-parameter alternative | `?secret_key=` on `GET /api/download/jobs/{job_id}/events` only |
|
||||
| Default bind address | `127.0.0.1:8786` |
|
||||
| Exempt endpoint | `GET /api/health` |
|
||||
| Failure status | `401 Unauthorized`, or `429 Too Many Requests` for a key over its rate limit |
|
||||
| Missing-header body | `{"status": 401, "message": "Secret Key is Empty."}` |
|
||||
| Invalid-key body | `{"status": 401, "message": "Secret Key is Invalid."}` |
|
||||
@@ -326,7 +325,7 @@ With `--caddy`, unshackle also starts `caddy run` using the `Caddyfile` located
|
||||
|
||||
The header and config are identical between modes, but the code that enforces them differs:
|
||||
|
||||
- **API-only server** (`--api-only`, or `--remote-only` which forces API-only). An `api_key_authentication` middleware runs on every request. It lets `GET /api/health` through unconditionally, then reads `X-Secret-Key`. An empty header returns the *"Secret Key is Empty."* body. A header that is not a configured user returns *"Secret Key is Invalid."*. In this mode the accepted-keys map holds your `api_secret` (labelled `api_user`, with no devices) plus every key under `serve.users`. A per-user key therefore authenticates here exactly as it does on the integrated server, and that user's `services` allowlist applies as usual. With `--no-key`, this middleware is not installed and the users map is empty.
|
||||
- **API-only server** (`--api-only`, or `--remote-only` which forces API-only). An `api_key_authentication` middleware runs on every request. It reads `X-Secret-Key` on every request, `GET /api/health` included. An empty header returns the *"Secret Key is Empty."* body. A header that is not a configured user returns *"Secret Key is Invalid."*. In this mode the accepted-keys map holds your `api_secret` (labelled `api_user`, with no devices) plus every key under `serve.users`. A per-user key therefore authenticates here exactly as it does on the integrated server, and that user's `services` allowlist applies as usual. With `--no-key`, this middleware is not installed and the users map is empty.
|
||||
|
||||
- **Integrated server** (default). A `serve_authentication` middleware wraps pywidevine's authentication (and pyplayready's for `/playready*` paths), and the REST routes live in the same application, guarded by the same check. Here the accepted-keys map is your full `serve` config: `api_secret` (auto-added with access to all loaded devices) plus every entry under `users`, each with its declared `devices` / `playready_devices`.
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ here, so use `id` as the identity and `label` only for display.
|
||||
| `bytes_out` | Response bodies only. An event stream reports no body length, so it counts as zero. |
|
||||
| `last_seen` | `null` until the API key makes its first request. |
|
||||
|
||||
Only a configured API key gets a row of counters. A request on an open route such as `/api/health` with an unknown key counts as `anonymous`, so an unauthenticated caller cannot grow the table.
|
||||
Only a configured API key gets a row of counters. A request that answers with an unknown key counts as `anonymous`, so an unauthenticated caller cannot grow the table.
|
||||
|
||||
Every API key the server counts has a row here, the dashboard key included, so a
|
||||
`requests_by_key` bucket other than `anonymous` always matches one. A "traffic by key" panel
|
||||
|
||||
@@ -8,11 +8,11 @@ Start the server with [`unshackle serve`](../../guide/cli-reference.md). By defa
|
||||
All examples below assume a base URL of `http://127.0.0.1:8786`. Replace the host and port so that they match your own `unshackle serve -h <host> -p <port>` invocation.
|
||||
|
||||
!!! tip "Interactive docs"
|
||||
The server mounts a live Swagger UI at **`/api/docs/`** whenever it does not operate in `--remote-only` mode. Its title is "Unshackle REST API" and its version tracks the installed package version. In API-only mode with an API key configured, the Swagger UI itself sits behind `X-Secret-Key`. Only `/api/health` is exempt.
|
||||
The server mounts a live Swagger UI at **`/api/docs/`** whenever it does not operate in `--remote-only` mode. Its title is "Unshackle REST API" and its version tracks the installed package version. In API-only mode with an API key configured, the Swagger UI itself sits behind `X-Secret-Key`, like every other route.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Authentication.** Every request except `GET /api/health` requires the `X-Secret-Key` header when you configure an API key. `--no-key` disables the check entirely. See [Authentication](authentication.md).
|
||||
- **Authentication.** Every request, `GET /api/health` included, requires the `X-Secret-Key` header when you configure an API key. `--no-key` disables the check entirely. See [Authentication](authentication.md).
|
||||
- **Content type.** Request bodies are JSON (`Content-Type: application/json`). All success responses are JSON unless this page documents a `204 No Content`.
|
||||
- **CORS.** Every response carries permissive CORS headers, and the server answers `OPTIONS` preflight requests automatically.
|
||||
- **Compression.** JSON responses of 256 bytes or more are gzip-compressed when the client sends `Accept-Encoding: gzip`. A [slow response](#slow-responses) is not compressed.
|
||||
@@ -30,12 +30,12 @@ These endpoints report server state and enumerate what is available. `GET /api/h
|
||||
|
||||
### `GET /api/health`
|
||||
|
||||
Health check. **This is the only route exempt from authentication**: you can call it without an `X-Secret-Key` header. It reports the running version and a best-effort update check.
|
||||
Health check. It reports the running version and commit and a best-effort update check, so it needs the `X-Secret-Key` header like every other route. A request without an API key gets a `401`, which still shows the server is up.
|
||||
|
||||
=== "Request"
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8786/api/health
|
||||
curl -H "X-Secret-Key: $SECRET" http://127.0.0.1:8786/api/health
|
||||
```
|
||||
|
||||
=== "Response `200`"
|
||||
@@ -68,7 +68,7 @@ If the update check fails (for example, no network), `update_available` and `lat
|
||||
|
||||
### `GET /api/services`
|
||||
|
||||
Show the streaming services available on this server, filtered by your allowlist. Each entry gives the service's tag, matching rules, capability flags, and its CLI parameters (useful when you make a UI that accepts service-specific options). `load_errors` lists the services the server skipped because they failed to import, at startup or after a service repository refresh; the server also writes each one to its log. An entry marked `pending_update` has newer code staged: it keeps its current code until its running or queued jobs and its live remote sessions finish.
|
||||
Show the streaming services available on this server, filtered by your allowlist. Each entry gives the service's tag, matching rules, capability flags, and its CLI parameters (useful when you make a UI that accepts service-specific options). For the admin key, `load_errors` lists the services the server skipped because they failed to import, at startup or after a service repository refresh; the server also writes each one to its log. Any other key gets an empty list; with `--no-key` every caller is the admin. An entry marked `pending_update` has newer code staged: it keeps its current code until its running or queued jobs and its live remote sessions finish.
|
||||
|
||||
=== "Request"
|
||||
|
||||
|
||||
@@ -221,8 +221,7 @@ body that uses an integer `status` and no `error_code` or `timestamp`.
|
||||
!!! warning "Handle both shapes"
|
||||
A client must expect two error formats: the standard structured error
|
||||
(string `status` of `"error"`, with `error_code`), and this authentication error
|
||||
(integer `status`, no `error_code`). The `GET /api/health` endpoint is exempt
|
||||
from authentication, so it never returns the key-error shape.
|
||||
(integer `status`, no `error_code`).
|
||||
|
||||
## Late failures with status `200`
|
||||
|
||||
|
||||
@@ -126,12 +126,12 @@ health check is `GET http://127.0.0.1:8786/api/health`.
|
||||
available at [`/api/docs/`](http://127.0.0.1:8786/api/docs/). The server generates
|
||||
it from the same route table it serves, so it always matches the version you
|
||||
operate. Note that in `--api-only` mode the docs page is itself behind the
|
||||
API key. Only `/api/health` is exempt from authentication.
|
||||
API key, like every other route.
|
||||
|
||||
A quick smoke test that needs no API key:
|
||||
A quick smoke test:
|
||||
|
||||
```shell title="Confirm the server is up"
|
||||
curl http://127.0.0.1:8786/api/health
|
||||
curl -H "X-Secret-Key: your-api-secret" http://127.0.0.1:8786/api/health
|
||||
```
|
||||
|
||||
```json title="Response"
|
||||
@@ -156,7 +156,7 @@ request and response in full.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/health` | Liveness and version check. Never requires an API key. |
|
||||
| `GET` | `/api/health` | Liveness and version check. |
|
||||
| `GET` | `/api/services` | List the services available on this server. |
|
||||
| `POST` | `/api/search` | Run a service's search for a query. |
|
||||
| `POST` | `/api/list-titles` | List the titles behind a service title ID. |
|
||||
@@ -220,7 +220,7 @@ request and response in full.
|
||||
## Authentication in one line
|
||||
|
||||
With the default configuration, send your API secret in the `X-Secret-Key` header on
|
||||
every request except `/api/health`:
|
||||
every request:
|
||||
|
||||
```shell title="An authenticated request"
|
||||
curl -X POST http://127.0.0.1:8786/api/search \
|
||||
|
||||
@@ -93,7 +93,7 @@ Swagger UI available at http://127.0.0.1:8786/api/docs/
|
||||
```
|
||||
|
||||
!!! note "Swagger UI is also behind auth"
|
||||
In `--api-only` mode (without `--no-key`), the Swagger UI at `/api/docs/` requires the `X-Secret-Key` header, the same as every other route. Only `/api/health` is exempt from authentication.
|
||||
In `--api-only` mode (without `--no-key`), the Swagger UI at `/api/docs/` requires the `X-Secret-Key` header, the same as every other route.
|
||||
|
||||
For the rest of this page we will assume the server is reachable at `http://127.0.0.1:8786` and store the secret in an environment variable:
|
||||
|
||||
@@ -106,10 +106,10 @@ export BASE="http://127.0.0.1:8786"
|
||||
|
||||
## Step 3: Make sure that the server is alive
|
||||
|
||||
The health endpoint is the one route that never requires an API key. Use it to make sure that the server operates, and to read its version:
|
||||
Use the health endpoint to make sure that the server operates, and to read its version. It needs the API key like every other route:
|
||||
|
||||
```shell
|
||||
curl "$BASE/api/health"
|
||||
curl -H "X-Secret-Key: $SECRET" "$BASE/api/health"
|
||||
```
|
||||
|
||||
```json title="200 OK"
|
||||
|
||||
Reference in New Issue
Block a user