fix: show only reachable URLs in Web UI startup message
When bound to 127.0.0.1, only show localhost URL instead of listing unreachable network interfaces. Add hint about WEB_UI_HOST=0.0.0.0 for phone/tablet access. Add VPN/multi-interface and token exposure tests (11 new tests).
This commit is contained in:
parent
a21f518c0b
commit
5b47b87c42
2 changed files with 135 additions and 6 deletions
|
|
@ -106,12 +106,16 @@ class WebAdapter(BasePlatformAdapter):
|
||||||
self._running = True
|
self._running = True
|
||||||
self._cleanup_task = asyncio.ensure_future(self._media_cleanup_loop())
|
self._cleanup_task = asyncio.ensure_future(self._media_cleanup_loop())
|
||||||
|
|
||||||
all_ips = self._get_local_ips()
|
if self._host in ("0.0.0.0", "::"):
|
||||||
primary_ip = self._get_local_ip()
|
all_ips = self._get_local_ips()
|
||||||
print(f"[{self.name}] Web UI: http://{primary_ip}:{self._port}")
|
primary_ip = self._get_local_ip()
|
||||||
for ip in all_ips:
|
print(f"[{self.name}] Web UI: http://{primary_ip}:{self._port}")
|
||||||
if ip != primary_ip:
|
for ip in all_ips:
|
||||||
print(f"[{self.name}] also: http://{ip}:{self._port}")
|
if ip != primary_ip:
|
||||||
|
print(f"[{self.name}] also: http://{ip}:{self._port}")
|
||||||
|
else:
|
||||||
|
print(f"[{self.name}] Web UI: http://{self._host}:{self._port}")
|
||||||
|
print(f"[{self.name}] Set WEB_UI_HOST=0.0.0.0 for phone/tablet access")
|
||||||
if self._token_auto_generated:
|
if self._token_auto_generated:
|
||||||
print(f"[{self.name}] Access token (auto-generated): {self._token}")
|
print(f"[{self.name}] Access token (auto-generated): {self._token}")
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ Covers:
|
||||||
18. Security: DOMPurify XSS prevention
|
18. Security: DOMPurify XSS prevention
|
||||||
19. Security: default bind to 127.0.0.1
|
19. Security: default bind to 127.0.0.1
|
||||||
20. Security: /remote-control token hiding in group chats
|
20. Security: /remote-control token hiding in group chats
|
||||||
|
21. Network: VPN/multi-interface IP detection edge cases
|
||||||
|
22. Network: startup message token exposure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
@ -795,3 +797,126 @@ class TestRemoteControlTokenHiding:
|
||||||
result = await runner._handle_remote_control_command(event)
|
result = await runner._handle_remote_control_command(event)
|
||||||
assert "mysecret" not in result
|
assert "mysecret" not in result
|
||||||
assert "hidden" in result.lower()
|
assert "hidden" in result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 21. VPN / multi-interface IP detection edge cases
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestVpnAndMultiInterfaceIp:
|
||||||
|
"""IP detection must prefer LAN IPs over VPN and handle edge cases."""
|
||||||
|
|
||||||
|
def test_lan_preferred_over_vpn(self):
|
||||||
|
"""192.168.x.x or 10.x.x.x should be chosen over 172.16.x.x VPN."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
WebAdapter, "_get_local_ips",
|
||||||
|
return_value=["172.16.0.2", "192.168.1.106"],
|
||||||
|
):
|
||||||
|
ip = WebAdapter._get_local_ip()
|
||||||
|
assert ip == "192.168.1.106"
|
||||||
|
|
||||||
|
def test_ten_network_preferred_over_vpn(self):
|
||||||
|
"""10.x.x.x corporate LAN should be preferred over 172.16.x.x VPN."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
WebAdapter, "_get_local_ips",
|
||||||
|
return_value=["172.16.5.1", "10.0.0.50"],
|
||||||
|
):
|
||||||
|
ip = WebAdapter._get_local_ip()
|
||||||
|
assert ip == "10.0.0.50"
|
||||||
|
|
||||||
|
def test_only_vpn_ip_still_returned(self):
|
||||||
|
"""If only VPN IP exists, return it rather than nothing."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
WebAdapter, "_get_local_ips",
|
||||||
|
return_value=["172.16.0.2"],
|
||||||
|
):
|
||||||
|
ip = WebAdapter._get_local_ip()
|
||||||
|
assert ip == "172.16.0.2"
|
||||||
|
|
||||||
|
def test_no_interfaces_returns_localhost(self):
|
||||||
|
"""If no IPs found at all, fall back to 127.0.0.1."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
WebAdapter, "_get_local_ips",
|
||||||
|
return_value=[],
|
||||||
|
):
|
||||||
|
ip = WebAdapter._get_local_ip()
|
||||||
|
assert ip == "127.0.0.1"
|
||||||
|
|
||||||
|
def test_multiple_lan_ips_returns_first_match(self):
|
||||||
|
"""Multiple LAN IPs: first 192.168/10.x match wins."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
with unittest.mock.patch.object(
|
||||||
|
WebAdapter, "_get_local_ips",
|
||||||
|
return_value=["172.16.0.2", "192.168.1.50", "10.0.0.1"],
|
||||||
|
):
|
||||||
|
ip = WebAdapter._get_local_ip()
|
||||||
|
assert ip == "192.168.1.50"
|
||||||
|
|
||||||
|
def test_get_local_ips_excludes_loopback(self):
|
||||||
|
"""_get_local_ips must not return 127.x.x.x addresses."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
import inspect
|
||||||
|
source = inspect.getsource(WebAdapter._get_local_ips)
|
||||||
|
# Must filter out 127.x addresses
|
||||||
|
assert "127." in source, \
|
||||||
|
"_get_local_ips must filter loopback addresses"
|
||||||
|
|
||||||
|
def test_get_local_ips_netifaces_fallback(self):
|
||||||
|
"""When netifaces is unavailable, ifconfig fallback must work."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
import inspect
|
||||||
|
source = inspect.getsource(WebAdapter._get_local_ips)
|
||||||
|
assert "ifconfig" in source, \
|
||||||
|
"_get_local_ips must have ifconfig fallback"
|
||||||
|
assert "ImportError" in source, \
|
||||||
|
"_get_local_ips must catch netifaces ImportError"
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 22. Startup message token exposure
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestStartupTokenExposure:
|
||||||
|
"""Configured tokens must not be printed in startup output."""
|
||||||
|
|
||||||
|
def test_auto_generated_flag_when_no_token(self):
|
||||||
|
"""Token auto-generation flag must be set when no token provided."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
config = PlatformConfig(enabled=True, extra={
|
||||||
|
"port": 8765, "host": "127.0.0.1", "token": "",
|
||||||
|
})
|
||||||
|
adapter = WebAdapter(config)
|
||||||
|
assert adapter._token_auto_generated is True
|
||||||
|
assert len(adapter._token) == 32 # secrets.token_hex(16) = 32 chars
|
||||||
|
|
||||||
|
def test_configured_flag_when_token_set(self):
|
||||||
|
"""Token auto-generation flag must be False when token is provided."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
config = PlatformConfig(enabled=True, extra={
|
||||||
|
"port": 8765, "host": "127.0.0.1", "token": "mytoken123",
|
||||||
|
})
|
||||||
|
adapter = WebAdapter(config)
|
||||||
|
assert adapter._token_auto_generated is False
|
||||||
|
assert adapter._token == "mytoken123"
|
||||||
|
|
||||||
|
def test_startup_log_hides_configured_token(self):
|
||||||
|
"""connect() must not print the token value when set via env."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
import inspect
|
||||||
|
source = inspect.getsource(WebAdapter.connect)
|
||||||
|
# Must check _token_auto_generated before printing
|
||||||
|
assert "_token_auto_generated" in source, \
|
||||||
|
"connect() must check _token_auto_generated before printing token"
|
||||||
|
|
||||||
|
def test_startup_log_shows_auto_token(self):
|
||||||
|
"""connect() must print the token when auto-generated."""
|
||||||
|
from gateway.platforms.web import WebAdapter
|
||||||
|
import inspect
|
||||||
|
source = inspect.getsource(WebAdapter.connect)
|
||||||
|
# Must have a branch that prints the actual token
|
||||||
|
assert "auto-generated" in source, \
|
||||||
|
"connect() must indicate when token is auto-generated"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue