diff --git a/custom_components/bahmcloud_store/views.py b/custom_components/bahmcloud_store/views.py index 55db9a9..7f8d43c 100644 --- a/custom_components/bahmcloud_store/views.py +++ b/custom_components/bahmcloud_store/views.py @@ -10,7 +10,7 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView if TYPE_CHECKING: - from .core import BCSCore # only for type hints, avoids circular import at runtime + from .core import BCSCore # type hints only, avoids circular import _LOGGER = logging.getLogger(__name__) @@ -18,34 +18,56 @@ _LOGGER = logging.getLogger(__name__) class StaticAssetsView(HomeAssistantView): url = "/api/bahmcloud_store_static/{path:.*}" name = "api:bahmcloud_store_static" - requires_auth = True + + # IMPORTANT: + # Custom panel JS is safe to serve without auth. Some setups (reverse proxies / cookie quirks) + # may cause 401 for module loads. Disabling auth here avoids "Unable to load custom panel". + requires_auth = False async def get(self, request: web.Request, path: str) -> web.Response: base = Path(__file__).resolve().parent / "panel" - target = (base / path).resolve() + base_resolved = base.resolve() - if not str(target).startswith(str(base.resolve())): + req_path = (path or "").lstrip("/") + + # Default file + if req_path == "": + req_path = "index.html" + + target = (base / req_path).resolve() + + # Security: prevent path traversal + if not str(target).startswith(str(base_resolved)): return web.Response(status=404) + # If folder -> index.html if target.is_dir(): - target = target / "index.html" + target = (target / "index.html").resolve() if not target.exists(): + _LOGGER.warning("BCS static asset not found: %s", target) return web.Response(status=404) - ct = "text/plain" + # Content types + ct = "text/plain; charset=utf-8" if target.suffix == ".js": - ct = "application/javascript" + ct = "application/javascript; charset=utf-8" elif target.suffix == ".html": - ct = "text/html" + ct = "text/html; charset=utf-8" elif target.suffix == ".css": - ct = "text/css" + ct = "text/css; charset=utf-8" elif target.suffix == ".svg": ct = "image/svg+xml" elif target.suffix == ".png": ct = "image/png" - return web.Response(body=target.read_bytes(), content_type=ct) + resp = web.Response(body=target.read_bytes(), content_type=ct) + + # Avoid stale caching issues during development + resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + resp.headers["Pragma"] = "no-cache" + + return resp class BCSApiView(HomeAssistantView): @@ -54,7 +76,6 @@ class BCSApiView(HomeAssistantView): requires_auth = True def __init__(self, core: Any) -> None: - # Any to avoid runtime import cycle (core imports views) self.core = core async def get(self, request: web.Request) -> web.Response: @@ -116,17 +137,14 @@ class BCSReadmeView(HomeAssistantView): html = None - # Render markdown via HA util (best effort) try: from homeassistant.util.markdown import async_render_markdown # type: ignore - rendered = await async_render_markdown(self.core.hass, md) - html = rendered + html = await async_render_markdown(self.core.hass, md) except Exception as e: _LOGGER.debug("Markdown render failed: %s", e) html = None - # Sanitize HTML if available if html: try: from homeassistant.util.sanitize_html import async_sanitize_html # type: ignore