Prevent invalid data from being passed to zeroconf (#39009)

This commit is contained in:
J. Nick Koston 2020-08-21 07:31:17 -05:00 committed by GitHub
parent 7c346c2f7c
commit 5a9246468e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 65 additions and 2 deletions

View file

@ -55,6 +55,13 @@ HOMEKIT_PROPERTIES = "properties"
HOMEKIT_PAIRED_STATUS_FLAG = "sf" HOMEKIT_PAIRED_STATUS_FLAG = "sf"
HOMEKIT_MODEL = "md" HOMEKIT_MODEL = "md"
# Property key=value has a max length of 255
# so we use 230 to leave space for key=
MAX_PROPERTY_VALUE_LEN = 230
# Dns label max length
MAX_NAME_LEN = 63
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@ -145,8 +152,10 @@ def setup(hass, config):
hass.helpers.instance_id.async_get(), hass.loop hass.helpers.instance_id.async_get(), hass.loop
).result() ).result()
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
params = { params = {
"location_name": hass.config.location_name, "location_name": valid_location_name,
"uuid": uuid, "uuid": uuid,
"version": __version__, "version": __version__,
"external_url": "", "external_url": "",
@ -178,9 +187,11 @@ def setup(hass, config):
except OSError: except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
_suppress_invalid_properties(params)
info = ServiceInfo( info = ServiceInfo(
ZEROCONF_TYPE, ZEROCONF_TYPE,
name=f"{hass.config.location_name}.{ZEROCONF_TYPE}", name=f"{valid_location_name}.{ZEROCONF_TYPE}",
server=f"{uuid}.local.", server=f"{uuid}.local.",
addresses=[host_ip_pton], addresses=[host_ip_pton],
port=hass.http.server_port, port=hass.http.server_port,
@ -358,3 +369,33 @@ def info_from_service(service):
} }
return info return info
def _suppress_invalid_properties(properties):
"""Suppress any properties that will cause zeroconf to fail to startup."""
for prop, prop_value in properties.items():
if not isinstance(prop_value, str):
continue
if len(prop_value.encode("utf-8")) > MAX_PROPERTY_VALUE_LEN:
_LOGGER.error(
"The property '%s' was suppressed because it is longer than the maximum length of %d bytes: %s",
prop,
MAX_PROPERTY_VALUE_LEN,
prop_value,
)
properties[prop] = ""
def _truncate_location_name_to_valid(location_name):
"""Truncate or return the location name usable for zeroconf."""
if len(location_name.encode("utf-8")) < MAX_NAME_LEN:
return location_name
_LOGGER.warning(
"The location name was truncated because it is longer than the maximum length of %d bytes: %s",
MAX_NAME_LEN,
location_name,
)
return location_name.encode("utf-8")[:MAX_NAME_LEN].decode("utf-8", "ignore")

View file

@ -102,6 +102,28 @@ async def test_setup(hass, mock_zeroconf):
assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf
async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog):
"""Test we still setup with long urls and names."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object(
zeroconf, "HaServiceBrowser", side_effect=service_update_mock
) as mock_service_browser, patch(
"homeassistant.components.zeroconf.get_url",
return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value",
), patch.object(
hass.config,
"location_name",
"\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string",
):
mock_zeroconf.get_service_info.side_effect = get_service_info_mock
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_service_browser.mock_calls) == 1
assert "https://this.url.is.way.too.long" in caplog.text
assert "German Umlaut" in caplog.text
async def test_setup_with_default_interface(hass, mock_zeroconf): async def test_setup_with_default_interface(hass, mock_zeroconf):
"""Test default interface config.""" """Test default interface config."""
with patch.object(hass.config_entries.flow, "async_init"), patch.object( with patch.object(hass.config_entries.flow, "async_init"), patch.object(