diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 1eeef3917d7..f570a30baa6 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -55,6 +55,13 @@ HOMEKIT_PROPERTIES = "properties" HOMEKIT_PAIRED_STATUS_FLAG = "sf" 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( { DOMAIN: vol.Schema( @@ -145,8 +152,10 @@ def setup(hass, config): hass.helpers.instance_id.async_get(), hass.loop ).result() + valid_location_name = _truncate_location_name_to_valid(hass.config.location_name) + params = { - "location_name": hass.config.location_name, + "location_name": valid_location_name, "uuid": uuid, "version": __version__, "external_url": "", @@ -178,9 +187,11 @@ def setup(hass, config): except OSError: host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) + _suppress_invalid_properties(params) + info = ServiceInfo( ZEROCONF_TYPE, - name=f"{hass.config.location_name}.{ZEROCONF_TYPE}", + name=f"{valid_location_name}.{ZEROCONF_TYPE}", server=f"{uuid}.local.", addresses=[host_ip_pton], port=hass.http.server_port, @@ -358,3 +369,33 @@ def info_from_service(service): } 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") diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 32891a04262..73629596c35 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -102,6 +102,28 @@ async def test_setup(hass, 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): """Test default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object(