2016-11-28 00:26:46 +00:00
|
|
|
"""Helper for aiohttp webclient stuff."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
import asyncio
|
2021-09-29 14:32:11 +00:00
|
|
|
from collections.abc import Awaitable, Callable
|
2021-03-23 13:36:43 +00:00
|
|
|
from contextlib import suppress
|
2019-09-04 03:36:04 +00:00
|
|
|
from ssl import SSLContext
|
2019-12-09 15:42:10 +00:00
|
|
|
import sys
|
2021-06-04 16:14:18 +00:00
|
|
|
from types import MappingProxyType
|
2022-07-14 20:37:12 +00:00
|
|
|
from typing import TYPE_CHECKING, Any, cast
|
2017-01-19 17:55:27 +00:00
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
import aiohttp
|
2017-01-19 17:55:27 +00:00
|
|
|
from aiohttp import web
|
2019-12-09 15:42:10 +00:00
|
|
|
from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT
|
|
|
|
from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout
|
2017-01-19 17:55:27 +00:00
|
|
|
import async_timeout
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2021-04-09 17:14:33 +00:00
|
|
|
from homeassistant import config_entries
|
2022-09-29 01:24:04 +00:00
|
|
|
from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__
|
2021-03-27 11:55:24 +00:00
|
|
|
from homeassistant.core import Event, HomeAssistant, callback
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2018-07-16 08:32:07 +00:00
|
|
|
from homeassistant.util import ssl as ssl_util
|
2023-02-16 10:37:57 +00:00
|
|
|
from homeassistant.util.json import json_loads
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2021-12-23 19:14:47 +00:00
|
|
|
from .frame import warn_use
|
2023-02-16 10:37:57 +00:00
|
|
|
from .json import json_dumps
|
2022-07-14 20:37:12 +00:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
from aiohttp.typedefs import JSONDecoder
|
|
|
|
|
2021-12-23 19:14:47 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_CONNECTOR = "aiohttp_connector"
|
|
|
|
DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify"
|
|
|
|
DATA_CLIENTSESSION = "aiohttp_clientsession"
|
|
|
|
DATA_CLIENTSESSION_NOTVERIFY = "aiohttp_clientsession_notverify"
|
2022-09-29 01:24:04 +00:00
|
|
|
SERVER_SOFTWARE = "{0}/{1} aiohttp/{2} Python/{3[0]}.{3[1]}".format(
|
|
|
|
APPLICATION_NAME, __version__, aiohttp.__version__, sys.version_info
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2023-05-15 19:29:41 +00:00
|
|
|
ENABLE_CLEANUP_CLOSED = not (3, 11, 1) <= sys.version_info < (3, 11, 4)
|
2023-05-14 00:15:02 +00:00
|
|
|
# Enabling cleanup closed on python 3.11.1+ leaks memory relatively quickly
|
|
|
|
# see https://github.com/aio-libs/aiohttp/issues/7252
|
|
|
|
# aiohttp interacts poorly with https://github.com/python/cpython/pull/98540
|
2023-05-15 19:29:41 +00:00
|
|
|
# The issue was fixed in 3.11.4 via https://github.com/python/cpython/pull/104485
|
2023-05-14 00:15:02 +00:00
|
|
|
|
2021-04-09 17:14:33 +00:00
|
|
|
WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session"
|
|
|
|
|
2023-03-13 02:57:22 +00:00
|
|
|
#
|
|
|
|
# The default connection limit of 100 meant that you could only have
|
|
|
|
# 100 concurrent connections.
|
|
|
|
#
|
|
|
|
# This was effectively a limit of 100 devices and than
|
|
|
|
# the supervisor API would fail as soon as it was hit.
|
|
|
|
#
|
|
|
|
# We now apply the 100 limit per host, so that we can have 100 connections
|
|
|
|
# to a single host, but can have more than 4096 connections in total to
|
|
|
|
# prevent a single host from using all available connections.
|
|
|
|
#
|
|
|
|
MAXIMUM_CONNECTIONS = 4096
|
|
|
|
MAXIMUM_CONNECTIONS_PER_HOST = 100
|
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2022-07-14 20:37:12 +00:00
|
|
|
class HassClientResponse(aiohttp.ClientResponse):
|
|
|
|
"""aiohttp.ClientResponse with a json method that uses json_loads by default."""
|
|
|
|
|
|
|
|
async def json(
|
|
|
|
self,
|
|
|
|
*args: Any,
|
|
|
|
loads: JSONDecoder = json_loads,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> Any:
|
|
|
|
"""Send a json request and parse the json response."""
|
|
|
|
return await super().json(*args, loads=loads, **kwargs)
|
|
|
|
|
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_get_clientsession(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant, verify_ssl: bool = True
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> aiohttp.ClientSession:
|
2016-11-28 00:26:46 +00:00
|
|
|
"""Return default aiohttp ClientSession.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2021-04-09 17:14:33 +00:00
|
|
|
key = DATA_CLIENTSESSION if verify_ssl else DATA_CLIENTSESSION_NOTVERIFY
|
2016-11-28 00:26:46 +00:00
|
|
|
|
|
|
|
if key not in hass.data:
|
2021-04-09 17:14:33 +00:00
|
|
|
hass.data[key] = _async_create_clientsession(
|
|
|
|
hass,
|
|
|
|
verify_ssl,
|
|
|
|
auto_cleanup_method=_async_register_default_clientsession_shutdown,
|
|
|
|
)
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2019-02-07 21:34:14 +00:00
|
|
|
return cast(aiohttp.ClientSession, hass.data[key])
|
2016-11-28 00:26:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_create_clientsession(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant,
|
2019-07-31 19:25:30 +00:00
|
|
|
verify_ssl: bool = True,
|
|
|
|
auto_cleanup: bool = True,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> aiohttp.ClientSession:
|
2016-11-28 00:26:46 +00:00
|
|
|
"""Create a new ClientSession with kwargs, i.e. for cookies.
|
|
|
|
|
|
|
|
If auto_cleanup is False, you need to call detach() after the session
|
|
|
|
returned is no longer used. Default is True, the session will be
|
2021-04-09 17:14:33 +00:00
|
|
|
automatically detached on homeassistant_stop or when being created
|
|
|
|
in config entry setup, the config entry is unloaded.
|
2016-11-28 00:26:46 +00:00
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2021-04-09 17:14:33 +00:00
|
|
|
auto_cleanup_method = None
|
|
|
|
if auto_cleanup:
|
|
|
|
auto_cleanup_method = _async_register_clientsession_shutdown
|
|
|
|
|
|
|
|
clientsession = _async_create_clientsession(
|
|
|
|
hass,
|
|
|
|
verify_ssl,
|
|
|
|
auto_cleanup_method=auto_cleanup_method,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
|
|
|
return clientsession
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2021-04-09 17:14:33 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_create_clientsession(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
verify_ssl: bool = True,
|
|
|
|
auto_cleanup_method: Callable[[HomeAssistant, aiohttp.ClientSession], None]
|
|
|
|
| None = None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> aiohttp.ClientSession:
|
|
|
|
"""Create a new ClientSession with kwargs, i.e. for cookies."""
|
2016-11-28 00:26:46 +00:00
|
|
|
clientsession = aiohttp.ClientSession(
|
2021-04-09 17:14:33 +00:00
|
|
|
connector=_async_get_connector(hass, verify_ssl),
|
2022-07-11 21:46:55 +00:00
|
|
|
json_serialize=json_dumps,
|
2022-07-14 20:37:12 +00:00
|
|
|
response_class=HassClientResponse,
|
2020-08-27 11:56:20 +00:00
|
|
|
**kwargs,
|
2016-11-28 00:26:46 +00:00
|
|
|
)
|
2021-06-04 16:14:18 +00:00
|
|
|
# Prevent packages accidentally overriding our default headers
|
|
|
|
# It's important that we identify as Home Assistant
|
|
|
|
# If a package requires a different user agent, override it by passing a headers
|
|
|
|
# dictionary to the request method.
|
2023-01-20 12:47:55 +00:00
|
|
|
# pylint: disable-next=protected-access
|
2023-01-08 23:44:09 +00:00
|
|
|
clientsession._default_headers = MappingProxyType( # type: ignore[assignment]
|
|
|
|
{USER_AGENT: SERVER_SOFTWARE},
|
|
|
|
)
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2023-03-08 21:57:54 +00:00
|
|
|
clientsession.close = warn_use( # type: ignore[method-assign]
|
2023-01-08 23:44:09 +00:00
|
|
|
clientsession.close,
|
|
|
|
WARN_CLOSE_MSG,
|
|
|
|
)
|
2020-05-13 07:58:33 +00:00
|
|
|
|
2021-04-09 17:14:33 +00:00
|
|
|
if auto_cleanup_method:
|
|
|
|
auto_cleanup_method(hass, clientsession)
|
2016-11-28 00:26:46 +00:00
|
|
|
|
|
|
|
return clientsession
|
|
|
|
|
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-02-07 21:34:14 +00:00
|
|
|
async def async_aiohttp_proxy_web(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant,
|
2019-07-31 19:25:30 +00:00
|
|
|
request: web.BaseRequest,
|
|
|
|
web_coro: Awaitable[aiohttp.ClientResponse],
|
|
|
|
buffer_size: int = 102400,
|
|
|
|
timeout: int = 10,
|
2021-03-17 17:34:19 +00:00
|
|
|
) -> web.StreamResponse | None:
|
2017-01-19 17:55:27 +00:00
|
|
|
"""Stream websession request to aiohttp web response."""
|
|
|
|
try:
|
2021-11-04 15:07:50 +00:00
|
|
|
async with async_timeout.timeout(timeout):
|
2018-07-23 12:36:36 +00:00
|
|
|
req = await web_coro
|
2017-03-30 07:50:53 +00:00
|
|
|
|
2017-03-31 09:55:22 +00:00
|
|
|
except asyncio.CancelledError:
|
|
|
|
# The user cancelled the request
|
2019-02-07 21:34:14 +00:00
|
|
|
return None
|
2017-03-31 09:55:22 +00:00
|
|
|
|
2017-03-30 07:50:53 +00:00
|
|
|
except asyncio.TimeoutError as err:
|
2017-03-31 09:55:22 +00:00
|
|
|
# Timeout trying to start the web request
|
2017-03-30 07:50:53 +00:00
|
|
|
raise HTTPGatewayTimeout() from err
|
|
|
|
|
|
|
|
except aiohttp.ClientError as err:
|
2017-03-31 09:55:22 +00:00
|
|
|
# Something went wrong with the connection
|
2017-03-30 07:50:53 +00:00
|
|
|
raise HTTPBadGateway() from err
|
2017-01-19 17:55:27 +00:00
|
|
|
|
2017-08-26 16:56:39 +00:00
|
|
|
try:
|
2018-07-23 12:36:36 +00:00
|
|
|
return await async_aiohttp_proxy_stream(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, request, req.content, req.headers.get(CONTENT_TYPE)
|
2017-08-26 16:56:39 +00:00
|
|
|
)
|
|
|
|
finally:
|
|
|
|
req.close()
|
2017-01-19 17:55:27 +00:00
|
|
|
|
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_aiohttp_proxy_stream(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant,
|
2019-07-31 19:25:30 +00:00
|
|
|
request: web.BaseRequest,
|
|
|
|
stream: aiohttp.StreamReader,
|
2021-03-17 17:34:19 +00:00
|
|
|
content_type: str | None,
|
2019-07-31 19:25:30 +00:00
|
|
|
buffer_size: int = 102400,
|
|
|
|
timeout: int = 10,
|
|
|
|
) -> web.StreamResponse:
|
2017-03-30 07:50:53 +00:00
|
|
|
"""Stream a stream to aiohttp web response."""
|
|
|
|
response = web.StreamResponse()
|
2020-10-25 12:16:23 +00:00
|
|
|
if content_type is not None:
|
|
|
|
response.content_type = content_type
|
2018-02-25 11:38:46 +00:00
|
|
|
await response.prepare(request)
|
2017-03-30 07:50:53 +00:00
|
|
|
|
2021-03-23 13:36:43 +00:00
|
|
|
# Suppressing something went wrong fetching data, closed connection
|
|
|
|
with suppress(asyncio.TimeoutError, aiohttp.ClientError):
|
2021-01-17 16:10:22 +00:00
|
|
|
while hass.is_running:
|
2021-11-04 15:07:50 +00:00
|
|
|
async with async_timeout.timeout(timeout):
|
2018-02-25 11:38:46 +00:00
|
|
|
data = await stream.read(buffer_size)
|
2017-03-30 07:50:53 +00:00
|
|
|
|
2017-01-24 19:43:36 +00:00
|
|
|
if not data:
|
|
|
|
break
|
2018-03-05 21:28:41 +00:00
|
|
|
await response.write(data)
|
2017-01-19 17:55:27 +00:00
|
|
|
|
2018-07-23 12:36:36 +00:00
|
|
|
return response
|
|
|
|
|
2017-01-19 17:55:27 +00:00
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
@callback
|
2019-02-07 21:34:14 +00:00
|
|
|
def _async_register_clientsession_shutdown(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant, clientsession: aiohttp.ClientSession
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> None:
|
2021-04-09 17:14:33 +00:00
|
|
|
"""Register ClientSession close on Home Assistant shutdown or config entry unload.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_close_websession(*_: Any) -> None:
|
|
|
|
"""Close websession."""
|
|
|
|
clientsession.detach()
|
|
|
|
|
|
|
|
unsub = hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_CLOSE, _async_close_websession
|
|
|
|
)
|
|
|
|
|
2021-10-17 18:08:11 +00:00
|
|
|
if not (config_entry := config_entries.current_entry.get()):
|
2021-04-09 17:14:33 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
config_entry.async_on_unload(unsub)
|
|
|
|
config_entry.async_on_unload(_async_close_websession)
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_register_default_clientsession_shutdown(
|
|
|
|
hass: HomeAssistant, clientsession: aiohttp.ClientSession
|
|
|
|
) -> None:
|
|
|
|
"""Register default ClientSession close on Home Assistant shutdown.
|
2016-11-28 00:26:46 +00:00
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
@callback
|
2019-02-07 21:34:14 +00:00
|
|
|
def _async_close_websession(event: Event) -> None:
|
2016-11-28 00:26:46 +00:00
|
|
|
"""Close websession."""
|
|
|
|
clientsession.detach()
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_websession)
|
2016-11-28 00:26:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def _async_get_connector(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant, verify_ssl: bool = True
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> aiohttp.BaseConnector:
|
2016-11-28 00:26:46 +00:00
|
|
|
"""Return the connector pool for aiohttp.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2018-03-15 20:49:49 +00:00
|
|
|
key = DATA_CONNECTOR if verify_ssl else DATA_CONNECTOR_NOTVERIFY
|
|
|
|
|
|
|
|
if key in hass.data:
|
2019-02-07 21:34:14 +00:00
|
|
|
return cast(aiohttp.BaseConnector, hass.data[key])
|
2017-02-13 05:24:07 +00:00
|
|
|
|
2016-11-28 00:26:46 +00:00
|
|
|
if verify_ssl:
|
2023-03-19 09:13:48 +00:00
|
|
|
ssl_context: bool | SSLContext = ssl_util.get_default_context()
|
2016-11-28 00:26:46 +00:00
|
|
|
else:
|
2023-03-24 07:40:47 +00:00
|
|
|
ssl_context = ssl_util.get_default_no_verify_context()
|
2018-03-15 20:49:49 +00:00
|
|
|
|
2023-03-13 02:57:22 +00:00
|
|
|
connector = aiohttp.TCPConnector(
|
2023-05-14 00:15:02 +00:00
|
|
|
enable_cleanup_closed=ENABLE_CLEANUP_CLOSED,
|
2023-03-13 02:57:22 +00:00
|
|
|
ssl=ssl_context,
|
|
|
|
limit=MAXIMUM_CONNECTIONS,
|
|
|
|
limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST,
|
|
|
|
)
|
2018-03-15 20:49:49 +00:00
|
|
|
hass.data[key] = connector
|
|
|
|
|
2019-02-07 21:34:14 +00:00
|
|
|
async def _async_close_connector(event: Event) -> None:
|
2018-03-15 20:49:49 +00:00
|
|
|
"""Close connector pool."""
|
2019-01-09 05:09:47 +00:00
|
|
|
await connector.close()
|
2018-03-15 20:49:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_connector)
|
2016-11-28 00:26:46 +00:00
|
|
|
|
2017-02-13 05:24:07 +00:00
|
|
|
return connector
|