From 0cf49deac05c8865dc6c1e2715959497a6ba2323 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 18 May 2026 03:11:08 +0300 Subject: [PATCH] fix(config): secure-by-default loopback bind and startup-error logging - Default `host: 127.0.0.1` in config.example.yaml; require explicit api_tokens or `allow_lan_without_auth: true` before binding LAN. - Mirror pre-uvicorn fatal errors to startup-errors.log in the config dir so silent boot failures via wscript/pythonw are diagnosable. Co-Authored-By: Claude Sonnet 4.5 --- config.example.yaml | 13 ++++++++++--- media_server/main.py | 31 ++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 4f573fa..0df2c17 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,7 +1,13 @@ # Media Server Configuration # Copy this file to config.yaml and customize as needed. -# By default, authentication is DISABLED (no tokens = open access). -# To enable auth, uncomment and configure the api_tokens section below. +# +# Secure-by-default: the server binds to loopback (127.0.0.1) only and refuses +# to bind a non-loopback address with no tokens configured. +# +# To expose on the LAN you must do ONE of: +# 1. Configure api_tokens below AND change host to "0.0.0.0", OR +# 2. Set `allow_lan_without_auth: true` (LAN-open, no auth — insecure on +# hostile networks, only acceptable on a trusted home LAN). # API Tokens - Multiple tokens with friendly labels # This allows you to identify which client is making requests in the logs @@ -11,8 +17,9 @@ # web_ui: "your-web-ui-token-here" # Server settings -host: "0.0.0.0" +host: "127.0.0.1" port: 8765 +# allow_lan_without_auth: true # uncomment + change host to 0.0.0.0 for LAN-open mode # Custom scripts scripts: diff --git a/media_server/main.py b/media_server/main.py index 59f9ae6..bc0f56c 100644 --- a/media_server/main.py +++ b/media_server/main.py @@ -320,44 +320,53 @@ def main(): print("\nAuthentication is DISABLED (no tokens configured)") return + # Stderr is invisible when launched via wscript / pythonw (Start Menu shortcut, + # autostart). Mirror pre-uvicorn failures to a file in the config dir so the + # next silent boot failure is diagnosable. + def _fatal(msg: str, exit_code: int = 1) -> None: + print(msg, file=sys.stderr) + try: + log_path = get_config_dir() / "startup-errors.log" + from datetime import datetime + with open(log_path, "a", encoding="utf-8") as f: + f.write(f"[{datetime.now().isoformat(timespec='seconds')}] {msg}\n") + except OSError: + pass + sys.exit(exit_code) + # First-run bootstrap: if no config has ever been written, generate one # with a random token instead of starting in the insecure "no-auth" mode. config_path = get_config_dir() / "config.yaml" if not config_path.exists() and not settings.api_tokens: try: generate_default_config(config_path) - print( + _fatal( f"\nFirst run: generated default config at {config_path}.\n" "Run --show-token to retrieve the API token, then restart.", - file=sys.stderr, + exit_code=0, ) - sys.exit(0) except OSError as e: print(f"WARNING: could not bootstrap config: {e}", file=sys.stderr) # Refuse to bind a non-loopback address with no tokens, unless explicitly opted in. non_loopback = args.host not in ("127.0.0.1", "localhost", "::1") if non_loopback and not settings.api_tokens and not settings.allow_lan_without_auth: - print( + _fatal( "ERROR: refusing to bind a non-loopback address with no api_tokens configured.\n" "Either set api_tokens in config.yaml, bind to 127.0.0.1," - " or set allow_lan_without_auth: true in config.yaml to override.", - file=sys.stderr, + " or set allow_lan_without_auth: true in config.yaml to override." ) - sys.exit(1) # Check if port is available before starting with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: try: sock.bind((args.host if args.host != "0.0.0.0" else "127.0.0.1", args.port)) except OSError: - print( + _fatal( f"ERROR: Port {args.port} is already in use. " f"Another instance of Media Server may be running.\n" - f"Stop the other process or use --port to pick a different port.", - file=sys.stderr, + f"Stop the other process or use --port to pick a different port." ) - sys.exit(1) from .tray import PYSTRAY_AVAILABLE, TrayManager