From 82710c6457e2c0029ef7c8cacf5e187e4a165814 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 25 May 2026 23:45:08 +0300 Subject: [PATCH] chore: release v0.3.1 --- RELEASE_NOTES.md | 58 ++++-------------------------------------------- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 55 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4447a3e..f379431 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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.` 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:` + `http://127.0.0.1:`, 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 | diff --git a/pyproject.toml b/pyproject.toml index 5c3bf0a..a730d50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" }