Commit Graph

5 Commits

Author SHA1 Message Date
alexei.dolgolyov d131ba461c fix: production-readiness hardening — security, perf, a11y, observability
Lint & Test / test (push) Successful in 20s
Security
- Default scripts_management, callbacks_management, links_management, and
  media_folders_management to False so a leaked token cannot escalate to RCE
  through admin CRUD endpoints.
- TokenSpec + scope hierarchy (read | control | admin); legacy bare-string
  api_tokens entries promote to admin for back-compat. Management endpoints
  now require admin scope.
- WebSocket subprotocol auth (Sec-WebSocket-Protocol: media-server.token.<T>)
  preferred over ?token= query so the token no longer lands in URL/history/
  Referer; query fallback retained for HA integration back-compat.
- Origin allow-list check on the WS endpoint (CSWSH defence).
- In-process token-bucket rate limiter: 5/min for failed auths,
  10/min for /api/scripts/execute and /api/callbacks/execute.
- shell=False subprocess path (shlex.split) + per-parameter regex `pattern`
  in ScriptParameterConfig to harden shell=true scripts against parameter
  injection (Windows cmd.exe env-var expansion).
- CSP gains form-action, worker-src, manifest-src directives.
- Refuse cors_origins=["*"] at startup; strip token=... from uvicorn access
  logs; validate Gitea release tag against strict SemVer regex.
- noopener noreferrer + no-referrer referrerpolicy on every outbound link.
- icacls hardening of config.yaml on Windows (current user + SYSTEM +
  Administrators only); 0600 still enforced on POSIX.
- WS volume handler clamps input and never drops the socket on bad messages.

Performance
- Album-art read in windows_media gated by track key — was decoding the
  WinRT thumbnail twice per second regardless of track changes.
- /api/media/artwork returns content-derived ETag + Cache-Control so the
  browser sends If-None-Match and gets 304s on track repeats.
- Foreground-service ctypes argtypes hoisted to one-time module init
  (was re-declaring ~14 prototypes per probe).
- 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.
- Visualizer rAF loop paused on document.hidden, resumed on visible.

Reliability / bug fixes
- Lifespan rewritten as try/yield/finally so a partial-startup failure
  cannot orphan background tasks or executors.
- _run_callback in routes/media.py keeps a strong task ref (GC-safe) and
  uses the dedicated callback executor instead of the default pool.
- macos_media.set_volume() no longer always returns True.
- TrayManager._restart_requested initialised in __init__; set before
  signalling exit so the main thread observes it correctly.
- Missing static_dir now logs a WARNING instead of silent UI disable.

UX / accessibility / PWA
- manifest.json theme_color and background_color match the Studio Reference
  base (#0E0D0B); added id and scope for PWA installability.
- ARIA on mini-player icon buttons; inner SVGs marked aria-hidden.
- OS mediaSession API wired so headset / lockscreen / Bluetooth buttons
  drive play/pause/next/prev/seek and show track metadata + artwork.

Observability
- X-Request-ID middleware (accept upstream id if it matches a safe regex,
  otherwise UUID4); request_id_var added to ContextVars and included in
  every log line alongside the token label.
- Audit log (append-only JSONL) for every script + callback execution,
  including the on_play/on_pause/etc. event callbacks. Background-thread
  writer; queue capped; flushed in lifespan teardown.

Deployment
- proxy_headers + forwarded_allow_ips plumbed through Settings →
  uvicorn.Config for reverse-proxy installs.
- HTTPS support via ssl_certfile + ssl_keyfile (+ optional password);
  startup refuses to launch with only one of the pair set.
- Thumbnail cache moved from project-root .cache to
  %LOCALAPPDATA%/media-server/cache (Windows) and
  $XDG_CACHE_HOME/media-server/thumbnails (POSIX).

Tests
- 35 new tests across auth scopes, rate limiter, browser path traversal
  (../ NUL UNC absolute), script-param validation incl. regex, Gitea tag
  whitelist, config atomic write + POSIX perms. 47 passed / 4 skipped.
2026-05-22 22:25:54 +03:00
alexei.dolgolyov bcc6d40ed7 fix: comprehensive security, bug, performance, and UI/UX audit
Lint & Test / test (push) Successful in 20s
Security
- Default bind 127.0.0.1; first-run bootstrap generates random api_token
  and refuses to bind non-loopback without auth unless explicitly opted in
- Path-traversal hardened: BrowserService.validate_path rejects absolute
  paths, drive letters, UNC, NUL bytes. /api/browser/{play,metadata,
  thumbnail} now require folder_id and a folder-relative path
- Pydantic validators on links: http(s) URLs only, mdi:<slug> icons only
- Scripts/callbacks/links create/update/delete gated by *_management flags
- Strict CSP, X-Frame-Options DENY, Referrer-Policy no-referrer,
  X-Content-Type-Options nosniff
- CORS locked to localhost:<port> + 127.0.0.1:<port> by default; configurable
- config.yaml writes atomic (tmp + os.replace) and 0o600 on POSIX
- Subprocesses spawned in their own process group / new session so timeout
  kills the whole tree (Windows CREATE_NEW_PROCESS_GROUP, POSIX
  start_new_session=True)
- Frontend XSS: monitor name + details escapeHtml'd; power button moved to
  delegated data-action handler; remote MDI SVGs parsed and sanitized
  (strip script/foreignObject/on*/javascript: hrefs) before innerHTML
- All dynamic URL segments now wrapped in encodeURIComponent

Bugs
- WebSocket reconnect: close previous socket before opening new, clear
  ping interval per-socket, clear reconnectTimeout up-front, retry on
  online/visibilitychange, try/catch JSON.parse
- Artwork fetch race: AbortController + generation guard
- _broadcast_after_open: initialize status, swallow per-poll errors,
  background tasks tracked in a strong-ref set with done-callback cleanup
- Audio analyzer: sticky _unavailable flag prevents infinite start/stop
  spin when no loopback device exists; cleared by set_device()
- Volume short-circuit cache invalidated when server reports remote volume
- Browser thumbnail race: per-folder generation counter + isConnected
  checks; aborts in-flight fetches on navigation
- Track-skip uses cached title instead of full WinRT status round-trip

Performance
- Linux MPRIS/pactl and /api/display DDC-CI handlers wrapped in
  asyncio.to_thread so blocking IO never stalls the event loop
- browse_directory moved off the event loop (SMB shares could freeze it)
- Windows status poll caches one asyncio loop per worker thread via
  threading.local instead of new_event_loop/close on every 0.5s tick
- broadcast() serializes JSON once and uses send_text to all clients
- Hourly thumbnail cache cleanup scheduled in lifespan (was never invoked
  — cache grew unbounded)
- Progress drag listeners attached only while dragging

Quality
- All asyncio.get_event_loop() in coroutines → get_running_loop()
- ThreadPoolExecutors shut down cleanly during lifespan teardown
- config_manager dedup: 12 near-identical methods collapsed onto generic
  _upsert/_delete helpers (~290 lines removed)
- Service worker no longer pass-throughs every fetch
- M3U playlist written via NamedTemporaryFile (no fixed-path symlink
  clobber race)
- __version__ now prefers live pyproject.toml in dev checkouts so
  pip install -e . users see the source-of-truth version, not the stale
  package-metadata version baked in at install time

UI/UX (Studio Reference)
- Green leftover focus rings (rgba(29,185,84,...)) all replaced with
  copper accent (rgba(var(--copper-rgb),...))
- Dialogs: square corners, copper top hairline, unified with editorial
  chrome
- .browser-item: transparent with copper hover border (was filled card)
- Audio device select uses var(--sans) instead of generic system font
- Mobile container padding tuned for ≤480px screens
- Breadcrumb home is a real <button> with aria-label; aria-current on root
- i18n: filled display.msg.power_*, execution.*, scripts.params.execute,
  callbacks.empty in both en + ru
2026-05-16 13:22:46 +03:00
alexei.dolgolyov 5439af1955 Add CI/CD pipelines, NSIS installer, ES module bundling, and ruff linting
Lint & Test / test (push) Failing after 9s
Release / create-release (push) Successful in 1s
Release / build-windows (push) Successful in 59s
- Add Gitea Actions workflows: test.yml (lint + test on push/PR) and
  release.yml (build + NSIS installer + upload on v* tags)
- Add NSIS installer with optional desktop shortcut and auto-start
- Add esbuild bundler: ES module migration with IIFE bundle output
- Add build-dist-windows.sh for cross-building Windows distribution
- Fix all ruff lint errors (import sorting, unused imports, line length)
- Remove redundant scripts (start-server.bat, stop-server.bat,
  start-server-background.vbs)
- Update CLAUDE.md with CI/CD and release documentation
2026-03-23 02:01:28 +03:00
alexei.dolgolyov 8db40d3ee9 UI polish: refresh button, negative thumbnail cache, and style fixes
- Add refresh button to browser toolbar to re-fetch current folder
- Cache "no thumbnail" results to avoid repeated slow SMB lookups
- Fix list view fallback icon sizing for files without album art
- Fix view toggle button hover (no background/scale on hover)
- Skip re-render when clicking already-active view mode button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 02:10:22 +03:00
alexei.dolgolyov 7c631d09f6 Add media browser feature with UI improvements
- Refactored index.html: Split into separate HTML (309 lines), CSS (908 lines), and JS (1,286 lines) files
- Implemented media browser with folder configuration, recursive navigation, and thumbnail display
- Added metadata extraction using mutagen library (title, artist, album, duration, bitrate, codec)
- Implemented thumbnail generation and caching with SHA256 hash-based keys and LRU eviction
- Added platform-specific file playback (os.startfile on Windows, xdg-open on Linux, open on macOS)
- Implemented path validation security to prevent directory traversal attacks
- Added smooth thumbnail loading with fade-in animation and loading spinner
- Added i18n support for browser (English and Russian)
- Updated dependencies: mutagen>=1.47.0, pillow>=10.0.0
- Added comprehensive media browser documentation to README

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 21:31:02 +03:00