diff --git a/custom_components/loopback_ban_exempt/__init__.py b/custom_components/loopback_ban_exempt/__init__.py new file mode 100644 index 0000000..26c4797 --- /dev/null +++ b/custom_components/loopback_ban_exempt/__init__.py @@ -0,0 +1,64 @@ +"""Exempt loopback addresses (127.0.0.1 / ::1) from Home Assistant's HTTP login-ban handling. + +Home Assistant has no built-in way to whitelist an IP from the ``http`` ban +system. Local tools running on the same host (e.g. a VS Code session that +queries ``/api/config`` without a token) therefore show up as failed login +attempts from ``127.0.0.1`` and can eventually get localhost banned, which +would break legitimate local access. + +This integration wraps ``process_wrong_login`` so that any request whose remote +address is a loopback address is silently ignored. External addresses are still +processed normally, so ``login_attempts_threshold`` protection stays intact. +""" + +from __future__ import annotations + +import logging +from ipaddress import ip_address + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "loopback_ban_exempt" + +# Modules that hold a reference to process_wrong_login (either the definition +# itself or an ``from ... import process_wrong_login``). All must be patched. +_TARGET_MODULES = ( + "homeassistant.components.http.ban", + "homeassistant.components.auth.login_flow", + "homeassistant.components.websocket_api.auth", +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Patch the ban handler to skip loopback addresses.""" + from homeassistant.components.http import ban as ban_module + + original = ban_module.process_wrong_login + + async def process_wrong_login(request): + """Ignore loopback, otherwise defer to the original handler.""" + remote = getattr(request, "remote", None) + try: + if remote is not None and ip_address(remote).is_loopback: + return None + except ValueError: + pass + return await original(request) + + patched = 0 + for mod_path in _TARGET_MODULES: + try: + module = __import__(mod_path, fromlist=["process_wrong_login"]) + except ImportError: + continue + if getattr(module, "process_wrong_login", None) is not None: + module.process_wrong_login = process_wrong_login + patched += 1 + + _LOGGER.info( + "Loopback ban exemption active (patched %s reference(s))", patched + ) + return True diff --git a/custom_components/loopback_ban_exempt/manifest.json b/custom_components/loopback_ban_exempt/manifest.json new file mode 100644 index 0000000..24ca7bc --- /dev/null +++ b/custom_components/loopback_ban_exempt/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "loopback_ban_exempt", + "name": "Loopback Ban Exempt", + "version": "1.0.0", + "documentation": "https://www.home-assistant.io/integrations/http/", + "dependencies": ["http"], + "codeowners": [], + "iot_class": "local_push" +}