From c2f5831181768b65ca7ad0d45f01a5af002948c4 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 4 Aug 2020 15:34:23 +0200 Subject: [PATCH] Support dual stack IP support (IPv4 and IPv6) (#38046) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/handler.py | 6 +- homeassistant/components/http/__init__.py | 16 +++-- homeassistant/components/http/web_runner.py | 67 +++++++++++++++++++++ tests/scripts/test_check_config.py | 1 - 4 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/http/web_runner.py diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index cce17695e30..861056a46e4 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -10,7 +10,6 @@ from homeassistant.components.http import ( CONF_SERVER_HOST, CONF_SERVER_PORT, CONF_SSL_CERTIFICATE, - DEFAULT_SERVER_HOST, ) from homeassistant.const import HTTP_BAD_REQUEST, HTTP_OK, SERVER_PORT @@ -142,10 +141,7 @@ class HassIO: "refresh_token": refresh_token.token, } - if ( - http_config.get(CONF_SERVER_HOST, DEFAULT_SERVER_HOST) - != DEFAULT_SERVER_HOST - ): + if http_config.get(CONF_SERVER_HOST) is not None: options["watchdog"] = False _LOGGER.warning( "Found incompatible HTTP option 'server_host'. Watchdog feature disabled" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index b387cea350e..75f13caa6aa 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -30,6 +30,7 @@ from .cors import setup_cors from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 +from .web_runner import HomeAssistantTCPSite # mypy: allow-untyped-defs, no-check-untyped-defs @@ -53,7 +54,6 @@ SSL_INTERMEDIATE = "intermediate" _LOGGER = logging.getLogger(__name__) -DEFAULT_SERVER_HOST = "0.0.0.0" DEFAULT_DEVELOPMENT = "0" # To be able to load custom cards. DEFAULT_CORS = "https://cast.home-assistant.io" @@ -69,7 +69,9 @@ HTTP_SCHEMA = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { - vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, + vol.Optional(CONF_SERVER_HOST): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, @@ -190,7 +192,7 @@ async def async_setup(hass, config): if conf is None: conf = HTTP_SCHEMA({}) - server_host = conf[CONF_SERVER_HOST] + server_host = conf.get(CONF_SERVER_HOST) server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) @@ -255,8 +257,9 @@ async def async_setup(hass, config): if host: port = None - elif server_host != DEFAULT_SERVER_HOST: - host = server_host + elif server_host is not None: + # Assume the first server host name provided as API host + host = server_host[0] port = server_port else: host = local_ip @@ -412,7 +415,8 @@ class HomeAssistantHTTP: self.runner = web.AppRunner(self.app) await self.runner.setup() - self.site = web.TCPSite( + + self.site = HomeAssistantTCPSite( self.runner, self.server_host, self.server_port, ssl_context=context ) try: diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py new file mode 100644 index 00000000000..67621d63412 --- /dev/null +++ b/homeassistant/components/http/web_runner.py @@ -0,0 +1,67 @@ +"""HomeAssistant specific aiohttp Site.""" +import asyncio +from ssl import SSLContext +from typing import List, Optional, Union + +from aiohttp import web +from yarl import URL + + +class HomeAssistantTCPSite(web.BaseSite): + """HomeAssistant specific aiohttp Site. + + Vanilla TCPSite accepts only str as host. However, the underlying asyncio's + create_server() implementation does take a list of strings to bind to multiple + host IP's. To support multiple server_host entries (e.g. to enable dual-stack + explicitly), we would like to pass an array of strings. Bring our own + implementation inspired by TCPSite. + + Custom TCPSite can be dropped when https://github.com/aio-libs/aiohttp/pull/4894 + is merged. + """ + + __slots__ = ("_host", "_port", "_reuse_address", "_reuse_port", "_hosturl") + + def __init__( + self, + runner: "web.BaseRunner", + host: Union[None, str, List[str]], + port: int, + *, + shutdown_timeout: float = 60.0, + ssl_context: Optional[SSLContext] = None, + backlog: int = 128, + reuse_address: Optional[bool] = None, + reuse_port: Optional[bool] = None, + ) -> None: # noqa: D107 + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._host = host + self._port = port + self._reuse_address = reuse_address + self._reuse_port = reuse_port + + @property + def name(self) -> str: # noqa: D102 + scheme = "https" if self._ssl_context else "http" + host = self._host[0] if isinstance(self._host, list) else "0.0.0.0" + return str(URL.build(scheme=scheme, host=host, port=self._port)) + + async def start(self) -> None: # noqa: D102 + await super().start() + loop = asyncio.get_running_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_server( + server, + self._host, + self._port, + ssl=self._ssl_context, + backlog=self._backlog, + reuse_address=self._reuse_address, + reuse_port=self._reuse_port, + ) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index c4f7d2b08c5..10034cb08af 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -113,7 +113,6 @@ def test_secrets(isfile_patch, loop): "cors_allowed_origins": ["http://google.com"], "ip_ban_enabled": True, "login_attempts_threshold": -1, - "server_host": "0.0.0.0", "server_port": 8123, "ssl_profile": "modern", }