Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for listening on a UNIX socket instead of IP #116259

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 72 additions & 5 deletions homeassistant/components/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ipaddress import IPv4Network, IPv6Network, ip_network
import logging
import os
import shutil
import socket
import ssl
from tempfile import NamedTemporaryFile
Expand Down Expand Up @@ -85,6 +86,9 @@

CONF_SERVER_HOST: Final = "server_host"
CONF_SERVER_PORT: Final = "server_port"
CONF_SOCKET_USER: Final = "socket_user"
CONF_SOCKET_GROUP: Final = "socket_group"
CONF_SOCKET_PERMISSIONS: Final = "socket_permissions"
CONF_BASE_URL: Final = "base_url"
CONF_SSL_CERTIFICATE: Final = "ssl_certificate"
CONF_SSL_PEER_CERTIFICATE: Final = "ssl_peer_certificate"
Expand Down Expand Up @@ -127,6 +131,11 @@
cv.ensure_list, vol.Length(min=1), [cv.string]
),
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_SOCKET_USER): vol.Any(vol.Coerce(int), vol.Coerce(str)),
vol.Optional(CONF_SOCKET_GROUP): vol.Any(vol.Coerce(int), vol.Coerce(str)),
vol.Optional(CONF_SOCKET_PERMISSIONS): vol.All(
vol.Coerce(int), vol.Range(min=0, max=0o777)
),
vol.Optional(CONF_BASE_URL): cv.string,
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile,
Expand Down Expand Up @@ -161,6 +170,9 @@

server_host: list[str]
server_port: int
socket_user: int | str
socket_group: int | str
socket_permissions: int
base_url: str
ssl_certificate: str
ssl_peer_certificate: str
Expand Down Expand Up @@ -210,6 +222,9 @@

server_host = conf[CONF_SERVER_HOST]
server_port = conf[CONF_SERVER_PORT]
socket_user = conf.get(CONF_SOCKET_USER)
socket_group = conf.get(CONF_SOCKET_GROUP)
socket_permissions = conf.get(CONF_SOCKET_PERMISSIONS)
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
Expand All @@ -232,6 +247,9 @@
ssl_key=ssl_key,
trusted_proxies=trusted_proxies,
ssl_profile=ssl_profile,
socket_user=socket_user,
socket_group=socket_group,
socket_permissions=socket_permissions,
)
await server.async_initialize(
cors_origins=cors_origins,
Expand Down Expand Up @@ -320,6 +338,9 @@
server_port: int,
trusted_proxies: list[IPv4Network | IPv6Network],
ssl_profile: str,
socket_user: int | str | None,
socket_group: int | str | None,
socket_permissions: int | None,
) -> None:
"""Initialize the HTTP Home Assistant server."""
self.app = HomeAssistantApplication(
Expand All @@ -342,8 +363,11 @@
self.server_port = server_port
self.trusted_proxies = trusted_proxies
self.ssl_profile = ssl_profile
self.socket_user = socket_user
self.socket_group = socket_group
self.socket_permissions = socket_permissions
self.runner: web.AppRunner | None = None
self.site: HomeAssistantTCPSite | None = None
self.site: web.BaseSite | None = None
self.context: ssl.SSLContext | None = None

async def async_initialize(
Expand Down Expand Up @@ -549,6 +573,34 @@
context.load_cert_chain(cert_pem.name, key_pem.name)
return context

async def _socket_set_ownership(self, socket_path: str) -> None:
"""Set the configured uid/gid and permissions on the socket."""
# They didn't find a way to put this in aiohttp yet so we have to do it here
# https://github.com/aio-libs/aiohttp/issues/4155#issuecomment-643509809

def _set_permissions() -> None:
if self.socket_permissions is None:
return
os.chmod(socket_path, self.socket_permissions)

Check warning on line 584 in homeassistant/components/http/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/__init__.py#L581-L584

Added lines #L581 - L584 were not covered by tests

def _set_user_group() -> None:
shutil.chown(socket_path, self.socket_user or -1, self.socket_group or -1)

Check warning on line 587 in homeassistant/components/http/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/__init__.py#L586-L587

Added lines #L586 - L587 were not covered by tests

if self.socket_permissions is not None:
try:
await self.hass.async_add_executor_job(_set_permissions)
except OSError as error:
_LOGGER.error(

Check warning on line 593 in homeassistant/components/http/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/__init__.py#L589-L593

Added lines #L589 - L593 were not covered by tests
"Failed to change permissions on %s: %s", socket_path, error
)
if self.socket_user is not None or self.socket_group is not None:
try:
await self.hass.async_add_executor_job(_set_user_group)
except OSError as error:
_LOGGER.error(

Check warning on line 600 in homeassistant/components/http/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/__init__.py#L596-L600

Added lines #L596 - L600 were not covered by tests
"Failed to change user/group on %s: %s", socket_path, error
)

async def start(self) -> None:
"""Start the aiohttp server."""
# Aiohttp freezes apps after start so that no changes can be made.
Expand All @@ -563,17 +615,32 @@
)
await self.runner.setup()

self.site = HomeAssistantTCPSite(
self.runner, self.server_host, self.server_port, ssl_context=self.context
)
socket_path: str | None = None
if self.server_host and self.server_host[0].startswith("unix:"):
socket_path = self.server_host[0].removeprefix("unix:")
self.site = web.UnixSite(

Check warning on line 621 in homeassistant/components/http/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/__init__.py#L620-L621

Added lines #L620 - L621 were not covered by tests
self.runner,
socket_path,
ssl_context=self.context,
)
else:
self.site = HomeAssistantTCPSite(
self.runner,
self.server_host,
self.server_port,
ssl_context=self.context,
)
try:
await self.site.start()
except OSError as error:
_LOGGER.error(
"Failed to create HTTP server at port %d: %s", self.server_port, error
)

_LOGGER.info("Now listening on port %d", self.server_port)
if socket_path is not None:
await self._socket_set_ownership(socket_path)

Check warning on line 641 in homeassistant/components/http/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/__init__.py#L641

Added line #L641 was not covered by tests

_LOGGER.info("Now listening on %s", self.site.name)

async def stop(self) -> None:
"""Stop the aiohttp server."""
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/http/forwarded.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections.abc import Awaitable, Callable
from ipaddress import IPv4Network, IPv6Network, ip_address
import logging
import socket

from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
from aiohttp.web import Application, HTTPBadRequest, Request, StreamResponse, middleware
Expand Down Expand Up @@ -90,7 +91,11 @@
# Connected IP isn't retrieveable from the request transport, continue
return await handler(request)

connected_ip = ip_address(request.transport.get_extra_info("peername")[0])
if request.transport.get_extra_info("socket").family == socket.AF_UNIX:
# UNIX sockets won't have a peername but always come from localhost anyway
connected_ip = ip_address("127.0.0.1")

Check warning on line 96 in homeassistant/components/http/forwarded.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/http/forwarded.py#L96

Added line #L96 was not covered by tests
else:
connected_ip = ip_address(request.transport.get_extra_info("peername")[0])

# We have X-Forwarded-For, but config does not agree
if not use_x_forwarded_for:
Expand Down