From ee55223065f3082ea1fdcae5e878dc4c55e2f961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Sun, 31 Jan 2021 17:59:14 +0100 Subject: [PATCH] SSDP response decode: replace invalid utf-8 characters (#42681) * SSDP response decode: replace invalid utf-8 characters * Add test to validate replaced data Co-authored-by: Joakim Plate --- homeassistant/components/ssdp/__init__.py | 4 +-- tests/components/ssdp/test_init.py | 43 +++++++++++++++++++++++ tests/test_util/aiohttp.py | 4 +-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e962c141bef..f07e88d811a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -171,13 +171,13 @@ class Scanner: session = self.hass.helpers.aiohttp_client.async_get_clientsession() try: resp = await session.get(xml_location, timeout=5) - xml = await resp.text() + xml = await resp.text(errors="replace") # Samsung Smart TV sometimes returns an empty document the # first time. Retry once. if not xml: resp = await session.get(xml_location, timeout=5) - xml = await resp.text() + xml = await resp.text(errors="replace") except (aiohttp.ClientError, asyncio.TimeoutError) as err: _LOGGER.debug("Error fetching %s: %s", xml_location, err) return {} diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 008995cd78d..bba809aedbb 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -170,3 +170,46 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ): await scanner.async_scan(None) + + +async def test_invalid_characters(hass, aioclient_mock): + """Test that we replace bad characters with placeholders.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + ABC + \xff\xff\xff\xff + + + """, + ) + scanner = ssdp.Scanner( + hass, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + }, + ) + + with patch( + "netdisco.ssdp.scan", + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["data"] == { + "ssdp_location": "http://1.1.1.1", + "ssdp_st": "mock-st", + "deviceType": "ABC", + "serialNumber": "ÿÿÿÿ", + } diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 53949c20b06..5219212f1cf 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -245,9 +245,9 @@ class AiohttpClientMockResponse: """Return mock response.""" return self.response - async def text(self, encoding="utf-8"): + async def text(self, encoding="utf-8", errors="strict"): """Return mock response as a string.""" - return self.response.decode(encoding) + return self.response.decode(encoding, errors=errors) async def json(self, encoding="utf-8", content_type=None): """Return mock response as a json."""