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 <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -1,7 +1,13 @@
|
|||||||
# Media Server Configuration
|
# Media Server Configuration
|
||||||
# Copy this file to config.yaml and customize as needed.
|
# 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
|
# API Tokens - Multiple tokens with friendly labels
|
||||||
# This allows you to identify which client is making requests in the logs
|
# This allows you to identify which client is making requests in the logs
|
||||||
@@ -11,8 +17,9 @@
|
|||||||
# web_ui: "your-web-ui-token-here"
|
# web_ui: "your-web-ui-token-here"
|
||||||
|
|
||||||
# Server settings
|
# Server settings
|
||||||
host: "0.0.0.0"
|
host: "127.0.0.1"
|
||||||
port: 8765
|
port: 8765
|
||||||
|
# allow_lan_without_auth: true # uncomment + change host to 0.0.0.0 for LAN-open mode
|
||||||
|
|
||||||
# Custom scripts
|
# Custom scripts
|
||||||
scripts:
|
scripts:
|
||||||
|
|||||||
+20
-11
@@ -320,44 +320,53 @@ def main():
|
|||||||
print("\nAuthentication is DISABLED (no tokens configured)")
|
print("\nAuthentication is DISABLED (no tokens configured)")
|
||||||
return
|
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
|
# 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.
|
# with a random token instead of starting in the insecure "no-auth" mode.
|
||||||
config_path = get_config_dir() / "config.yaml"
|
config_path = get_config_dir() / "config.yaml"
|
||||||
if not config_path.exists() and not settings.api_tokens:
|
if not config_path.exists() and not settings.api_tokens:
|
||||||
try:
|
try:
|
||||||
generate_default_config(config_path)
|
generate_default_config(config_path)
|
||||||
print(
|
_fatal(
|
||||||
f"\nFirst run: generated default config at {config_path}.\n"
|
f"\nFirst run: generated default config at {config_path}.\n"
|
||||||
"Run --show-token to retrieve the API token, then restart.",
|
"Run --show-token to retrieve the API token, then restart.",
|
||||||
file=sys.stderr,
|
exit_code=0,
|
||||||
)
|
)
|
||||||
sys.exit(0)
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"WARNING: could not bootstrap config: {e}", file=sys.stderr)
|
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.
|
# 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")
|
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:
|
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"
|
"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,"
|
"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.",
|
" or set allow_lan_without_auth: true in config.yaml to override."
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Check if port is available before starting
|
# Check if port is available before starting
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
try:
|
try:
|
||||||
sock.bind((args.host if args.host != "0.0.0.0" else "127.0.0.1", args.port))
|
sock.bind((args.host if args.host != "0.0.0.0" else "127.0.0.1", args.port))
|
||||||
except OSError:
|
except OSError:
|
||||||
print(
|
_fatal(
|
||||||
f"ERROR: Port {args.port} is already in use. "
|
f"ERROR: Port {args.port} is already in use. "
|
||||||
f"Another instance of Media Server may be running.\n"
|
f"Another instance of Media Server may be running.\n"
|
||||||
f"Stop the other process or use --port to pick a different port.",
|
f"Stop the other process or use --port to pick a different port."
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
from .tray import PYSTRAY_AVAILABLE, TrayManager
|
from .tray import PYSTRAY_AVAILABLE, TrayManager
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user