diff --git a/README.md b/README.md index c9500cc..0c73031 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,8 @@ python3 -m venv .venv .venv/bin/python app.py ``` -On the iPhone, open `http://YOUR-COMPUTER-IP:5000/?key=YOUR_API_KEY` in Safari. -The key is saved only for that browser session and removed from the displayed -URL. The iPhone and computer must be on the same local network. +On the iPhone, open `http://YOUR-COMPUTER-IP:5000/` in Safari. The iPhone and +computer must be on the same local network. ## Configure buttons and scenes @@ -71,16 +70,14 @@ Credentials are saved with owner-only permissions in ```yaml bridge: 192.168.1.10 -app_key: your-hue-application-key. # the hue bride api key -api_key: your-dashboard-api-key # the key for this services api +app_key: your-hue-application-key ``` The file is ignored by Git and should not be shared. `hue.py register` creates -or updates it, including a random dashboard API key. You can override values -for either tool with environment variables: +or updates it. You can override the bridge values with environment variables: ```sh -FASTHUE_API_KEY=YOUR_DASHBOARD_KEY HUE_BRIDGE=192.168.1.10 HUE_APP_KEY=YOUR_HUE_KEY .venv/bin/python app.py +HUE_BRIDGE=192.168.1.10 HUE_APP_KEY=YOUR_HUE_KEY .venv/bin/python app.py ``` The reverse-proxy path is configured separately in `config.yaml`: @@ -93,17 +90,16 @@ Set it to an empty value when FastHue is served at `/`. ## HTTP API -Every API request needs the single `api_key` from `secrets.yaml` as a Bearer -token. Override it with `FASTHUE_API_KEY` or `--api-key` when necessary. +The dashboard API does not require a key. Do not expose it to untrusted users: +every client that can reach it can read and control the configured lights. ```sh -export FASTHUE_API_KEY='the-api_key-from-secrets.yaml' -curl -H "Authorization: Bearer $FASTHUE_API_KEY" http://localhost:5000/api/groups -curl -X POST -H "Authorization: Bearer $FASTHUE_API_KEY" \ +curl http://localhost:5000/api/groups +curl -X POST \ http://localhost:5000/api/groups/GROUP_UUID/next-scene -curl -X POST -H "Authorization: Bearer $FASTHUE_API_KEY" \ +curl -X POST \ http://localhost:5000/api/groups/GROUP_UUID/off -curl -X POST -H "Authorization: Bearer $FASTHUE_API_KEY" \ +curl -X POST \ -H 'Content-Type: application/json' \ -d '{"name":"Relax"}' \ 'http://localhost:5000/api/groups/Ground%20floor/scene' @@ -190,7 +186,6 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Prefix /hue; - proxy_set_header Authorization $http_authorization; } } ``` @@ -199,9 +194,8 @@ The `www-data` group in the systemd unit and `--umask 007` make the socket readable by Nginx. If your Nginx workers use another group, replace `www-data` in the unit with that group. Test and reload Nginx with `sudo nginx -t && sudo systemctl reload nginx`. -Use the HTTPS URL on the iPhone with `/hue/?key=YOUR_API_KEY` once per browser -session. Set `url_prefix` in `config.yaml` to another leading-slash prefix when -needed, and use the same value in Nginx's `location` and +Use the HTTPS URL on the iPhone with `/hue/`. Set `url_prefix` in `config.yaml` +to another leading-slash prefix when needed, and use the same value in Nginx's `location` and `X-Forwarded-Prefix`. Keep `secrets.yaml` readable only by the service account. diff --git a/app.py b/app.py index eefbc69..784fbfa 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,6 @@ from __future__ import annotations import argparse -import hmac import os from pathlib import Path from typing import Any @@ -124,7 +123,7 @@ def next_scene(button: dict[str, Any], bridge: HueBridge) -> dict[str, Any]: return {"name": selected.get("metadata", {}).get("name", "scene")} -def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str = "") -> Flask: +def create_app(yaml_path: Path, verify_tls: bool, url_prefix: str = "") -> Flask: app = Flask(__name__) if url_prefix: # Gunicorn only receives traffic over its private socket from Nginx. @@ -132,22 +131,11 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1) buttons = read_dashboard(yaml_path) - def api_key_required(view: Any) -> Any: - def protected(*args: Any, **kwargs: Any) -> Any: - authorization = request.headers.get("Authorization", "") - expected = f"Bearer {api_key}" - if not hmac.compare_digest(authorization, expected): - return jsonify({"error": "Unauthorized"}), 401 - return view(*args, **kwargs) - protected.__name__ = view.__name__ - return protected - @app.get("/") def index() -> str: return render_template("index.html", buttons=buttons) @app.get("/api/groups") - @api_key_required def groups() -> Any: try: return jsonify({"groups": dashboard_state(buttons, verify_tls)}) @@ -155,7 +143,6 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str return jsonify({"error": str(exc)}), 503 @app.post("/api/groups//next-scene") - @api_key_required def activate_next_scene(group_id: str) -> Any: try: button = next(item for item in buttons if item["id"] == group_id) @@ -168,7 +155,6 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str return jsonify({"error": str(exc)}), 503 @app.post("/api/groups//off") - @api_key_required def turn_group_off(group_id: str) -> Any: try: button = next(item for item in buttons if item["id"] == group_id) @@ -182,7 +168,6 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str return jsonify({"error": str(exc)}), 503 @app.post("/api/groups//scene") - @api_key_required def activate_group_scene(group_name: str) -> Any: payload = request.get_json(silent=True) or {} scene_name = payload.get("name") @@ -226,12 +211,8 @@ def configured_url_prefix(path: Path = APP_CONFIG) -> str: def create_gunicorn_app() -> Flask: """Gunicorn application factory; configuration is read from environment/files.""" config_path = Path(os.environ.get("FASTHUE_DASHBOARD_CONFIG", ROOT / "hue.yaml")) - secrets_config = load_config(Path(os.environ.get("FASTHUE_SECRETS_FILE", DEFAULT_CONFIG)).expanduser()) - api_key = os.environ.get("FASTHUE_API_KEY") or secrets_config.get("api_key") - if not api_key: - raise RuntimeError("add api_key to secrets.yaml or set FASTHUE_API_KEY") verify_tls = os.environ.get("FASTHUE_VERIFY_TLS", "").casefold() in {"1", "true", "yes"} - return create_app(config_path, verify_tls, api_key, configured_url_prefix()) + return create_app(config_path, verify_tls, configured_url_prefix()) # Older Gunicorn releases do not support --factory. Expose a WSGI callable at @@ -244,19 +225,14 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--config", type=Path, default=ROOT / "hue.yaml", help="Dashboard YAML file") parser.add_argument("--verify-tls", action="store_true", help="Verify the Hue bridge certificate") - parser.add_argument("--api-key", help="Required Bearer token for the dashboard API") parser.add_argument("--url-prefix", help="Override the reverse-proxy URL prefix from config.yaml") parser.add_argument("--host", default="0.0.0.0", help="Listen address") parser.add_argument("--port", type=int, default=5000, help="Listen port") args = parser.parse_args() - secrets_config = load_config(Path(os.environ.get("FASTHUE_SECRETS_FILE", DEFAULT_CONFIG)).expanduser()) - api_key = args.api_key or os.environ.get("FASTHUE_API_KEY") or secrets_config.get("api_key") - if not api_key: - parser.error("add api_key to secrets.yaml, set FASTHUE_API_KEY, or supply --api-key") url_prefix = args.url_prefix.rstrip("/") if args.url_prefix is not None else configured_url_prefix() if url_prefix and not url_prefix.startswith("/"): parser.error("--url-prefix must start with /, for example /hue") - create_app(args.config, args.verify_tls, api_key, url_prefix).run(host=args.host, port=args.port, debug=False) + create_app(args.config, args.verify_tls, url_prefix).run(host=args.host, port=args.port, debug=False) if __name__ == "__main__": diff --git a/hue.py b/hue.py index 11aeed4..67fe6c8 100644 --- a/hue.py +++ b/hue.py @@ -9,7 +9,6 @@ from __future__ import annotations import argparse import json -import secrets import ssl import sys from dataclasses import dataclass @@ -229,9 +228,8 @@ def register(args: argparse.Namespace) -> None: **existing, "bridge": bridge.host, "app_key": app_key, - "api_key": existing.get("api_key", secrets.token_urlsafe(32)), }) - print(f"Saved bridge address, application key, and dashboard API key in {config_path}") + print(f"Saved bridge address and application key in {config_path}") def build_parser() -> argparse.ArgumentParser: diff --git a/static/app.js b/static/app.js index cd7ad36..d97dbd2 100644 --- a/static/app.js +++ b/static/app.js @@ -1,20 +1,9 @@ const status = document.querySelector('#status'); const buttons = [...document.querySelectorAll('.group-button')]; const apiPrefix = document.body.dataset.apiPrefix; -const url = new URL(window.location.href); -const suppliedKey = url.searchParams.get('key'); -if (suppliedKey) { - sessionStorage.setItem('fasthue-api-key', suppliedKey); - url.searchParams.delete('key'); - history.replaceState({}, '', url); -} -const apiKey = sessionStorage.getItem('fasthue-api-key'); function apiFetch(path, options = {}) { - return fetch(path, { - ...options, - headers: { ...options.headers, Authorization: `Bearer ${apiKey || ''}` }, - }); + return fetch(path, options); } function showStatus(message, persistent = false) {