mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-27 17:44:01 +08:00
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:
@@ -35,11 +35,6 @@ jobs:
|
|||||||
- name: Run hassfest validation
|
- name: Run hassfest validation
|
||||||
uses: home-assistant/actions/hassfest@master
|
uses: home-assistant/actions/hassfest@master
|
||||||
|
|
||||||
- name: Print hassfest version
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "Hassfest version: $(hassfest --version)"
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import asyncio
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
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
|
# Register update listener for options changes
|
||||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
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)
|
_LOGGER.debug("Setup completed for %s", instance_name)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp.abc import AbstractResolver
|
from aiohttp.abc import AbstractResolver
|
||||||
from aiohttp.resolver import DefaultResolver
|
|
||||||
|
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -86,10 +85,8 @@ class _PinnedResolver(AbstractResolver):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pinned: dict[str, list[tuple[str, int]]],
|
pinned: dict[str, list[tuple[str, int]]],
|
||||||
fallback: AbstractResolver | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self._pinned = pinned
|
self._pinned = pinned
|
||||||
self._fallback = fallback or DefaultResolver()
|
|
||||||
|
|
||||||
async def resolve(
|
async def resolve(
|
||||||
self,
|
self,
|
||||||
@@ -99,7 +96,11 @@ class _PinnedResolver(AbstractResolver):
|
|||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
entries = self._pinned.get(host.lower())
|
entries = self._pinned.get(host.lower())
|
||||||
if entries is None:
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
"hostname": host,
|
"hostname": host,
|
||||||
@@ -113,7 +114,7 @@ class _PinnedResolver(AbstractResolver):
|
|||||||
]
|
]
|
||||||
|
|
||||||
async def close(self) -> None:
|
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:
|
def _family_for(ip: str) -> int:
|
||||||
"""Return AF_INET or AF_INET6 based on the IP literal."""
|
"""Return AF_INET or AF_INET6 based on the IP literal."""
|
||||||
|
|||||||
Reference in New Issue
Block a user