From 3d2daa694bcd03d808bcf9d456d28e50ccba30ea Mon Sep 17 00:00:00 2001 From: Normann Date: Sat, 26 Sep 2026 16:54:27 +0200 Subject: [PATCH] fix(dash): use reachable host in footer API docs link (#1354) link to EOS API using reachable host --- src/akkudoktoreos/server/dash/footer.py | 11 +++++++--- src/akkudoktoreos/server/eosdash.py | 4 ++-- tests/test_footer.py | 28 +++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 tests/test_footer.py diff --git a/src/akkudoktoreos/server/dash/footer.py b/src/akkudoktoreos/server/dash/footer.py index 98998299..7b7c9c55 100644 --- a/src/akkudoktoreos/server/dash/footer.py +++ b/src/akkudoktoreos/server/dash/footer.py @@ -1,4 +1,5 @@ from typing import Optional, Union +from urllib.parse import urlunsplit import requests from loguru import logger @@ -38,11 +39,15 @@ def get_alive(eos_host: str, eos_port: Union[str, int]) -> str: return alive -def Footer(eos_host: Optional[str], eos_port: Optional[Union[str, int]]) -> str: +def Footer(eos_host: Optional[str], eos_port: Optional[Union[str, int]], request_host: str) -> str: if eos_host is None: eos_host = get_config().server.host if eos_port is None: eos_port = get_config().server.port + # A bind address is useful inside the container, but cannot be opened by a browser. + public_host = request_host if eos_host in ("0.0.0.0", "::") else eos_host # noqa: S104 + url_host = f"[{public_host}]" if public_host and ":" in public_host else public_host + docs_url = urlunsplit(("http", f"{url_host}:{eos_port}", "/docs", "", "")) alive_icon = None if eos_host is None or eos_port is None: alive = "EOS server not given: {eos_host}:{eos_port}" @@ -55,7 +60,7 @@ def Footer(eos_host: Optional[str], eos_port: Optional[Union[str, int]]) -> str: LoadingT.sm, ), ) - alive = f"EOS {eos_host}:{eos_port}" + alive = f"EOS {url_host}:{eos_port}" if alive_icon: alive_cls = f"{ButtonT.primary} uk-link rounded-md" else: @@ -63,7 +68,7 @@ def Footer(eos_host: Optional[str], eos_port: Optional[Union[str, int]]) -> str: return DivFullySpaced( P( alive_icon, - A(alive, href=f"http://{eos_host}:{eos_port}/docs", target="_blank", cls=alive_cls), + A(alive, href=docs_url, target="_blank", cls=alive_cls), ), P( A( diff --git a/src/akkudoktoreos/server/eosdash.py b/src/akkudoktoreos/server/eosdash.py index 44526b99..bacc90ef 100644 --- a/src/akkudoktoreos/server/eosdash.py +++ b/src/akkudoktoreos/server/eosdash.py @@ -264,7 +264,7 @@ def get_eosdash(request: Request): # type: ignore "About": "/eosdash/about", }, About(), - Footer(*eos_server()), + Footer(*eos_server(), request.url.hostname or "localhost"), "/eosdash/footer", ), ) @@ -280,7 +280,7 @@ def get_eosdash_footer(request: Request): # type: ignore Returns: Footer: The Footer component. """ - return Footer(*eos_server()) + return Footer(*eos_server(), request.url.hostname or "localhost") @app.get("/eosdash/about") diff --git a/tests/test_footer.py b/tests/test_footer.py new file mode 100644 index 00000000..16be854d --- /dev/null +++ b/tests/test_footer.py @@ -0,0 +1,28 @@ +"""Regression tests for browser-facing links in the EOSdash footer.""" + +from unittest.mock import patch + +import pytest +from bs4 import BeautifulSoup +from fasthtml.common import to_xml + +from akkudoktoreos.server.dash.footer import Footer + + +@pytest.mark.parametrize( + "bind_host,request_host,expected_host", + [ + ("0.0.0.0", "energy.example.com", "energy.example.com"), + ("::", "2001:db8::1", "[2001:db8::1]"), + ("api.example.com", "dashboard.example.com", "api.example.com"), + ], +) +def test_footer_docs_link_uses_reachable_host(bind_host, request_host, expected_host): + """The browser link must not expose a wildcard bind address.""" + with patch("akkudoktoreos.server.dash.footer.get_alive", return_value="alive") as health: + footer = BeautifulSoup(to_xml(Footer(bind_host, 8503, request_host)), "html.parser") + + link = footer.find("a", href=f"http://{expected_host}:8503/docs") + assert link is not None + assert link.get_text(strip=True) == f"EOS {expected_host}:8503" + health.assert_called_once_with(bind_host, 8503)