[api] Eliminate std::string from ClientInfo struct (#12566)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
J. Nick Koston
2026-01-06 11:32:23 -10:00
committed by GitHub
parent 412ab5dbbf
commit 2147ddf8c7
16 changed files with 200 additions and 181 deletions

View File

@@ -101,16 +101,14 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto &noise_ctx = parent->get_noise_ctx(); auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) { if (noise_ctx.has_psk()) {
this->helper_ = this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else { } else {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
} }
#elif defined(USE_API_PLAINTEXT) #elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE) #elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{ this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)};
#else #else
#error "No frame helper defined" #error "No frame helper defined"
#endif #endif
@@ -131,8 +129,9 @@ void APIConnection::start() {
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
return; return;
} }
this->client_info_.peername = helper_->getpeername(); // Initialize client name with peername (IP address) until Hello message provides actual name
this->client_info_.name = this->client_info_.peername; const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
@@ -252,8 +251,7 @@ void APIConnection::loop() {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
this->client_info_.peername.c_str());
} }
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting // Only send ping if we're not disconnecting
@@ -287,7 +285,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // 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; this->flags_.next_close = true;
DisconnectResponse resp; DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
@@ -1504,9 +1502,10 @@ void APIConnection::complete_authentication_() {
} }
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(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 #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 #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1521,12 +1520,12 @@ void APIConnection::complete_authentication_() {
} }
bool APIConnection::send_hello_response(const HelloRequest &msg) { bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->client_info_.name.assign(msg.client_info.c_str(), msg.client_info.size()); // Copy client name with truncation if needed (set_client_name handles truncation)
this->client_info_.peername = this->helper_->getpeername(); 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_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; 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(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; 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() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); 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() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@@ -2084,8 +2083,13 @@ void APIConnection::process_state_subscriptions_() {
} }
#endif // USE_API_HOMEASSISTANT_STATES #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) { 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); LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
} }

View File

@@ -9,18 +9,13 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
#include <functional> #include <functional>
#include <vector> #include <vector>
namespace esphome::api { 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 // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending // 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 try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const std::string &get_name() const { return this->client_info_.name; } const char *get_name() const { return this->helper_->get_client_name(); }
const std::string &get_peername() const { return this->client_info_.peername; } /// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
@@ -526,10 +522,7 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<camera::CameraImageReader> image_reader_; std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif #endif
// Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) // Group 3: 4-byte types
ClientInfo client_info_;
// Group 4: 4-byte types
uint32_t last_traffic_; uint32_t last_traffic_;
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
int state_subs_at_ = -1; int state_subs_at_ = -1;
@@ -756,6 +749,8 @@ class APIConnection final : public APIServerConnection {
return this->schedule_batch_(); 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 // Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err); void log_warning_(const LogString *message, APIError err);
// Helper to handle fatal errors with logging // Helper to handle fatal errors with logging

View File

@@ -1,6 +1,5 @@
#include "api_frame_helper.h" #include "api_frame_helper.h"
#ifdef USE_API #ifdef USE_API
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.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) // 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; static constexpr size_t API_MAX_LOG_BYTES = 168;
#define HELPER_LOG(msg, ...) \ #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #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 #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \ #define LOG_PACKET_RECEIVED(buffer) \
@@ -243,6 +245,8 @@ APIError APIFrameHelper::init_common_() {
HELPER_LOG("Bad state for init %d", (int) state_); HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_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); int err = this->socket_->setblocking(false);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;

View File

@@ -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) // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
// Forward declaration
struct ClientInfo;
class ProtoWriteBuffer; 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 { struct ReadPacketBuffer {
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call)
uint16_t data_len; uint16_t data_len;
@@ -86,14 +86,23 @@ const LogString *api_error_to_logstr(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default; APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
: socket_(std::move(socket)), client_info_(client_info) {}
// 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 ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop(); virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 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); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() { APIError close() {
state_ = State::CLOSED; state_ = State::CLOSED;
@@ -186,9 +195,10 @@ class APIFrameHelper {
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
// Pointer to client info (4 bytes on 32-bit) // Client name buffer - stores name from Hello message or initial peername
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
const ClientInfo *client_info_{nullptr}; // Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together // Group smaller types together
uint16_t rx_buf_len_ = 0; uint16_t rx_buf_len_ = 0;

View File

@@ -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) // 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; static constexpr size_t API_MAX_LOG_BYTES = 168;
#define HELPER_LOG(msg, ...) \ #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #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 #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \ #define LOG_PACKET_RECEIVED(buffer) \

View File

@@ -9,8 +9,8 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper { class APINoiseFrameHelper final : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { : APIFrameHelper(std::move(socket)), ctx_(ctx) {
// Noise header structure: // Noise header structure:
// Pos 0: indicator (0x01) // Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian) // Pos 1-2: encrypted payload size (16-bit big-endian)

View File

@@ -1,7 +1,6 @@
#include "api_frame_helper_plaintext.h" #include "api_frame_helper_plaintext.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.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) // 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; static constexpr size_t API_MAX_LOG_BYTES = 168;
#define HELPER_LOG(msg, ...) \ #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #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 #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \ #define LOG_PACKET_RECEIVED(buffer) \

View File

@@ -7,8 +7,7 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper { class APIPlaintextFrameHelper final : public APIFrameHelper {
public: public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
: APIFrameHelper(std::move(socket), client_info) {
// Plaintext header structure (worst case): // Plaintext header structure (worst case):
// Pos 0: indicator (0x00) // Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes) // Pos 1-3: payload size varint (up to 3 bytes)

View File

@@ -125,15 +125,18 @@ void APIServer::loop() {
if (!sock) if (!sock)
break; break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit // Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) { 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 // Immediately close - socket destructor will handle cleanup
sock.reset(); sock.reset();
continue; continue;
} }
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn); this->clients_.emplace_back(conn);
@@ -166,8 +169,7 @@ void APIServer::loop() {
// Network is down - disconnect all clients // Network is down - disconnect all clients
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->on_fatal_error(); client->on_fatal_error();
ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
client->client_info_.peername.c_str());
} }
// Continue to process and clean up the clients below // Continue to process and clean up the clients below
} }
@@ -185,12 +187,12 @@ void APIServer::loop() {
// Rare case: handle disconnection // Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER #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 #endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get()); this->unregister_active_action_calls_for_connection(client.get());
#endif #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) // Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) { if (client_index < this->clients_.size() - 1) {

View File

@@ -4,6 +4,7 @@
#include "esphome/components/sha256/sha256.h" #include "esphome/components/sha256/sha256.h"
#endif #endif
#include "esphome/components/network/util.h" #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.h"
#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_libretiny.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_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); }
void ESPHomeOTAComponent::log_start_(const LogString *phase) { 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) { void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) {

View File

@@ -14,31 +14,7 @@
namespace esphome::socket { namespace esphome::socket {
std::string format_sockaddr(const struct sockaddr_storage &storage) { class BSDSocketImpl final : public Socket {
if (storage.ss_family == AF_INET) {
const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&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<const struct sockaddr_in6 *>(&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 {
public: public:
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT
@@ -93,23 +69,9 @@ class BSDSocketImpl : public Socket {
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getpeername(this->fd_, addr, addrlen); 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 { int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return ::getsockname(this->fd_, addr, addrlen); 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 { int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return ::getsockopt(this->fd_, level, optname, optval, optlen); return ::getsockopt(this->fd_, level, optname, optval, optlen);
} }

View File

@@ -71,7 +71,7 @@ class LWIPRawImpl : public Socket {
errno = EINVAL; errno = EINVAL;
return nullptr; 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) { if (pcb_ == nullptr) {
errno = EBADF; errno = EBADF;
return -1; return -1;
@@ -135,7 +135,7 @@ class LWIPRawImpl : public Socket {
} }
return 0; return 0;
} }
int close() override { int close() final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -152,7 +152,7 @@ class LWIPRawImpl : public Socket {
pcb_ = nullptr; pcb_ = nullptr;
return 0; return 0;
} }
int shutdown(int how) override { int shutdown(int how) final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -178,7 +178,7 @@ class LWIPRawImpl : public Socket {
return 0; return 0;
} }
int getpeername(struct sockaddr *name, socklen_t *addrlen) override { int getpeername(struct sockaddr *name, socklen_t *addrlen) final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -189,14 +189,7 @@ class LWIPRawImpl : public Socket {
} }
return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen);
} }
std::string getpeername() override { int getsockname(struct sockaddr *name, socklen_t *addrlen) final {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return "";
}
return this->format_ip_address_(pcb_->remote_ip);
}
int getsockname(struct sockaddr *name, socklen_t *addrlen) override {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -207,14 +200,7 @@ class LWIPRawImpl : public Socket {
} }
return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen);
} }
std::string getsockname() override { int getsockopt(int level, int optname, void *optval, socklen_t *optlen) final {
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 {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -248,7 +234,7 @@ class LWIPRawImpl : public Socket {
errno = EINVAL; errno = EINVAL;
return -1; 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) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -282,7 +268,7 @@ class LWIPRawImpl : public Socket {
errno = EOPNOTSUPP; errno = EOPNOTSUPP;
return -1; return -1;
} }
ssize_t read(void *buf, size_t len) override { ssize_t read(void *buf, size_t len) final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -340,7 +326,7 @@ class LWIPRawImpl : public Socket {
return read; 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; ssize_t ret = 0;
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
ssize_t err = read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); ssize_t err = read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
@@ -358,7 +344,7 @@ class LWIPRawImpl : public Socket {
return ret; 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; errno = ENOTSUP;
return -1; return -1;
} }
@@ -412,7 +398,7 @@ class LWIPRawImpl : public Socket {
} }
return 0; 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); ssize_t written = internal_write(buf, len);
if (written == -1) if (written == -1)
return -1; return -1;
@@ -427,7 +413,7 @@ class LWIPRawImpl : public Socket {
} }
return written; 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; ssize_t written = 0;
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
ssize_t err = internal_write(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); ssize_t err = internal_write(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
@@ -453,12 +439,12 @@ class LWIPRawImpl : public Socket {
} }
return written; 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); // return ::sendto(fd_, buf, len, flags, to, tolen);
errno = ENOSYS; errno = ENOSYS;
return -1; return -1;
} }
int setblocking(bool blocking) override { int setblocking(bool blocking) final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
return -1; return -1;
@@ -517,19 +503,6 @@ class LWIPRawImpl : public Socket {
} }
protected: 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) { int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) {
if (family_ == AF_INET) { if (family_ == AF_INET) {
if (*addrlen < sizeof(struct sockaddr_in)) { 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) // 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 // 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: public:
LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {}

View File

@@ -9,29 +9,7 @@
namespace esphome::socket { namespace esphome::socket {
std::string format_sockaddr(const struct sockaddr_storage &storage) { class LwIPSocketImpl final : public Socket {
if (storage.ss_family == AF_INET) {
const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&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<const struct sockaddr_in6 *>(&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 {
public: public:
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT
@@ -88,23 +66,9 @@ class LwIPSocketImpl : public Socket {
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getpeername(this->fd_, addr, addrlen); 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 { int getsockname(struct sockaddr *addr, socklen_t *addrlen) override {
return lwip_getsockname(this->fd_, addr, addrlen); 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 { int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return lwip_getsockopt(this->fd_, level, optname, optval, optlen); return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
} }

View File

@@ -10,6 +10,87 @@ namespace esphome::socket {
Socket::~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<const struct in_addr *>(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<const ip6_addr_t *>(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<char, SOCKADDR_STR_LEN> buf) {
if (storage.ss_family == AF_INET) {
const auto *addr = reinterpret_cast<const struct sockaddr_in *>(&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<const struct sockaddr_in6 *>(&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<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getpeername(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
}
size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
if (this->getsockname(reinterpret_cast<struct sockaddr *>(&storage), &len) != 0) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
}
std::unique_ptr<Socket> socket_ip(int type, int protocol) { std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#if USE_NETWORK_IPV6 #if USE_NETWORK_IPV6
return socket(AF_INET6, type, protocol); return socket(AF_INET6, type, protocol);

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <span>
#include <string> #include <string>
#include "esphome/core/optional.h" #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) #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
namespace esphome::socket { 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 { class Socket {
public: public:
Socket() = default; Socket() = default;
@@ -31,9 +41,15 @@ class Socket {
virtual int shutdown(int how) = 0; virtual int shutdown(int how) = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 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 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<char, SOCKADDR_STR_LEN> 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<char, SOCKADDR_STR_LEN> buf);
virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; 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 setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0;
virtual int listen(int backlog) = 0; virtual int listen(int backlog) = 0;

View File

@@ -3,6 +3,7 @@
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
#include "esphome/components/socket/socket.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes> #include <cinttypes>
@@ -433,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
"Multiple API Clients attempting to connect to Voice Assistant\n" "Multiple API Clients attempting to connect to Voice Assistant\n"
"Current client: %s (%s)\n" "Current client: %s (%s)\n"
"New client: %s (%s)", "New client: %s (%s)",
this->api_client_->get_name().c_str(), this->api_client_->get_peername().c_str(), this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(),
client->get_name().c_str(), client->get_peername().c_str()); client->get_peername());
return; return;
} }