fix: Fail-closed pinned resolver, close session on HA stop, drop dead workflow step

- _PinnedResolver raises on unpinned hosts instead of falling back to
  live DNS: nothing legitimate resolves other hosts on a pinned session
  (redirects are off), so fail closed
- dedicated session is also closed on EVENT_HOMEASSISTANT_CLOSE: core
  stop does not unload entries, and the raw ClientSession has no HA
  auto-cleanup, which left 'Unclosed client session' logs at shutdown
- hassfest.yaml: remove the version-print step, hassfest runs inside
  the action's docker image and is never on the runner PATH
This commit is contained in:
SMKRV
2026-07-07 01:49:03 +03:00
parent fc59f584a3
commit 65dbb5dfc0
3 changed files with 20 additions and 11 deletions
+14 -1
View File
@@ -16,7 +16,7 @@ import asyncio
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.const import CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_CLOSE
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -379,6 +379,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Register update listener for options changes
entry.async_on_unload(entry.add_update_listener(async_update_options))
# HA Core stop does not unload entries, so close the dedicated
# session on the CLOSE event too; unload removes this listener
# and closes the session via APIClient.shutdown() instead.
async def _async_close_session_on_stop(_event) -> None:
if not session.closed:
await session.close()
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, _async_close_session_on_stop
)
)
_LOGGER.debug("Setup completed for %s", instance_name)
return True
+6 -5
View File
@@ -17,7 +17,6 @@ from urllib.parse import urlparse
import aiohttp
from aiohttp.abc import AbstractResolver
from aiohttp.resolver import DefaultResolver
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
@@ -86,10 +85,8 @@ class _PinnedResolver(AbstractResolver):
def __init__(
self,
pinned: dict[str, list[tuple[str, int]]],
fallback: AbstractResolver | None = None,
) -> None:
self._pinned = pinned
self._fallback = fallback or DefaultResolver()
async def resolve(
self,
@@ -99,7 +96,11 @@ class _PinnedResolver(AbstractResolver):
) -> list[dict[str, Any]]:
entries = self._pinned.get(host.lower())
if entries is None:
return await self._fallback.resolve(host, port, family)
# Every request on a pinned session must target the validated
# host. Resolving anything else means a request escaped the pin
# (a new call site or a config bug) — fail closed rather than
# fall back to live, unvalidated DNS.
raise OSError(f"Refusing to resolve unpinned host: {host}")
return [
{
"hostname": host,
@@ -113,7 +114,7 @@ class _PinnedResolver(AbstractResolver):
]
async def close(self) -> None:
await self._fallback.close()
"""Nothing to close; the resolver holds only the pinned map."""
def _family_for(ip: str) -> int:
"""Return AF_INET or AF_INET6 based on the IP literal."""