From 2147ddf8c7f25fac1a941b1980afdd69ac9349bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 11:32:23 -1000 Subject: [PATCH] [api] Eliminate std::string from ClientInfo struct (#12566) Co-authored-by: Keith Burzinski --- esphome/components/api/api_connection.cpp | 42 +++++----- esphome/components/api/api_connection.h | 19 ++--- esphome/components/api/api_frame_helper.cpp | 10 ++- esphome/components/api/api_frame_helper.h | 28 ++++--- .../components/api/api_frame_helper_noise.cpp | 7 +- .../components/api/api_frame_helper_noise.h | 4 +- .../api/api_frame_helper_plaintext.cpp | 8 +- .../api/api_frame_helper_plaintext.h | 3 +- esphome/components/api/api_server.cpp | 14 ++-- .../components/esphome/ota/ota_esphome.cpp | 5 +- .../components/socket/bsd_sockets_impl.cpp | 40 +-------- .../components/socket/lwip_raw_tcp_impl.cpp | 57 ++++--------- .../components/socket/lwip_sockets_impl.cpp | 38 +-------- esphome/components/socket/socket.cpp | 81 +++++++++++++++++++ esphome/components/socket/socket.h | 20 ++++- .../voice_assistant/voice_assistant.cpp | 5 +- 16 files changed, 200 insertions(+), 181 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 27344a53ec..30f7b5710c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -101,16 +101,14 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) auto &noise_ctx = parent->get_noise_ctx(); if (noise_ctx.has_psk()) { - this->helper_ = - std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; } else { - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{ - new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -131,8 +129,9 @@ void APIConnection::start() { this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); return; } - this->client_info_.peername = helper_->getpeername(); - this->client_info_.name = this->client_info_.peername; + // Initialize client name with peername (IP address) until Hello message provides actual name + const char *peername = this->helper_->get_client_peername(); + this->helper_->set_client_name(peername, strlen(peername)); } APIConnection::~APIConnection() { @@ -252,8 +251,7 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); - ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), - this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { // Only send ping if we're not disconnecting @@ -287,7 +285,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); @@ -1504,9 +1502,10 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); - ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); + this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), + std::string(this->helper_->get_client_peername())); #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1521,12 +1520,12 @@ void APIConnection::complete_authentication_() { } bool APIConnection::send_hello_response(const HelloRequest &msg) { - this->client_info_.name.assign(msg.client_info.c_str(), msg.client_info.size()); - this->client_info_.peername = this->helper_->getpeername(); + // Copy client name with truncation if needed (set_client_name handles truncation) + this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), - this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), + this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -1836,7 +1835,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -2084,8 +2083,13 @@ void APIConnection::process_state_subscriptions_() { } #endif // USE_API_HOMEASSISTANT_STATES +void APIConnection::log_client_(int level, const LogString *message) { + esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(), + this->helper_->get_client_peername(), LOG_STR_ARG(message)); +} + void APIConnection::log_warning_(const LogString *message, APIError err) { - ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(), LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index cffd52bfdb..802681f32f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -9,18 +9,13 @@ #include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/string_ref.h" #include #include namespace esphome::api { -// Client information structure -struct ClientInfo { - std::string name; // Client name from Hello message - std::string peername; // IP:port from socket -}; - // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending @@ -279,8 +274,9 @@ class APIConnection final : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - const std::string &get_name() const { return this->client_info_.name; } - const std::string &get_peername() const { return this->client_info_.peername; } + const char *get_name() const { return this->helper_->get_client_name(); } + /// Get peer name (IP address) - cached at connection init time + const char *get_peername() const { return this->helper_->get_client_peername(); } protected: // Helper function to handle authentication completion @@ -526,10 +522,7 @@ class APIConnection final : public APIServerConnection { std::unique_ptr image_reader_; #endif - // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) - ClientInfo client_info_; - - // Group 4: 4-byte types + // Group 3: 4-byte types uint32_t last_traffic_; #ifdef USE_API_HOMEASSISTANT_STATES int state_subs_at_ = -1; @@ -756,6 +749,8 @@ class APIConnection final : public APIServerConnection { return this->schedule_batch_(); } + // Helper function to log client messages with name and peername + void log_client_(int level, const LogString *message); // Helper function to log API errors with errno void log_warning_(const LogString *message, APIError err); // Helper to handle fatal errors with logging diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 420f42a90a..dd44fe9e17 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,6 +1,5 @@ #include "api_frame_helper.h" #ifdef USE_API -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -16,8 +15,11 @@ static const char *const TAG = "api.frame_helper"; // Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t API_MAX_LOG_BYTES = 168; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) \ @@ -243,6 +245,8 @@ APIError APIFrameHelper::init_common_() { HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } + // Cache peername now while socket is valid - needed for error logging after socket failure + this->socket_->getpeername_to(this->client_peername_); int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 383e763e6d..76a93d094e 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -33,11 +33,11 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; -// Forward declaration -struct ClientInfo; - class ProtoWriteBuffer; +// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) +static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; + struct ReadPacketBuffer { const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; @@ -86,14 +86,23 @@ const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_(std::move(socket)), client_info_(client_info) {} + explicit APIFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + + // Get client name (null-terminated) + const char *get_client_name() const { return this->client_name_; } + // Get client peername/IP (null-terminated, cached at init time for availability after socket failure) + const char *get_client_peername() const { return this->client_peername_; } + // Set client name from buffer with length (truncates if needed) + void set_client_name(const char *name, size_t len) { + size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); + memcpy(this->client_name_, name, copy_len); + this->client_name_[copy_len] = '\0'; + } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } - std::string getpeername() { return socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { state_ = State::CLOSED; @@ -186,9 +195,10 @@ class APIFrameHelper { std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector rx_buf_; - // Pointer to client info (4 bytes on 32-bit) - // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. - const ClientInfo *client_info_{nullptr}; + // Client name buffer - stores name from Hello message or initial peername + char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; + // Cached peername/IP address - captured at init time for availability after socket failure + char client_peername_[socket::SOCKADDR_STR_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index be8d93fbf9..21b0463dfe 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -27,8 +27,11 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") // Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t API_MAX_LOG_BYTES = 168; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) \ diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 1268086194..183b8c8a51 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,8 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx) + : APIFrameHelper(std::move(socket)), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index a974a2458e..3dfd683929 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -1,7 +1,6 @@ #include "api_frame_helper_plaintext.h" #ifdef USE_API #ifdef USE_API_PLAINTEXT -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -21,8 +20,11 @@ static const char *const TAG = "api.plaintext"; // Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t API_MAX_LOG_BYTES = 168; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) \ diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 7af9fc64b9..96d47e9c7b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -7,8 +7,7 @@ namespace esphome::api { class APIPlaintextFrameHelper final : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info) { + explicit APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { // Plaintext header structure (worst case): // Pos 0: indicator (0x00) // Pos 1-3: payload size varint (up to 3 bytes) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a7b046447d..4ececfec94 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -125,15 +125,18 @@ void APIServer::loop() { if (!sock) break; + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + // Check if we're at the connection limit if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str()); + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); // Immediately close - socket destructor will handle cleanup sock.reset(); continue; } - ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); + ESP_LOGD(TAG, "Accept %s", peername); auto *conn = new APIConnection(std::move(sock), this); this->clients_.emplace_back(conn); @@ -166,8 +169,7 @@ void APIServer::loop() { // Network is down - disconnect all clients for (auto &client : this->clients_) { client->on_fatal_error(); - ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), - client->client_info_.peername.c_str()); + client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect")); } // Continue to process and clean up the clients below } @@ -185,12 +187,12 @@ void APIServer::loop() { // Rare case: handle disconnection #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername); + this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername())); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->unregister_active_action_calls_for_connection(client.get()); #endif - ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f71163f79e..dfa637f701 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -4,6 +4,7 @@ #include "esphome/components/sha256/sha256.h" #endif #include "esphome/components/network/util.h" +#include "esphome/components/socket/socket.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_libretiny.h" @@ -443,7 +444,9 @@ void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } void ESPHomeOTAComponent::log_start_(const LogString *phase) { - ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); + char peername[socket::SOCKADDR_STR_LEN]; + this->client_->getpeername_to(peername); + ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername); } void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 09cd81752a..73be025376 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -14,31 +14,7 @@ namespace esphome::socket { -std::string format_sockaddr(const struct sockaddr_storage &storage) { - if (storage.ss_family == AF_INET) { - const struct sockaddr_in *addr = reinterpret_cast(&storage); - char buf[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) != nullptr) - return std::string{buf}; - } -#if LWIP_IPV6 - else if (storage.ss_family == AF_INET6) { - const struct sockaddr_in6 *addr = reinterpret_cast(&storage); - char buf[INET6_ADDRSTRLEN]; - // Format IPv4-mapped IPv6 addresses as regular IPv4 addresses - if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && - addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && - inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf, sizeof(buf)) != nullptr) { - return std::string{buf}; - } - if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) != nullptr) - return std::string{buf}; - } -#endif - return {}; -} - -class BSDSocketImpl : public Socket { +class BSDSocketImpl final : public Socket { public: BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT @@ -93,23 +69,9 @@ class BSDSocketImpl : public Socket { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(this->fd_, addr, addrlen); } - std::string getpeername() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (::getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(this->fd_, addr, addrlen); } - std::string getsockname() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (::getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { return ::getsockopt(this->fd_, level, optname, optval, optlen); } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index cb5d17d5af..429f59ceca 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -71,7 +71,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return nullptr; } - int bind(const struct sockaddr *name, socklen_t addrlen) override { + int bind(const struct sockaddr *name, socklen_t addrlen) final { if (pcb_ == nullptr) { errno = EBADF; return -1; @@ -135,7 +135,7 @@ class LWIPRawImpl : public Socket { } return 0; } - int close() override { + int close() final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -152,7 +152,7 @@ class LWIPRawImpl : public Socket { pcb_ = nullptr; return 0; } - int shutdown(int how) override { + int shutdown(int how) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -178,7 +178,7 @@ class LWIPRawImpl : public Socket { return 0; } - int getpeername(struct sockaddr *name, socklen_t *addrlen) override { + int getpeername(struct sockaddr *name, socklen_t *addrlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -189,14 +189,7 @@ class LWIPRawImpl : public Socket { } return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); } - std::string getpeername() override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return ""; - } - return this->format_ip_address_(pcb_->remote_ip); - } - int getsockname(struct sockaddr *name, socklen_t *addrlen) override { + int getsockname(struct sockaddr *name, socklen_t *addrlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -207,14 +200,7 @@ class LWIPRawImpl : public Socket { } return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); } - std::string getsockname() override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return ""; - } - return this->format_ip_address_(pcb_->local_ip); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -248,7 +234,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return -1; } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -282,7 +268,7 @@ class LWIPRawImpl : public Socket { errno = EOPNOTSUPP; return -1; } - ssize_t read(void *buf, size_t len) override { + ssize_t read(void *buf, size_t len) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -340,7 +326,7 @@ class LWIPRawImpl : public Socket { return read; } - ssize_t readv(const struct iovec *iov, int iovcnt) override { + ssize_t readv(const struct iovec *iov, int iovcnt) final { ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); @@ -358,7 +344,7 @@ class LWIPRawImpl : public Socket { return ret; } - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) final { errno = ENOTSUP; return -1; } @@ -412,7 +398,7 @@ class LWIPRawImpl : public Socket { } return 0; } - ssize_t write(const void *buf, size_t len) override { + ssize_t write(const void *buf, size_t len) final { ssize_t written = internal_write(buf, len); if (written == -1) return -1; @@ -427,7 +413,7 @@ class LWIPRawImpl : public Socket { } return written; } - ssize_t writev(const struct iovec *iov, int iovcnt) override { + ssize_t writev(const struct iovec *iov, int iovcnt) final { ssize_t written = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); @@ -453,12 +439,12 @@ class LWIPRawImpl : public Socket { } return written; } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) final { // return ::sendto(fd_, buf, len, flags, to, tolen); errno = ENOSYS; return -1; } - int setblocking(bool blocking) override { + int setblocking(bool blocking) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -517,19 +503,6 @@ class LWIPRawImpl : public Socket { } protected: - std::string format_ip_address_(const ip_addr_t &ip) { - char buffer[50] = {}; - if (IP_IS_V4_VAL(ip)) { - inet_ntoa_r(ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(ip)) { - inet6_ntoa_r(ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); - } - int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { @@ -584,7 +557,7 @@ class LWIPRawImpl : public Socket { // Listening socket class - only allocates accept queue when needed (for bind+listen sockets) // This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040 -class LWIPRawListenImpl : public LWIPRawImpl { +class LWIPRawListenImpl final : public LWIPRawImpl { public: LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 23fb1a7f6f..a885f243f3 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -9,29 +9,7 @@ namespace esphome::socket { -std::string format_sockaddr(const struct sockaddr_storage &storage) { - if (storage.ss_family == AF_INET) { - const struct sockaddr_in *addr = reinterpret_cast(&storage); - char buf[INET_ADDRSTRLEN]; - const char *ret = lwip_inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } -#if LWIP_IPV6 - else if (storage.ss_family == AF_INET6) { - const struct sockaddr_in6 *addr = reinterpret_cast(&storage); - char buf[INET6_ADDRSTRLEN]; - const char *ret = lwip_inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } -#endif - return {}; -} - -class LwIPSocketImpl : public Socket { +class LwIPSocketImpl final : public Socket { public: LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT @@ -88,23 +66,9 @@ class LwIPSocketImpl : public Socket { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getpeername(this->fd_, addr, addrlen); } - std::string getpeername() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (lwip_getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getsockname(this->fd_, addr, addrlen); } - std::string getsockname() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (lwip_getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index ffe0233abc..c92e33393b 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,6 +10,87 @@ namespace esphome::socket { Socket::~Socket() {} +// Platform-specific inet_ntop wrappers +#if defined(USE_SOCKET_IMPL_LWIP_TCP) +// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + inet_ntoa_r(*reinterpret_cast(addr), buf, size); + return buf; +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + inet6_ntoa_r(*reinterpret_cast(addr), buf, size); + return buf; +} +#endif +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +// LWIP sockets (LibreTiny, ESP32 Arduino) +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + return lwip_inet_ntop(AF_INET, addr, buf, size); +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + return lwip_inet_ntop(AF_INET6, addr, buf, size); +} +#endif +#else +// BSD sockets (host, ESP32-IDF) +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + return inet_ntop(AF_INET, addr, buf, size); +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + return inet_ntop(AF_INET6, addr, buf, size); +} +#endif +#endif + +// Format sockaddr into caller-provided buffer, returns length written (excluding null) +static size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span buf) { + if (storage.ss_family == AF_INET) { + const auto *addr = reinterpret_cast(&storage); + if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr) + return strlen(buf.data()); + } +#if USE_NETWORK_IPV6 + else if (storage.ss_family == AF_INET6) { + const auto *addr = reinterpret_cast(&storage); +#ifndef USE_SOCKET_IMPL_LWIP_TCP + // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) + if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && + addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && + esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#endif + if (esphome_inet_ntop6(&addr->sin6_addr, buf.data(), buf.size()) != nullptr) + return strlen(buf.data()); + } +#endif + buf[0] = '\0'; + return 0; +} + +size_t Socket::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(storage, buf); +} + +size_t Socket::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(storage, buf); +} + std::unique_ptr socket_ip(int type, int protocol) { #if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 75eb07de4a..9f9f61de85 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include "esphome/core/optional.h" @@ -8,6 +9,15 @@ #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) namespace esphome::socket { +// Maximum length for formatted socket address string (IP address without port) +// IPv4: "255.255.255.255" = 15 chars + null = 16 +// IPv6: full address = 45 chars + null = 46 +#if USE_NETWORK_IPV6 +static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN +#else +static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +#endif + class Socket { public: Socket() = default; @@ -31,9 +41,15 @@ class Socket { virtual int shutdown(int how) = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual std::string getpeername() = 0; virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual std::string getsockname() = 0; + + /// Format peer address into a fixed-size buffer (no heap allocation) + /// Non-virtual wrapper around getpeername() - can be optimized away if unused + /// Returns number of characters written (excluding null terminator), or 0 on error + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + /// Non-virtual wrapper around getsockname() - can be optimized away if unused + size_t getsockname_to(std::span buf); virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; virtual int listen(int backlog) = 0; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 05c356ae4c..0e0616c508 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -3,6 +3,7 @@ #ifdef USE_VOICE_ASSISTANT +#include "esphome/components/socket/socket.h" #include "esphome/core/log.h" #include @@ -433,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr "Multiple API Clients attempting to connect to Voice Assistant\n" "Current client: %s (%s)\n" "New client: %s (%s)", - this->api_client_->get_name().c_str(), this->api_client_->get_peername().c_str(), - client->get_name().c_str(), client->get_peername().c_str()); + this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(), + client->get_peername()); return; }