1 """Support to serve the Home Assistant API as WSGI application."""
3 from __future__
import annotations
6 from collections.abc
import Collection
7 from dataclasses
import dataclass
9 from functools
import partial
10 from ipaddress
import IPv4Network, IPv6Network, ip_network
15 from tempfile
import NamedTemporaryFile
16 from typing
import Any, Final, TypedDict, cast
18 from aiohttp
import web
19 from aiohttp.abc
import AbstractStreamWriter
20 from aiohttp.http_parser
import RawRequestMessage
21 from aiohttp.streams
import StreamReader
22 from aiohttp.typedefs
import JSONDecoder, StrOrURL
23 from aiohttp.web_exceptions
import HTTPMovedPermanently, HTTPRedirection
24 from aiohttp.web_protocol
import RequestHandler
25 from cryptography
import x509
26 from cryptography.hazmat.primitives
import hashes, serialization
27 from cryptography.hazmat.primitives.asymmetric
import rsa
28 from cryptography.x509.oid
import NameOID
29 import voluptuous
as vol
34 EVENT_HOMEASSISTANT_START,
35 EVENT_HOMEASSISTANT_STOP,
43 KEY_ALLOW_CONFIGURED_CORS,
56 async_when_setup_or_start,
62 from .auth
import async_setup_auth
63 from .ban
import setup_bans
64 from .const
import DOMAIN, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER
65 from .cors
import setup_cors
66 from .decorators
import require_admin
67 from .forwarded
import async_setup_forwarded
68 from .headers
import setup_headers
69 from .request_context
import setup_request_context
70 from .security_filter
import setup_security_filter
71 from .static
import CACHE_HEADERS, CachingStaticResource
72 from .web_runner
import HomeAssistantTCPSite
74 CONF_SERVER_HOST: Final =
"server_host"
75 CONF_SERVER_PORT: Final =
"server_port"
76 CONF_BASE_URL: Final =
"base_url"
77 CONF_SSL_CERTIFICATE: Final =
"ssl_certificate"
78 CONF_SSL_PEER_CERTIFICATE: Final =
"ssl_peer_certificate"
79 CONF_SSL_KEY: Final =
"ssl_key"
80 CONF_CORS_ORIGINS: Final =
"cors_allowed_origins"
81 CONF_USE_X_FORWARDED_FOR: Final =
"use_x_forwarded_for"
82 CONF_USE_X_FRAME_OPTIONS: Final =
"use_x_frame_options"
83 CONF_TRUSTED_PROXIES: Final =
"trusted_proxies"
84 CONF_LOGIN_ATTEMPTS_THRESHOLD: Final =
"login_attempts_threshold"
85 CONF_IP_BAN_ENABLED: Final =
"ip_ban_enabled"
86 CONF_SSL_PROFILE: Final =
"ssl_profile"
88 SSL_MODERN: Final =
"modern"
89 SSL_INTERMEDIATE: Final =
"intermediate"
91 _LOGGER: Final = logging.getLogger(__name__)
93 DEFAULT_DEVELOPMENT: Final =
"0"
96 DEFAULT_CORS: Final[list[str]] = [
"https://cast.home-assistant.io"]
97 NO_LOGIN_ATTEMPT_THRESHOLD: Final = -1
99 MAX_CLIENT_SIZE: Final = 1024**2 * 16
100 MAX_LINE_SIZE: Final = 24570
102 STORAGE_KEY: Final = DOMAIN
103 STORAGE_VERSION: Final = 1
104 SAVE_DELAY: Final = 180
106 _HAS_IPV6 = hasattr(socket,
"AF_INET6")
107 _DEFAULT_BIND = [
"0.0.0.0",
"::"]
if _HAS_IPV6
else [
"0.0.0.0"]
109 HTTP_SCHEMA: Final = vol.All(
110 cv.deprecated(CONF_BASE_URL),
113 vol.Optional(CONF_SERVER_HOST, default=_DEFAULT_BIND): vol.All(
114 cv.ensure_list, vol.Length(min=1), [cv.string]
116 vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
117 vol.Optional(CONF_BASE_URL): cv.string,
118 vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
119 vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile,
120 vol.Optional(CONF_SSL_KEY): cv.isfile,
121 vol.Optional(CONF_CORS_ORIGINS, default=DEFAULT_CORS): vol.All(
122 cv.ensure_list, [cv.string]
124 vol.Inclusive(CONF_USE_X_FORWARDED_FOR,
"proxy"): cv.boolean,
125 vol.Inclusive(CONF_TRUSTED_PROXIES,
"proxy"): vol.All(
126 cv.ensure_list, [ip_network]
129 CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD
130 ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD),
131 vol.Optional(CONF_IP_BAN_ENABLED, default=
True): cv.boolean,
132 vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In(
133 [SSL_INTERMEDIATE, SSL_MODERN]
135 vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=
True): cv.boolean,
140 CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA)
143 @dataclass(slots=True)
145 """Configuration for a static path."""
149 cache_headers: bool =
True
153 True: CachingStaticResource,
154 False: web.StaticResource,
159 """Typed dict for config data."""
161 server_host: list[str]
165 ssl_peer_certificate: str
167 cors_allowed_origins: list[str]
168 use_x_forwarded_for: bool
169 use_x_frame_options: bool
170 trusted_proxies: list[IPv4Network | IPv6Network]
171 login_attempts_threshold: int
178 """Return the last known working config."""
179 store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
180 return await store.async_load()
184 """Configuration settings for API server."""
193 """Initialize a new API config object."""
200 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
201 """Set up the HTTP API and debug interface."""
206 conf: ConfData |
None = config.get(DOMAIN)
209 conf = cast(ConfData, HTTP_SCHEMA({}))
211 server_host = conf[CONF_SERVER_HOST]
212 server_port = conf[CONF_SERVER_PORT]
213 ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
214 ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
215 ssl_key = conf.get(CONF_SSL_KEY)
216 cors_origins = conf[CONF_CORS_ORIGINS]
217 use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR,
False)
218 use_x_frame_options = conf[CONF_USE_X_FRAME_OPTIONS]
219 trusted_proxies = conf.get(CONF_TRUSTED_PROXIES)
or []
220 is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
221 login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
222 ssl_profile = conf[CONF_SSL_PROFILE]
228 server_host=server_host,
229 server_port=server_port,
230 ssl_certificate=ssl_certificate,
231 ssl_peer_certificate=ssl_peer_certificate,
233 trusted_proxies=trusted_proxies,
234 ssl_profile=ssl_profile,
236 await server.async_initialize(
237 cors_origins=cors_origins,
238 use_x_forwarded_for=use_x_forwarded_for,
239 login_threshold=login_threshold,
240 is_ban_enabled=is_ban_enabled,
241 use_x_frame_options=use_x_frame_options,
244 async
def stop_server(event: Event) ->
None:
245 """Stop the server."""
248 async
def start_server(*_: Any) ->
None:
249 """Start the server."""
251 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
253 assert conf
is not None
260 local_ip = await source_ip_task
263 if server_host
is not None:
265 host = server_host[0]
268 local_ip, host, server_port, ssl_certificate
is not None
272 def _async_check_ssl_issue(_: Event) ->
None:
274 ssl_certificate
is not None
275 and (hass.config.external_url
or hass.config.internal_url)
is None
285 except CloudNotAvailable:
286 ir.async_create_issue(
289 "ssl_configured_without_configured_urls",
291 severity=ir.IssueSeverity.ERROR,
292 translation_key=
"ssl_configured_without_configured_urls",
295 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_check_ssl_issue)
301 """Home Assistant request object."""
303 async
def json(self, *, loads: JSONDecoder = json_loads) -> Any:
304 """Return body as JSON."""
311 """Home Assistant application."""
315 message: RawRequestMessage,
316 payload: StreamReader,
317 protocol: RequestHandler,
318 writer: AbstractStreamWriter,
319 task: asyncio.Task[
None],
320 _cls: type[web.Request] = HomeAssistantRequest,
322 """Create request instance."""
331 client_max_size=self._client_max_size,
336 path: str, request: web.Request
337 ) -> web.FileResponse:
338 return web.FileResponse(path, headers=CACHE_HEADERS)
341 async
def _serve_file(path: str, request: web.Request) -> web.FileResponse:
342 return web.FileResponse(path)
346 """HTTP server for Home Assistant."""
351 ssl_certificate: str |
None,
352 ssl_peer_certificate: str |
None,
354 server_host: list[str] |
None,
356 trusted_proxies: list[IPv4Network | IPv6Network],
359 """Initialize the HTTP Home Assistant server."""
362 client_max_size=MAX_CLIENT_SIZE,
364 "max_line_size": MAX_LINE_SIZE,
365 "max_field_size": MAX_LINE_SIZE,
376 self.
runnerrunner: web.AppRunner |
None =
None
377 self.
sitesite: HomeAssistantTCPSite |
None =
None
378 self.
contextcontext: ssl.SSLContext |
None =
None
383 cors_origins: list[str],
384 use_x_forwarded_for: bool,
385 login_threshold: int,
386 is_ban_enabled: bool,
387 use_x_frame_options: bool,
389 """Initialize the server."""
390 self.
appapp[KEY_HASS] = self.
hasshass
391 self.
appapp[
"hass"] = self.
hasshass
414 def register_view(self, view: HomeAssistantView | type[HomeAssistantView]) ->
None:
415 """Register a view with the WSGI server.
417 The view argument must be a class that inherits from HomeAssistantView.
418 It is optional to instantiate it before registering; this method will
419 handle it either way.
421 if isinstance(view, type):
425 if not hasattr(view,
"url"):
426 class_name = view.__class__.__name__
427 raise AttributeError(f
'{class_name} missing required attribute "url"')
429 if not hasattr(view,
"name"):
430 class_name = view.__class__.__name__
431 raise AttributeError(f
'{class_name} missing required attribute "name"')
433 view.register(self.
hasshass, self.
appapp, self.
appapp.router)
438 redirect_to: StrOrURL,
440 redirect_exc: type[HTTPRedirection] = HTTPMovedPermanently,
442 """Register a redirect with the server.
444 If given this must be either a string or callable. In case of a
445 callable it's called with the url adapter that triggered the match and
446 the values of the URL as keyword arguments and has to return the target
447 for the redirect, otherwise it has to be a string with placeholders in
451 async
def redirect(request: web.Request) -> web.StreamResponse:
452 """Redirect to location."""
454 raise redirect_exc(redirect_to)
456 self.
appapp[KEY_ALLOW_CONFIGURED_CORS](
457 self.
appapp.router.add_route(
"GET", url, redirect)
461 self, configs: Collection[StaticPathConfig]
462 ) -> dict[str, CachingStaticResource | web.StaticResource |
None]:
463 """Create a list of static resources."""
465 config.url_path: _STATIC_CLASSES[config.cache_headers](
466 config.url_path, config.path
468 if os.path.isdir(config.path)
470 for config
in configs
474 self, configs: Collection[StaticPathConfig]
476 """Register a folder or file to serve as a static path."""
477 resources = await self.
hasshass.async_add_executor_job(
485 configs: Collection[StaticPathConfig],
486 resources: dict[str, CachingStaticResource | web.StaticResource |
None],
488 """Register a folders or files to serve as a static path."""
490 allow_cors = app[KEY_ALLOW_CONFIGURED_CORS]
491 for config
in configs:
492 if resource := resources[config.url_path]:
493 app.router.register_resource(resource)
497 _serve_file_with_cache_headers
if config.cache_headers
else _serve_file
500 self.
appapp.router.add_route(
501 "GET", config.url_path, partial(target, config.path)
506 self, url_path: str, path: str, cache_headers: bool =
True
508 """Register a folder or file to serve as a static path."""
510 "calls hass.http.register_static_path which is deprecated because "
511 "it does blocking I/O in the event loop, instead "
512 "call `await hass.http.async_register_static_paths("
513 f
'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`',
514 exclude_integrations={
"http"},
515 core_behavior=frame.ReportBehavior.LOG,
516 breaks_in_ha_version=
"2025.7",
523 context: ssl.SSLContext |
None =
None
526 if self.
ssl_profilessl_profile == SSL_INTERMEDIATE:
527 context = ssl_util.server_context_intermediate()
529 context = ssl_util.server_context_modern()
531 except OSError
as error:
532 if not self.
hasshass.config.recovery_mode:
534 f
"Could not use SSL certificate from {self.ssl_certificate}:"
538 "Could not read SSL certificate from %s: %s",
544 except OSError
as error2:
546 "Could not create an emergency self signed ssl certificate: %s",
552 "Home Assistant is running in recovery mode with an emergency self"
553 " signed ssl certificate because the configured SSL certificate was"
561 "Failed to create ssl context, no fallback available because a peer"
562 " certificate is required."
565 context.verify_mode = ssl.CERT_REQUIRED
571 """Create an emergency ssl certificate so we can still startup."""
572 context = ssl_util.server_context_modern()
575 host = cast(str,
URL(
get_url(self.
hasshass, prefer_external=
True)).host)
576 except NoURLAvailableError:
577 host =
"homeassistant.local"
578 key = rsa.generate_private_key(
579 public_exponent=65537,
582 subject = issuer = x509.Name(
585 NameOID.ORGANIZATION_NAME,
"Home Assistant Emergency Certificate"
587 x509.NameAttribute(NameOID.COMMON_NAME, host),
590 now = dt_util.utcnow()
592 x509.CertificateBuilder()
593 .subject_name(subject)
595 .public_key(key.public_key())
596 .serial_number(x509.random_serial_number())
597 .not_valid_before(now)
598 .not_valid_after(now + datetime.timedelta(days=30))
600 x509.SubjectAlternativeName([x509.DNSName(host)]),
603 .sign(key, hashes.SHA256())
605 with NamedTemporaryFile()
as cert_pem, NamedTemporaryFile()
as key_pem:
606 cert_pem.write(cert.public_bytes(serialization.Encoding.PEM))
609 serialization.Encoding.PEM,
610 format=serialization.PrivateFormat.TraditionalOpenSSL,
611 encryption_algorithm=serialization.NoEncryption(),
616 context.load_cert_chain(cert_pem.name, key_pem.name)
620 """Start the aiohttp server."""
625 self.
appapp._router.freeze =
lambda:
None
628 self.
appapp, handler_cancellation=
True, shutdown_timeout=10
637 except OSError
as error:
639 "Failed to create HTTP server at port %d: %s", self.
server_portserver_port, error
642 _LOGGER.info(
"Now listening on port %d", self.
server_portserver_port)
645 """Stop the aiohttp server."""
646 if self.
sitesite
is not None:
648 if self.
runnerrunner
is not None:
649 await self.
runnerrunner.cleanup()
653 hass: HomeAssistant, conf: dict, server: HomeAssistantHTTP
655 """Startup the http server and save the config."""
659 store: storage.Store[dict[str, Any]] = storage.Store(
660 hass, STORAGE_VERSION, STORAGE_KEY
663 if CONF_TRUSTED_PROXIES
in conf:
664 conf[CONF_TRUSTED_PROXIES] = [
665 str(cast(IPv4Network | IPv6Network, ip).network_address)
666 for ip
in conf[CONF_TRUSTED_PROXIES]
669 store.async_delay_save(
lambda: conf, SAVE_DELAY)
None __init__(self, str local_ip, str host, int port, bool use_ssl)
web.Request _make_request(self, RawRequestMessage message, StreamReader payload, RequestHandler protocol, AbstractStreamWriter writer, asyncio.Task[None] task, type[web.Request] _cls=HomeAssistantRequest)
None async_initialize(self, *list[str] cors_origins, bool use_x_forwarded_for, int login_threshold, bool is_ban_enabled, bool use_x_frame_options)
None async_register_static_paths(self, Collection[StaticPathConfig] configs)
ssl.SSLContext _create_emergency_ssl_context(self)
ssl.SSLContext|None _create_ssl_context(self)
None __init__(self, HomeAssistant hass, str|None ssl_certificate, str|None ssl_peer_certificate, str|None ssl_key, list[str]|None server_host, int server_port, list[IPv4Network|IPv6Network] trusted_proxies, str ssl_profile)
None register_view(self, HomeAssistantView|type[HomeAssistantView] view)
dict[str, CachingStaticResource|web.StaticResource|None] _make_static_resources(self, Collection[StaticPathConfig] configs)
None _async_register_static_paths(self, Collection[StaticPathConfig] configs, dict[str, CachingStaticResource|web.StaticResource|None] resources)
None register_static_path(self, str url_path, str path, bool cache_headers=True)
None register_redirect(self, str url, StrOrURL redirect_to, *type[HTTPRedirection] redirect_exc=HTTPMovedPermanently)
Any json(self, *JSONDecoder loads=json_loads)
str async_remote_ui_url(HomeAssistant hass)
None async_setup_auth(HomeAssistant hass, Application app)
None setup_bans(HomeAssistant hass, Application app, int login_threshold)
None setup_cors(Application app, list[str] origins)
None async_setup_forwarded(Application app, bool|None use_x_forwarded_for, list[IPv4Network|IPv6Network] trusted_proxies)
None setup_request_context(Application app, ContextVar[Request|None] context)
None setup_security_filter(Application app)
dict[str, Any]|None async_get_last_config(HomeAssistant hass)
web.FileResponse _serve_file_with_cache_headers(str path, web.Request request)
web.FileResponse _serve_file(str path, web.Request request)
None start_http_server_and_save_config(HomeAssistant hass, dict conf, HomeAssistantHTTP server)
bool async_setup(HomeAssistant hass, ConfigType config)
str async_get_source_ip(HomeAssistant hass, str|UndefinedType target_ip=UNDEFINED)
bool setup(HomeAssistant hass, ConfigType config)
ModuleType async_import_module(HomeAssistant hass, str name)
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
None async_when_setup_or_start(core.HomeAssistant hass, str component, Callable[[core.HomeAssistant, str], Awaitable[None]] when_setup_cb)
Generator[None] async_start_setup(core.HomeAssistant hass, str integration, SetupPhases phase, str|None group=None)