diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 637e9da6f9c..b249b7536b5 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -43,7 +43,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_ENCODING, + CONF_SSL_CIPHER_LIST, COORDINATOR, + DEFAULT_SSL_CIPHER_LIST, DOMAIN, PLATFORM_IDX, REST, @@ -185,6 +187,7 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res method: str = config[CONF_METHOD] payload: str | None = config.get(CONF_PAYLOAD) verify_ssl: bool = config[CONF_VERIFY_SSL] + ssl_cipher_list: str = config.get(CONF_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST) username: str | None = config.get(CONF_USERNAME) password: str | None = config.get(CONF_PASSWORD) headers: dict[str, str] | None = config.get(CONF_HEADERS) @@ -218,5 +221,6 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res params, payload, verify_ssl, + ssl_cipher_list, timeout, ) diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py index bdc0c5af492..0bf0ea9743d 100644 --- a/homeassistant/components/rest/const.py +++ b/homeassistant/components/rest/const.py @@ -1,12 +1,16 @@ """The rest component constants.""" +from homeassistant.util.ssl import SSLCipherList + DOMAIN = "rest" DEFAULT_METHOD = "GET" DEFAULT_VERIFY_SSL = True +DEFAULT_SSL_CIPHER_LIST = SSLCipherList.PYTHON_DEFAULT DEFAULT_FORCE_UPDATE = False DEFAULT_ENCODING = "UTF-8" CONF_ENCODING = "encoding" +CONF_SSL_CIPHER_LIST = "ssl_cipher_list" DEFAULT_BINARY_SENSOR_NAME = "REST Binary Sensor" DEFAULT_SENSOR_NAME = "REST Sensor" diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index a6501c769d2..8f1dd937391 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -9,6 +9,7 @@ import httpx from homeassistant.core import HomeAssistant from homeassistant.helpers import template from homeassistant.helpers.httpx_client import create_async_httpx_client +from homeassistant.util.ssl import SSLCipherList DEFAULT_TIMEOUT = 10 @@ -29,6 +30,7 @@ class RestData: params: dict[str, str] | None, data: str | None, verify_ssl: bool, + ssl_cipher_list: str, timeout: int = DEFAULT_TIMEOUT, ) -> None: """Initialize the data object.""" @@ -42,6 +44,7 @@ class RestData: self._request_data = data self._timeout = timeout self._verify_ssl = verify_ssl + self._ssl_cipher_list = SSLCipherList(ssl_cipher_list) self._async_client: httpx.AsyncClient | None = None self.data: str | None = None self.last_exception: Exception | None = None @@ -55,7 +58,10 @@ class RestData: """Get the latest data from REST service with provided method.""" if not self._async_client: self._async_client = create_async_httpx_client( - self._hass, verify_ssl=self._verify_ssl, default_encoding=self._encoding + self._hass, + verify_ssl=self._verify_ssl, + default_encoding=self._encoding, + ssl_cipher_list=self._ssl_cipher_list, ) rendered_headers = template.render_complex(self._headers, parse_result=False) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index 8e0fa9de00e..c5abe42d7fc 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -31,14 +31,17 @@ from homeassistant.helpers.template_entity import ( TEMPLATE_ENTITY_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA, ) +from homeassistant.util.ssl import SSLCipherList from .const import ( CONF_ENCODING, CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, + CONF_SSL_CIPHER_LIST, DEFAULT_ENCODING, DEFAULT_FORCE_UPDATE, DEFAULT_METHOD, + DEFAULT_SSL_CIPHER_LIST, DEFAULT_VERIFY_SSL, DOMAIN, METHODS, @@ -58,6 +61,10 @@ RESOURCE_SCHEMA = { vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional( + CONF_SSL_CIPHER_LIST, + default=DEFAULT_SSL_CIPHER_LIST, + ): vol.In([e.value for e in SSLCipherList]), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, } diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 3f7625da501..fd595ef07a6 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -2,7 +2,7 @@ import asyncio from http import HTTPStatus import ssl -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest @@ -30,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util.ssl import SSLCipherList from tests.common import get_fixture_path @@ -157,6 +158,44 @@ async def test_setup_encoding(hass: HomeAssistant) -> None: assert hass.states.get("sensor.mysensor").state == "tack själv" +@respx.mock +@pytest.mark.parametrize( + ("ssl_cipher_list", "ssl_cipher_list_expected"), + ( + ("python_default", SSLCipherList.PYTHON_DEFAULT), + ("intermediate", SSLCipherList.INTERMEDIATE), + ("modern", SSLCipherList.MODERN), + ), +) +async def test_setup_ssl_ciphers( + hass: HomeAssistant, ssl_cipher_list: str, ssl_cipher_list_expected: SSLCipherList +) -> None: + """Test setup with minimum configuration.""" + with patch( + "homeassistant.components.rest.data.create_async_httpx_client", + return_value=MagicMock(request=AsyncMock(return_value=respx.MockResponse())), + ) as httpx: + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + "resource": "http://localhost", + "method": "GET", + "ssl_cipher_list": ssl_cipher_list, + } + }, + ) + await hass.async_block_till_done() + httpx.assert_called_once_with( + hass, + verify_ssl=True, + default_encoding="UTF-8", + ssl_cipher_list=ssl_cipher_list_expected, + ) + + @respx.mock async def test_manual_update(hass: HomeAssistant) -> None: """Test setup with minimum configuration."""