diff --git a/scripts/install.sh b/scripts/install.sh index 89eb072..9b5fc75 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -163,7 +163,10 @@ done enable_web="n" read -rp "Enable web dashboard (TUI in browser)? (y/n) [n]: " enable_web /dev/null; then + echo 'WEB_USER="admin"' >> "$CONFIG_DIR/gniza.conf" + fi if ! grep -q "^WEB_API_KEY=" "$CONFIG_DIR/gniza.conf" 2>/dev/null || \ [ -z "$(grep '^WEB_API_KEY=' "$CONFIG_DIR/gniza.conf" 2>/dev/null | sed 's/^WEB_API_KEY="//' | sed 's/"$//')" ]; then api_key="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" @@ -172,10 +175,10 @@ if [ "$enable_web" = "y" ] || [ "$enable_web" = "Y" ]; then else echo "WEB_API_KEY=\"${api_key}\"" >> "$CONFIG_DIR/gniza.conf" fi - info "Generated Web API key: $api_key" - echo "Save this key -- you will need it to access the dashboard." + info "Web credentials: user=admin password=$api_key" + echo "Save these -- you will need them to access the dashboard." else - info "Web API key already configured." + info "Web credentials already configured." fi # Install systemd service if [ "$MODE" = "root" ]; then diff --git a/tui/__main__.py b/tui/__main__.py index ad7950d..6e44e99 100644 --- a/tui/__main__.py +++ b/tui/__main__.py @@ -1,4 +1,7 @@ +import base64 import os +import re +import secrets import socket import subprocess import sys @@ -57,11 +60,43 @@ def _parse_web_args(): return host, port +def _load_web_credentials() -> tuple[str, str]: + """Load WEB_USER and WEB_API_KEY from gniza.conf.""" + config_dir = os.environ.get("GNIZA_CONFIG_DIR", "") + if not config_dir: + # Detect from mode + if os.geteuid() == 0: + config_dir = "/etc/gniza" + else: + config_dir = os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + "gniza", + ) + conf_path = Path(config_dir) / "gniza.conf" + user = "admin" + api_key = "" + if conf_path.is_file(): + kv_re = re.compile(r'^([A-Z_][A-Z0-9_]*)="(.*)"$') + for line in conf_path.read_text().splitlines(): + m = kv_re.match(line.strip()) + if m: + if m.group(1) == "WEB_USER": + user = m.group(2) + elif m.group(1) == "WEB_API_KEY": + api_key = m.group(2) + return user, api_key + + def main(): if "--web" in sys.argv: + from aiohttp import web as aio_web from textual_serve.server import Server host, port = _parse_web_args() + web_user, web_key = _load_web_credentials() + + if not web_key: + print("WARNING: No WEB_API_KEY in gniza.conf. Web dashboard is unprotected.") os.environ["PYTHONPATH"] = f"{_ROOT}:{os.environ.get('PYTHONPATH', '')}" os.environ["GNIZA_DIR"] = _ROOT @@ -74,14 +109,49 @@ def main(): public_url = f"http://{public_host}:{port}" print(f"GNIZA web: serving TUI at {public_url}") + if web_key: + print(f"GNIZA web: login with user={web_user!r}") server = Server( - f"python3 -m tui", + "python3 -m tui", host=host, port=port, title="GNIZA Backup", public_url=public_url, ) + + # Add HTTP Basic Auth middleware if API key is configured + if web_key: + _orig_make_app = server._make_app + + async def _authed_make_app(): + app = await _orig_make_app() + + @aio_web.middleware + async def basic_auth(request, handler): + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith("Basic "): + try: + decoded = base64.b64decode(auth_header[6:]).decode("utf-8") + req_user, req_pass = decoded.split(":", 1) + if ( + secrets.compare_digest(req_user, web_user) + and secrets.compare_digest(req_pass, web_key) + ): + return await handler(request) + except Exception: + pass + return aio_web.Response( + status=401, + headers={"WWW-Authenticate": 'Basic realm="GNIZA"'}, + text="Authentication required", + ) + + app.middlewares.insert(0, basic_auth) + return app + + server._make_app = _authed_make_app + server.serve() else: app = GnizaApp()