Add default headers to webserver responses (#97784)
* Add default headers to webserver responses * Set default server header * Fix other testspull/97462/head^2
parent
3df71eca45
commit
369a484a78
|
@ -53,6 +53,7 @@ from .const import ( # noqa: F401
|
||||||
)
|
)
|
||||||
from .cors import setup_cors
|
from .cors import setup_cors
|
||||||
from .forwarded import async_setup_forwarded
|
from .forwarded import async_setup_forwarded
|
||||||
|
from .headers import setup_headers
|
||||||
from .request_context import current_request, setup_request_context
|
from .request_context import current_request, setup_request_context
|
||||||
from .security_filter import setup_security_filter
|
from .security_filter import setup_security_filter
|
||||||
from .static import CACHE_HEADERS, CachingStaticResource
|
from .static import CACHE_HEADERS, CachingStaticResource
|
||||||
|
@ -69,6 +70,7 @@ CONF_SSL_PEER_CERTIFICATE: Final = "ssl_peer_certificate"
|
||||||
CONF_SSL_KEY: Final = "ssl_key"
|
CONF_SSL_KEY: Final = "ssl_key"
|
||||||
CONF_CORS_ORIGINS: Final = "cors_allowed_origins"
|
CONF_CORS_ORIGINS: Final = "cors_allowed_origins"
|
||||||
CONF_USE_X_FORWARDED_FOR: Final = "use_x_forwarded_for"
|
CONF_USE_X_FORWARDED_FOR: Final = "use_x_forwarded_for"
|
||||||
|
CONF_USE_X_FRAME_OPTIONS: Final = "use_x_frame_options"
|
||||||
CONF_TRUSTED_PROXIES: Final = "trusted_proxies"
|
CONF_TRUSTED_PROXIES: Final = "trusted_proxies"
|
||||||
CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold"
|
CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold"
|
||||||
CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled"
|
CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled"
|
||||||
|
@ -118,6 +120,7 @@ HTTP_SCHEMA: Final = vol.All(
|
||||||
vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In(
|
vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In(
|
||||||
[SSL_INTERMEDIATE, SSL_MODERN]
|
[SSL_INTERMEDIATE, SSL_MODERN]
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -136,6 +139,7 @@ class ConfData(TypedDict, total=False):
|
||||||
ssl_key: str
|
ssl_key: str
|
||||||
cors_allowed_origins: list[str]
|
cors_allowed_origins: list[str]
|
||||||
use_x_forwarded_for: bool
|
use_x_forwarded_for: bool
|
||||||
|
use_x_frame_options: bool
|
||||||
trusted_proxies: list[IPv4Network | IPv6Network]
|
trusted_proxies: list[IPv4Network | IPv6Network]
|
||||||
login_attempts_threshold: int
|
login_attempts_threshold: int
|
||||||
ip_ban_enabled: bool
|
ip_ban_enabled: bool
|
||||||
|
@ -180,6 +184,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
ssl_key = conf.get(CONF_SSL_KEY)
|
ssl_key = conf.get(CONF_SSL_KEY)
|
||||||
cors_origins = conf[CONF_CORS_ORIGINS]
|
cors_origins = conf[CONF_CORS_ORIGINS]
|
||||||
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
||||||
|
use_x_frame_options = conf[CONF_USE_X_FRAME_OPTIONS]
|
||||||
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or []
|
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or []
|
||||||
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
||||||
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
||||||
|
@ -200,6 +205,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
use_x_forwarded_for=use_x_forwarded_for,
|
use_x_forwarded_for=use_x_forwarded_for,
|
||||||
login_threshold=login_threshold,
|
login_threshold=login_threshold,
|
||||||
is_ban_enabled=is_ban_enabled,
|
is_ban_enabled=is_ban_enabled,
|
||||||
|
use_x_frame_options=use_x_frame_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def stop_server(event: Event) -> None:
|
async def stop_server(event: Event) -> None:
|
||||||
|
@ -331,6 +337,7 @@ class HomeAssistantHTTP:
|
||||||
use_x_forwarded_for: bool,
|
use_x_forwarded_for: bool,
|
||||||
login_threshold: int,
|
login_threshold: int,
|
||||||
is_ban_enabled: bool,
|
is_ban_enabled: bool,
|
||||||
|
use_x_frame_options: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the server."""
|
"""Initialize the server."""
|
||||||
self.app[KEY_HASS] = self.hass
|
self.app[KEY_HASS] = self.hass
|
||||||
|
@ -348,6 +355,7 @@ class HomeAssistantHTTP:
|
||||||
|
|
||||||
await async_setup_auth(self.hass, self.app)
|
await async_setup_auth(self.hass, self.app)
|
||||||
|
|
||||||
|
setup_headers(self.app, use_x_frame_options)
|
||||||
setup_cors(self.app, cors_origins)
|
setup_cors(self.app, cors_origins)
|
||||||
|
|
||||||
if self.ssl_certificate:
|
if self.ssl_certificate:
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""Middleware that helps with the control of headers in our responses."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
from aiohttp.web import Application, Request, StreamResponse, middleware
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def setup_headers(app: Application, use_x_frame_options: bool) -> None:
|
||||||
|
"""Create headers middleware for the app."""
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def headers_middleware(
|
||||||
|
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
|
||||||
|
) -> StreamResponse:
|
||||||
|
"""Process request and add headers to the responses."""
|
||||||
|
response = await handler(request)
|
||||||
|
response.headers["Referrer-Policy"] = "no-referrer"
|
||||||
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
|
|
||||||
|
# Set an empty server header, to prevent aiohttp of setting one.
|
||||||
|
response.headers["Server"] = ""
|
||||||
|
|
||||||
|
if use_x_frame_options:
|
||||||
|
response.headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
app.middlewares.append(headers_middleware)
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""Test headers middleware."""
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from homeassistant.components.http.headers import setup_headers
|
||||||
|
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def mock_handler(request):
|
||||||
|
"""Return OK."""
|
||||||
|
return web.Response(text="OK")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_headers_added(aiohttp_client: ClientSessionGenerator) -> None:
|
||||||
|
"""Test that headers are being added on each request."""
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get("/", mock_handler)
|
||||||
|
|
||||||
|
setup_headers(app, use_x_frame_options=True)
|
||||||
|
|
||||||
|
mock_api_client = await aiohttp_client(app)
|
||||||
|
resp = await mock_api_client.get("/")
|
||||||
|
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
assert resp.headers["Referrer-Policy"] == "no-referrer"
|
||||||
|
assert resp.headers["Server"] == ""
|
||||||
|
assert resp.headers["X-Content-Type-Options"] == "nosniff"
|
||||||
|
assert resp.headers["X-Frame-Options"] == "SAMEORIGIN"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_allow_framing(aiohttp_client: ClientSessionGenerator) -> None:
|
||||||
|
"""Test that we allow framing when disabled."""
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get("/", mock_handler)
|
||||||
|
|
||||||
|
setup_headers(app, use_x_frame_options=False)
|
||||||
|
|
||||||
|
mock_api_client = await aiohttp_client(app)
|
||||||
|
resp = await mock_api_client.get("/")
|
||||||
|
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
assert "X-Frame-Options" not in resp.headers
|
|
@ -117,6 +117,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None:
|
||||||
"login_attempts_threshold": -1,
|
"login_attempts_threshold": -1,
|
||||||
"server_port": 8123,
|
"server_port": 8123,
|
||||||
"ssl_profile": "modern",
|
"ssl_profile": "modern",
|
||||||
|
"use_x_frame_options": True,
|
||||||
}
|
}
|
||||||
assert res["secret_cache"] == {
|
assert res["secret_cache"] == {
|
||||||
get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"}
|
get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"}
|
||||||
|
|
Loading…
Reference in New Issue