Compare commits
2 Commits
v0.3.0
...
82710c6457
| Author | SHA1 | Date | |
|---|---|---|---|
| 82710c6457 | |||
| 9b9a2b5c9f |
+4
-54
@@ -1,60 +1,10 @@
|
||||
## v0.3.0 (2026-05-22)
|
||||
## v0.3.1 (2026-05-25)
|
||||
|
||||
Production-readiness hardening release: security, performance, accessibility, and observability. Substantial new functionality (HTTPS, audit log, OS mediaSession integration, rate limiter, X-Request-ID, ETag-cached artwork) alongside the security defaults flip described below.
|
||||
|
||||
### Behavioral Changes (worth reading before upgrade)
|
||||
|
||||
- **Admin scope is now required for management endpoints**, and `scripts_management`, `callbacks_management`, `links_management`, `media_folders_management` default to `False`. Legacy bare-string `api_tokens` entries are auto-promoted to `admin` scope, so existing single-token deployments keep working. If you ran with a non-admin token and used CRUD on `/api/scripts`, `/api/callbacks`, `/api/links`, or `/api/media-folders`, you'll need an admin-scope token (see new `TokenSpec` format in `config.example.yaml`). ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`cors_origins: ["*"]` is now refused at startup** — set explicit origins instead. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Thumbnail cache directory moved** from project-root `.cache` to `%LOCALAPPDATA%/media-server/cache` on Windows and `$XDG_CACHE_HOME/media-server/thumbnails` on POSIX. The old `.cache` directory can be deleted. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **WebSocket auth prefers the `Sec-WebSocket-Protocol: media-server.token.<T>` subprotocol** so the token no longer ends up in URL/history/Referer. The `?token=` query fallback is retained for HA integration back-compat. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
|
||||
### Features
|
||||
|
||||
- **HTTPS support** via `ssl_certfile` + `ssl_keyfile` (+ optional `ssl_keyfile_password`); startup refuses to launch with only one of the pair set. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Reverse-proxy support**: `proxy_headers` + `forwarded_allow_ips` plumbed through `Settings` to `uvicorn.Config`. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **OS media session integration**: headset / lockscreen / Bluetooth media-key buttons now drive play/pause/next/prev/seek and the browser-level mediaSession shows track metadata + artwork. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Token scope hierarchy** (`read | control | admin`) with structured `TokenSpec` entries; legacy bare-string tokens promote to `admin`. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **In-process token-bucket rate limiter**: 5/min for failed auths, 10/min for `/api/scripts/execute` and `/api/callbacks/execute`. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **WebSocket Origin allow-list check** (CSWSH defence). ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Script parameter validation**: per-parameter `pattern` regex in `ScriptParameterConfig` plus `shell=False` (`shlex.split`) execution path to harden against parameter injection on `cmd.exe`. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **CSP tightened** with `form-action`, `worker-src`, `manifest-src` directives. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`noopener noreferrer` + `no-referrer` referrerpolicy** applied to every outbound link in the WebUI. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Windows config.yaml ACL hardening** via `icacls` (current user + SYSTEM + Administrators only); `0600` continues to be enforced on POSIX. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **PWA installability**: `manifest.json` gets `id`, `scope`, and `theme_color` / `background_color` matching the Studio Reference base (`#0E0D0B`). ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Accessibility**: ARIA labels on mini-player icon buttons; inner SVGs marked `aria-hidden`. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
|
||||
### Performance
|
||||
|
||||
- **Album-art read in `windows_media` gated by track key** — was decoding the WinRT thumbnail twice per second regardless of track changes. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`/api/media/artwork` returns content-derived `ETag` + `Cache-Control`** so the browser sends `If-None-Match` and gets `304` on track repeats. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Foreground-service `ctypes` argtypes hoisted to one-time module init** — was re-declaring ~14 prototypes per probe. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`display_service._static_cache` keyed by `(edid_hash, ...)` tuple** with eviction of disappeared monitors — fixes stale capabilities on hot-plug swaps where the new topology has the same monitor count. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Visualizer `requestAnimationFrame` loop paused on `document.hidden`, resumed on `visible`**. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
Hotfix for the v0.3.0 production-readiness release: the new WebSocket Origin allow-list rejected same-origin connections from any LAN IP, breaking the Web UI on `host: 0.0.0.0` deployments unless `cors_origins` was explicitly configured.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Lifespan rewritten as `try` / `yield` / `finally`** so a partial-startup failure cannot orphan background tasks or executors. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`_run_callback` in `routes/media.py` keeps a strong task reference (GC-safe)** and uses the dedicated callback executor instead of the default pool. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`macos_media.set_volume()` no longer always returns `True`** regardless of the underlying AppleScript result. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`TrayManager._restart_requested` initialised in `__init__`** and set before signalling exit so the main thread observes it correctly. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Missing `static_dir` now logs a `WARNING`** instead of silently disabling the UI. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **WebSocket volume handler clamps input** and never drops the socket on bad messages. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Gitea release tag validated against a strict SemVer regex** before being used in a release URL. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
|
||||
### Observability
|
||||
|
||||
- **`X-Request-ID` middleware** — accepts an upstream id if it matches a safe regex, otherwise generates a `UUID4`. `request_id_var` added to `ContextVars` and included in every log line alongside the token label. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **Append-only JSONL audit log** for every script + callback execution (including `on_play` / `on_pause` / etc. event callbacks). Background-thread writer; queue capped; flushed in lifespan teardown. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **`token=...` stripped from uvicorn access logs**. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
|
||||
---
|
||||
|
||||
### Development / Internal
|
||||
|
||||
#### Tests
|
||||
|
||||
- **35 new tests** across auth scopes, the rate limiter, browser path traversal (`../`, `NUL`, UNC, absolute paths), script-parameter validation including the regex, the Gitea tag whitelist, and atomic config writes + POSIX permissions. Suite: 47 passed / 4 skipped. ([d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4))
|
||||
- **WebSocket Origin check now accepts same-origin connections.** When `cors_origins` is unset, the default allow-list was hard-coded to `http://localhost:<port>` + `http://127.0.0.1:<port>`, so a browser opening the UI via the LAN IP (e.g. `http://192.168.2.100:8765`) had its WebSocket closed with code 4003 ("Origin not allowed") and never recovered. The endpoint now also accepts any `Origin` whose authority matches the request's `Host` header (with either `http://` or `https://` scheme) — same-origin connections are by definition not CSWSH, so the cross-origin defence introduced in v0.3.0 is preserved. ([9b9a2b5](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/9b9a2b5))
|
||||
|
||||
---
|
||||
|
||||
@@ -63,6 +13,6 @@ Production-readiness hardening release: security, performance, accessibility, an
|
||||
|
||||
| Hash | Message | Author |
|
||||
|------|---------|--------|
|
||||
| [d131ba4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d131ba4) | fix: production-readiness hardening — security, perf, a11y, observability | alexei.dolgolyov |
|
||||
| [9b9a2b5](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/9b9a2b5) | fix(ws): accept same-origin WebSocket connections in default Origin allow-list | alexei.dolgolyov |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -414,8 +414,13 @@ async def websocket_endpoint(
|
||||
accept_subprotocol = proto
|
||||
break
|
||||
effective_token = subprotocol_token or token
|
||||
# Origin check — block CSWSH from third-party LAN pages. We accept the same
|
||||
# set of origins as CORS plus the default localhost loopback.
|
||||
# Origin check — block CSWSH from third-party LAN pages. Accept the same
|
||||
# set of origins as CORS plus the default localhost loopback, AND any
|
||||
# same-origin connection (where Origin matches the request's Host header).
|
||||
# Same-origin is inherently safe from CSWSH because CSWSH is a *cross*-
|
||||
# origin attack — without this, binding to 0.0.0.0 and accessing the UI
|
||||
# via a LAN IP would have its WebSocket rejected by the browser-sent
|
||||
# Origin, which the static allowlist can't anticipate.
|
||||
allowed_origins = set(
|
||||
settings.cors_origins
|
||||
or [
|
||||
@@ -427,8 +432,17 @@ async def websocket_endpoint(
|
||||
# Same-origin connections from native apps may omit Origin entirely; only
|
||||
# reject when an Origin is present AND not in the allow-list.
|
||||
if origin is not None and origin not in allowed_origins:
|
||||
await websocket.close(code=4003, reason="Origin not allowed")
|
||||
return
|
||||
host_header = websocket.headers.get("host", "")
|
||||
# Origin uses http/https; match against both scheme variants of Host
|
||||
# so HTTPS deployments without an explicit cors_origins still work.
|
||||
same_origin_candidates = (
|
||||
{f"http://{host_header}", f"https://{host_header}"}
|
||||
if host_header
|
||||
else set()
|
||||
)
|
||||
if origin not in same_origin_candidates:
|
||||
await websocket.close(code=4003, reason="Origin not allowed")
|
||||
return
|
||||
|
||||
# Verify token
|
||||
from ..auth import auth_enabled, get_token_label, token_label_var
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "media-server"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
description = "REST API server for controlling system-wide media playback"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
|
||||
Reference in New Issue
Block a user