Support dual stack IP support (IPv4 and IPv6) (#38046)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Stefan Agner 2020-08-04 15:34:23 +02:00 committed by GitHub
parent fe07d79744
commit c2f5831181
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 12 deletions

View file

@ -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"

View file

@ -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:

View file

@ -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,
)

View file

@ -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",
}