Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions homeassistant/components/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,16 @@
from .decorators import require_admin # noqa: F401
from .forwarded import async_setup_forwarded
from .headers import setup_headers
from .issue import (
async_create_deprecated_yaml_issue,
async_create_failed_to_start_issue,
)
from .request_context import setup_request_context
from .security_filter import setup_security_filter
from .static import CACHE_HEADERS, CachingStaticResource
from .storage import async_get_store, to_stored
from .web_runner import HomeAssistantTCPSite, HomeAssistantUnixSite
from .websocket_api import async_register_websocket_api

CONF_SERVER_HOST: Final = "server_host"
CONF_SERVER_PORT: Final = "server_port"
Expand Down Expand Up @@ -201,11 +207,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# we import aiohttp_fast_zlib
(await async_import_module(hass, "aiohttp_fast_zlib")).enable()

conf: ConfData | None = config.get(DOMAIN)
yaml_conf: ConfData | None = config.get(DOMAIN)
store = async_get_store(hass)
stored = await store.async_load()

if conf is None:
if stored is None:
if yaml_conf is not None:
stored = to_stored(dict(yaml_conf))
await store.async_save(stored)
async_create_deprecated_yaml_issue(hass)
else:
stored = to_stored(cast(dict[str, Any], HTTP_SCHEMA({})))
await store.async_save(stored)
elif yaml_conf is not None:
async_create_deprecated_yaml_issue(hass)

try:
conf = cast(ConfData, HTTP_SCHEMA(dict(stored)))
except vol.Invalid as err:
_LOGGER.error(
"Stored HTTP config is invalid: %s. Falling back to defaults", err
)
async_create_failed_to_start_issue(hass, error=str(err))
conf = cast(ConfData, HTTP_SCHEMA({}))

Comment thread
MindFreeze marked this conversation as resolved.
async_register_websocket_api(hass)

if CONF_SERVER_HOST in conf and is_hassio(hass):
issue_id = "server_host_deprecated_hassio"
ir.async_create_issue(
Expand Down
39 changes: 39 additions & 0 deletions homeassistant/components/http/issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Repair issues for the HTTP integration."""

from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .const import DOMAIN

# HTTP touches every installation, so we allow at least a full 12 months for
# users to migrate.
BREAKS_IN_HA_VERSION = "2027.6.0"


Comment thread
MindFreeze marked this conversation as resolved.
@callback
def async_create_deprecated_yaml_issue(hass: HomeAssistant) -> None:
"""Create a repair issue for deprecated YAML configuration."""
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
is_fixable=False,
breaks_in_ha_version=BREAKS_IN_HA_VERSION,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={"domain": DOMAIN},
)


@callback
def async_create_failed_to_start_issue(hass: HomeAssistant, *, error: str) -> None:
"""Surface that HTTP fell back to safe defaults during startup."""
async_create_issue(
hass,
DOMAIN,
"http_failed_to_start",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="http_failed_to_start",
translation_placeholders={"error": error},
)
59 changes: 59 additions & 0 deletions homeassistant/components/http/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Persistent storage for the user-managed HTTP integration config."""

from ipaddress import IPv4Network, IPv6Network
from typing import Any, TypedDict, cast

from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import Store

# A separate key from STORAGE_KEY ("http") in __init__.py, which is used for the
# recovery-mode snapshot of the last successfully resolved config. The two stores
# must not share a key.
USER_CONFIG_STORAGE_KEY = "http.config"
USER_CONFIG_STORAGE_VERSION = 1


class HttpUserConfig(TypedDict, total=False):
"""User-managed HTTP config persisted via Store.

Mirrors the validated output of ``HTTP_SCHEMA`` except ``trusted_proxies`` is
held as a list of strings for JSON serialization.
"""

server_host: list[str]
server_port: int
Comment thread
MindFreeze marked this conversation as resolved.
ssl_certificate: str
ssl_peer_certificate: str
ssl_key: str
cors_allowed_origins: list[str]
use_x_forwarded_for: bool
trusted_proxies: list[str]
use_x_frame_options: bool
ip_ban_enabled: bool
login_attempts_threshold: int
ssl_profile: str


class HttpConfigStore(Store[HttpUserConfig]):
"""Store for user-managed HTTP config."""


def async_get_store(hass: HomeAssistant) -> HttpConfigStore:
"""Return the user-config Store, private to the integration."""
return HttpConfigStore(
hass,
USER_CONFIG_STORAGE_VERSION,
USER_CONFIG_STORAGE_KEY,
private=True,
Comment thread
MindFreeze marked this conversation as resolved.
atomic_writes=True,
)
Comment on lines +41 to +49


def to_stored(conf: dict[str, Any]) -> HttpUserConfig:
"""Convert a validated ``HTTP_SCHEMA`` dict into a JSON-serializable form."""
out: dict[str, Any] = {k: v for k, v in conf.items() if k != "base_url"}
if "trusted_proxies" in out:
out["trusted_proxies"] = [
str(cast(IPv4Network | IPv6Network, n)) for n in out["trusted_proxies"]
]
Comment thread
MindFreeze marked this conversation as resolved.
return cast(HttpUserConfig, out)
8 changes: 8 additions & 0 deletions homeassistant/components/http/strings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"issues": {
"deprecated_yaml": {
"description": "Your existing HTTP configuration from `configuration.yaml` has been imported. The `http` integration is now configured from the UI under **Settings** > **System** > **Network**.\n\nPlease remove the `http:` block from your `configuration.yaml` and restart Home Assistant.",
"title": "The HTTP YAML configuration is deprecated"
},
"http_failed_to_start": {
"description": "The stored HTTP configuration is no longer valid (for example, an SSL certificate path may have moved). Home Assistant started with default HTTP settings so you can recover access.\n\nReview the values under **Settings** > **System** > **Network**.\n\nError: `{error}`",
"title": "HTTP configuration was rejected on startup"
},
"server_host_deprecated_hassio": {
"description": "The deprecated `server_host` configuration option in the HTTP integration is prone to break the communication between Home Assistant Core and Supervisor, and will be removed.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"title": "The `server_host` HTTP configuration may break Home Assistant Core - Supervisor communication"
Expand Down
67 changes: 67 additions & 0 deletions homeassistant/components/http/websocket_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""WebSocket API for the HTTP integration user config."""

from typing import Any, cast

import voluptuous as vol

from homeassistant.core import HomeAssistant

from .storage import async_get_store, to_stored


def async_register_websocket_api(hass: HomeAssistant) -> None:
"""Register the HTTP config WebSocket commands.

The core ``homeassistant.components.websocket_api`` dependency is imported
lazily because it imports from ``homeassistant.components.http``; using its
decorators at this module's load time would create a circular import.
"""
from homeassistant.components import websocket_api # noqa: PLC0415

@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "http/config/get"})
@websocket_api.async_response
async def ws_get_config(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return the current user-managed HTTP config."""
store = async_get_store(hass)
stored = await store.async_load() or {}
connection.send_result(msg["id"], {"config": stored})

@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "http/config/update",
vol.Required("config"): dict,
}
)
@websocket_api.async_response
async def ws_update_config(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Merge the incoming patch into the stored HTTP config and save."""
# Local import: HTTP_SCHEMA lives in __init__.py, which already imports
# this module to wire up the commands.
from . import HTTP_SCHEMA # noqa: PLC0415

store = async_get_store(hass)
existing = await store.async_load() or {}
merged = {**existing, **msg["config"]}

try:
validated = HTTP_SCHEMA(merged)
except vol.Invalid as err:
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
return

stored = to_stored(cast(dict[str, Any], validated))
await store.async_save(stored)
connection.send_result(msg["id"], {"config": stored})
Comment on lines +52 to +64

websocket_api.async_register_command(hass, ws_get_config)
websocket_api.async_register_command(hass, ws_update_config)
16 changes: 12 additions & 4 deletions tests/components/http/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,16 +688,24 @@ async def test_ssl_issue_urls_configured(
"expected_issues",
),
[
(False, {}, ["0.0.0.0", "::"], set()),
(False, {"server_host": "0.0.0.0"}, ["0.0.0.0"], set()),
(True, {}, ["0.0.0.0", "::"], set()),
(False, {}, ["0.0.0.0", "::"], {("http", "deprecated_yaml")}),
(
False,
{"server_host": "0.0.0.0"},
["0.0.0.0"],
{("http", "deprecated_yaml")},
),
(True, {}, ["0.0.0.0", "::"], {("http", "deprecated_yaml")}),
(
True,
{"server_host": "0.0.0.0"},
[
"0.0.0.0",
],
{("http", "server_host_deprecated_hassio")},
{
("http", "server_host_deprecated_hassio"),
("http", "deprecated_yaml"),
},
),
],
)
Expand Down
Loading
Loading