diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 269791f5e7..3d2cd8ef50 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -149,6 +149,21 @@ def require_wake_loop_threadsafe() -> None: IPV4_ALLOW_SCHEMA = cv.All(cv.ensure_list(cv.ipv4network), cv.Length(max=255)) +_HOST = cv.Any(cv.domain, cv.hostname) + + +def ipv4_host(value: object) -> str: + """Validate an IPv4 address or a hostname; the resolver behind it is IPv4 only.""" + value = cv.string(value) + try: + cv.ipv6address(value) + except cv.Invalid: + return _HOST(value) + raise cv.Invalid( + "IPv6 addresses are not supported, use an IPv4 address or a hostname" + ) + + def _network_order(addr: IPv4Address) -> int: """The s_addr value for addr on the little endian targets.""" return int.from_bytes(addr.packed, "little") diff --git a/esphome/components/tcp_uart/__init__.py b/esphome/components/tcp_uart/__init__.py index 8e14e690f9..5481c8b315 100644 --- a/esphome/components/tcp_uart/__init__.py +++ b/esphome/components/tcp_uart/__init__.py @@ -54,7 +54,7 @@ CONFIG_SCHEMA = cv.All( { "client": BASE_SCHEMA.extend( { - cv.Required(CONF_HOST): cv.string, + cv.Required(CONF_HOST): socket.ipv4_host, } ), "server": BASE_SCHEMA.extend( diff --git a/esphome/components/uart_tcp/__init__.py b/esphome/components/uart_tcp/__init__.py index 3829f046ca..6e09ecac23 100644 --- a/esphome/components/uart_tcp/__init__.py +++ b/esphome/components/uart_tcp/__init__.py @@ -44,7 +44,7 @@ BASE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "client": BASE_SCHEMA.extend({cv.Required(CONF_HOST): cv.string}), + "client": BASE_SCHEMA.extend({cv.Required(CONF_HOST): socket.ipv4_host}), "server": BASE_SCHEMA.extend( {cv.Optional(CONF_ALLOWED_IPS): socket.IPV4_ALLOW_SCHEMA} ), diff --git a/tests/unit_tests/components/socket/test_socket_ipv4_host.py b/tests/unit_tests/components/socket/test_socket_ipv4_host.py new file mode 100644 index 0000000000..7953447b95 --- /dev/null +++ b/tests/unit_tests/components/socket/test_socket_ipv4_host.py @@ -0,0 +1,23 @@ +"""Tests for the socket component's IPv4 host validator.""" + +import pytest + +from esphome.components import socket +import esphome.config_validation as cv + + +@pytest.mark.parametrize("value", ["192.0.2.10", "bridge.local", "bridge"]) +def test_ipv4_host_accepts_ipv4_address_and_hostname(value: str) -> None: + assert socket.ipv4_host(value) == value + + +@pytest.mark.parametrize("value", ["2001:db8::10", "::1", "::ffff:192.0.2.10"]) +def test_ipv4_host_rejects_ipv6_address(value: str) -> None: + with pytest.raises(cv.Invalid, match="IPv6 addresses are not supported"): + socket.ipv4_host(value) + + +@pytest.mark.parametrize("value", ["bridge host", ""]) +def test_ipv4_host_rejects_invalid_host(value: str) -> None: + with pytest.raises(cv.Invalid): + socket.ipv4_host(value)