diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 212da80312..567937a4db 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -185,7 +185,11 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_ return 0; } #else - server->sin_addr.s_addr = inet_addr(ip_address); + // inet_aton() reports failure separately from the address, so 255.255.255.255 needs no special case + if (inet_aton(ip_address, &server->sin_addr) == 0) { + errno = EINVAL; + return 0; + } #endif server->sin_port = htons(port); return sizeof(sockaddr_in); diff --git a/tests/components/socket/__init__.py b/tests/components/socket/__init__.py new file mode 100644 index 0000000000..8a3da97e23 --- /dev/null +++ b/tests/components/socket/__init__.py @@ -0,0 +1,16 @@ +# This file's presence makes pytest treat this directory as a package named "socket", +# shadowing the stdlib module -- no test_*.py (Python-level) tests can live here. +# required for cpp unit testing. +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() + real_to_code = manifest.to_code + + async def to_code_testing(config): + await real_to_code(config) + cg.add_define("USE_NETWORK_IPV6", True) + + manifest.to_code = to_code_testing diff --git a/tests/components/socket/conftest.py b/tests/components/socket/conftest.py deleted file mode 100644 index 5d93cac232..0000000000 --- a/tests/components/socket/conftest.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Configuration file for socket component tests.""" - -import pytest - -from esphome.core import CORE - - -@pytest.fixture(autouse=True) -def reset_core(): - """Reset CORE after each test.""" - yield - CORE.reset() diff --git a/tests/components/socket/test_sockaddr_host.cpp b/tests/components/socket/test_sockaddr_host.cpp new file mode 100644 index 0000000000..74ba5de8ce --- /dev/null +++ b/tests/components/socket/test_sockaddr_host.cpp @@ -0,0 +1,146 @@ +#include + +#include +#include + +#include "esphome/components/socket/socket.h" + +#ifdef USE_HOST + +namespace esphome::socket::testing { + +// ========================================================================= +// Independent of USE_NETWORK_IPV6 +// ========================================================================= + +TEST(SetSockaddr, IPv4Basic) { + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "192.168.1.1", 6053); + ASSERT_EQ(len, sizeof(sockaddr_in)); + auto *in = reinterpret_cast(&addr); + EXPECT_EQ(in->sin_family, AF_INET); + EXPECT_EQ(ntohs(in->sin_port), 6053); + EXPECT_EQ(in->sin_addr.s_addr, htonl(0xC0A80101)); +} + +TEST(SetSockaddr, IPv4Broadcast) { + // 255.255.255.255 is INADDR_NONE's bit pattern but also a valid address. + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "255.255.255.255", 12345); + ASSERT_EQ(len, sizeof(sockaddr_in)); + auto *in = reinterpret_cast(&addr); + EXPECT_EQ(in->sin_addr.s_addr, ESPHOME_INADDR_NONE); +} + +TEST(SetSockaddr, IPv4MalformedYieldsZeroAndEINVAL) { + struct sockaddr_storage addr {}; + errno = 0; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "not-an-ip", 1); + EXPECT_EQ(len, 0u); + EXPECT_EQ(errno, EINVAL); +} + +TEST(SetSockaddr, IPv4BufferTooSmallYieldsZeroAndEINVAL) { + struct sockaddr_storage addr {}; + errno = 0; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(sockaddr_in) - 1, "192.168.1.1", 1); + EXPECT_EQ(len, 0u); + EXPECT_EQ(errno, EINVAL); +} + +TEST(SetSockaddr, StringOverloadMatchesCStringOverload) { + struct sockaddr_storage addr_c {}; + struct sockaddr_storage addr_s {}; + socklen_t len_c = set_sockaddr((struct sockaddr *) &addr_c, sizeof(addr_c), "10.0.0.5", 80); + socklen_t len_s = set_sockaddr((struct sockaddr *) &addr_s, sizeof(addr_s), std::string("10.0.0.5"), 80); + ASSERT_EQ(len_c, len_s); + EXPECT_EQ(std::memcmp(&addr_c, &addr_s, len_c), 0); +} + +TEST(FormatSockaddrTo, IPv4RoundTrip) { + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "203.0.113.7", 1); + ASSERT_GT(len, 0u); + char buf[SOCKADDR_STR_LEN]; + size_t written = format_sockaddr_to((struct sockaddr *) &addr, len, buf); + EXPECT_GT(written, 0u); + EXPECT_STREQ(buf, "203.0.113.7"); +} + +TEST(FormatSockaddrTo, UnknownFamilyYieldsEmptyString) { + struct sockaddr addr {}; + addr.sa_family = AF_UNSPEC; + char buf[SOCKADDR_STR_LEN] = {'x', '\0'}; + size_t written = format_sockaddr_to(&addr, sizeof(addr), buf); + EXPECT_EQ(written, 0u); + EXPECT_STREQ(buf, ""); +} + +// ========================================================================= +// IPv6 - this directory's __init__.py forces USE_NETWORK_IPV6 on +// ========================================================================= + +#if USE_NETWORK_IPV6 + +TEST(SetSockaddr, IPv6Basic) { + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "::1", 443); + ASSERT_EQ(len, sizeof(sockaddr_in6)); + auto *in6 = reinterpret_cast(&addr); + EXPECT_EQ(in6->sin6_family, AF_INET6); + EXPECT_EQ(ntohs(in6->sin6_port), 443); + EXPECT_TRUE(IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr)); +} + +TEST(SetSockaddr, IPv6MalformedYieldsZeroAndEINVAL) { + struct sockaddr_storage addr {}; + errno = 0; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "gg::not-ipv6", 1); + EXPECT_EQ(len, 0u); + EXPECT_EQ(errno, EINVAL); +} + +TEST(SetSockaddrAny, FillsIPv6AnyAddressWithPort) { + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr_any((struct sockaddr *) &addr, sizeof(addr), 9999); + ASSERT_EQ(len, sizeof(sockaddr_in6)); + auto *in6 = reinterpret_cast(&addr); + EXPECT_EQ(in6->sin6_family, AF_INET6); + EXPECT_EQ(ntohs(in6->sin6_port), 9999); + EXPECT_TRUE(IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)); +} + +TEST(FormatSockaddrTo, IPv6RoundTrip) { + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "2001:db8::1", 1); + ASSERT_GT(len, 0u); + char buf[SOCKADDR_STR_LEN]; + size_t written = format_sockaddr_to((struct sockaddr *) &addr, len, buf); + EXPECT_GT(written, 0u); + EXPECT_STREQ(buf, "2001:db8::1"); +} + +TEST(FormatSockaddrTo, V4MappedFormatsAsPlainIPv4) { + struct sockaddr_storage addr {}; + socklen_t len = set_sockaddr((struct sockaddr *) &addr, sizeof(addr), "::ffff:198.51.100.9", 1); + ASSERT_GT(len, 0u); + char buf[SOCKADDR_STR_LEN]; + size_t written = format_sockaddr_to((struct sockaddr *) &addr, len, buf); + EXPECT_GT(written, 0u); + EXPECT_STREQ(buf, "198.51.100.9"); +} + +TEST(SocketIp, CreatesIPv6Socket) { + auto sock = socket_ip(SOCK_DGRAM, IPPROTO_UDP); + ASSERT_NE(sock, nullptr); + struct sockaddr_storage addr {}; + socklen_t len = sizeof(addr); + ASSERT_EQ(sock->getsockname((struct sockaddr *) &addr, &len), 0); + EXPECT_EQ(addr.ss_family, AF_INET6); +} + +#endif // USE_NETWORK_IPV6 + +} // namespace esphome::socket::testing + +#endif // USE_HOST