mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 10:08:40 +00:00
The noise frame helper was calling state_action_() (447 bytes) on every read_packet() and write_protobuf_messages() call. In the DATA state (steady-state after handshake), this function falls through all handshake checks and returns OK — pure overhead on every encrypted packet. Add check_data_state_() inline helper that replaces the call with a single byte-load + branch instruction in the hot path. The handshake state machine continues to be driven by loop() as before. Also applies consistent state checking to the plaintext frame helper.
315 lines
11 KiB
C++
315 lines
11 KiB
C++
#include "api_frame_helper_plaintext.h"
|
|
#ifdef USE_API
|
|
#ifdef USE_API_PLAINTEXT
|
|
#include "esphome/core/application.h"
|
|
#include "esphome/core/hal.h"
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/core/log.h"
|
|
#include "proto.h"
|
|
#include <cstring>
|
|
#include <cinttypes>
|
|
|
|
#ifdef USE_ESP8266
|
|
#include <pgmspace.h>
|
|
#endif
|
|
|
|
namespace esphome::api {
|
|
|
|
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;
|
|
|
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
|
#define HELPER_LOG(msg, ...) \
|
|
do { \
|
|
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
|
this->get_peername_to(peername_buf); \
|
|
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
|
} while (0)
|
|
#else
|
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
|
#endif
|
|
|
|
#ifdef HELPER_LOG_PACKETS
|
|
#define LOG_PACKET_RECEIVED(buffer) \
|
|
do { \
|
|
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
|
ESP_LOGVV(TAG, "Received frame: %s", \
|
|
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
|
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
|
} while (0)
|
|
#define LOG_PACKET_SENDING(data, len) \
|
|
do { \
|
|
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
|
ESP_LOGVV(TAG, "Sending raw: %s", \
|
|
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
|
} while (0)
|
|
#else
|
|
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
|
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
|
#endif
|
|
|
|
/// Initialize the frame helper, returns OK if successful.
|
|
APIError APIPlaintextFrameHelper::init() {
|
|
APIError err = init_common_();
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
|
|
state_ = State::DATA;
|
|
return APIError::OK;
|
|
}
|
|
APIError APIPlaintextFrameHelper::loop() {
|
|
if (state_ != State::DATA) {
|
|
return APIError::BAD_STATE;
|
|
}
|
|
// Use base class implementation for buffer sending
|
|
return APIFrameHelper::loop();
|
|
}
|
|
|
|
/** Read a packet into the rx_buf_.
|
|
*
|
|
* @return See APIError
|
|
*
|
|
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
|
*/
|
|
APIError APIPlaintextFrameHelper::try_read_frame_() {
|
|
// read header
|
|
while (!rx_header_parsed_) {
|
|
// Now that we know when the socket is ready, we can read up to 3 bytes
|
|
// into the rx_header_buf_ before we have to switch back to reading
|
|
// one byte at a time to ensure we don't read past the message and
|
|
// into the next one.
|
|
|
|
// Read directly into rx_header_buf_ at the current position
|
|
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
|
|
ssize_t received =
|
|
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
|
|
APIError err = handle_socket_read_result_(received);
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
|
|
// If this was the first read, validate the indicator byte
|
|
if (rx_header_buf_pos_ == 0 && received > 0) {
|
|
if (rx_header_buf_[0] != 0x00) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
|
return APIError::BAD_INDICATOR;
|
|
}
|
|
}
|
|
|
|
rx_header_buf_pos_ += received;
|
|
|
|
// Check for buffer overflow
|
|
if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Header buffer overflow");
|
|
return APIError::BAD_DATA_PACKET;
|
|
}
|
|
|
|
// Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
|
|
if (rx_header_buf_pos_ < 3) {
|
|
continue;
|
|
}
|
|
|
|
// At this point, we have at least 3 bytes total:
|
|
// - Validated indicator byte (0x00) stored at position 0
|
|
// - At least 2 bytes in the buffer for the varints
|
|
// Buffer layout:
|
|
// [0]: indicator byte (0x00)
|
|
// [1-3]: Message size varint (variable length)
|
|
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
|
|
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
|
|
// [2-5]: Message type varint (variable length)
|
|
// We now attempt to parse both varints. If either is incomplete,
|
|
// we'll continue reading more bytes.
|
|
|
|
// Skip indicator byte at position 0
|
|
uint8_t varint_pos = 1;
|
|
uint32_t consumed = 0;
|
|
|
|
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
|
if (!msg_size_varint.has_value()) {
|
|
// not enough data there yet
|
|
continue;
|
|
}
|
|
|
|
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
|
|
MAX_MESSAGE_SIZE);
|
|
return APIError::BAD_DATA_PACKET;
|
|
}
|
|
rx_header_parsed_len_ = msg_size_varint->as_uint16();
|
|
|
|
// Move to next varint position
|
|
varint_pos += consumed;
|
|
|
|
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
|
if (!msg_type_varint.has_value()) {
|
|
// not enough data there yet
|
|
continue;
|
|
}
|
|
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
|
|
std::numeric_limits<uint16_t>::max());
|
|
return APIError::BAD_DATA_PACKET;
|
|
}
|
|
rx_header_parsed_type_ = msg_type_varint->as_uint16();
|
|
rx_header_parsed_ = true;
|
|
}
|
|
// header reading done
|
|
|
|
// Reserve space for body (+ null terminator so protobuf StringRef fields
|
|
// can be safely null-terminated in-place after decode)
|
|
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) {
|
|
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
|
|
}
|
|
|
|
if (rx_buf_len_ < rx_header_parsed_len_) {
|
|
// more data to read
|
|
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
|
|
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
|
|
APIError err = handle_socket_read_result_(received);
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
rx_buf_len_ += static_cast<uint16_t>(received);
|
|
if (static_cast<uint16_t>(received) != to_read) {
|
|
// not all read
|
|
return APIError::WOULD_BLOCK;
|
|
}
|
|
}
|
|
|
|
LOG_PACKET_RECEIVED(this->rx_buf_);
|
|
|
|
// Clear state for next frame (rx_buf_ still contains data for caller)
|
|
this->rx_buf_len_ = 0;
|
|
this->rx_header_buf_pos_ = 0;
|
|
this->rx_header_parsed_ = false;
|
|
|
|
return APIError::OK;
|
|
}
|
|
|
|
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|
APIError aerr = this->check_data_state_();
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
aerr = this->try_read_frame_();
|
|
if (aerr != APIError::OK) {
|
|
if (aerr == APIError::BAD_INDICATOR) {
|
|
// Make sure to tell the remote that we don't
|
|
// understand the indicator byte so it knows
|
|
// we do not support it.
|
|
struct iovec iov[1];
|
|
// The \x00 first byte is the marker for plaintext.
|
|
//
|
|
// The remote will know how to handle the indicator byte,
|
|
// but it likely won't understand the rest of the message.
|
|
//
|
|
// We must send at least 3 bytes to be read, so we add
|
|
// a message after the indicator byte to ensures its long
|
|
// enough and can aid in debugging.
|
|
static constexpr uint8_t INDICATOR_MSG_SIZE = 19;
|
|
#ifdef USE_ESP8266
|
|
static const char MSG_PROGMEM[] PROGMEM = "\x00"
|
|
"Bad indicator byte";
|
|
char msg[INDICATOR_MSG_SIZE];
|
|
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
|
|
iov[0].iov_base = (void *) msg;
|
|
#else
|
|
static const char MSG[] = "\x00"
|
|
"Bad indicator byte";
|
|
iov[0].iov_base = (void *) MSG;
|
|
#endif
|
|
iov[0].iov_len = INDICATOR_MSG_SIZE;
|
|
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
|
|
}
|
|
return aerr;
|
|
}
|
|
|
|
buffer->data = this->rx_buf_.data();
|
|
buffer->data_len = this->rx_header_parsed_len_;
|
|
buffer->type = this->rx_header_parsed_type_;
|
|
return APIError::OK;
|
|
}
|
|
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
|
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
|
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
|
}
|
|
|
|
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
|
std::span<const MessageInfo> messages) {
|
|
APIError aerr = this->check_data_state_();
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
if (messages.empty()) {
|
|
return APIError::OK;
|
|
}
|
|
|
|
uint8_t *buffer_data = buffer.get_buffer()->data();
|
|
|
|
// Stack-allocated iovec array - no heap allocation
|
|
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
|
uint16_t total_write_len = 0;
|
|
|
|
for (const auto &msg : messages) {
|
|
// Calculate varint sizes for header layout
|
|
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
|
|
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
|
|
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
|
|
|
// Calculate where to start writing the header
|
|
// The header starts at the latest possible position to minimize unused padding
|
|
//
|
|
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
|
|
// [0-2] - Unused padding
|
|
// [3] - 0x00 indicator byte
|
|
// [4] - Payload size varint (1 byte, for sizes 0-127)
|
|
// [5] - Message type varint (1 byte, for types 0-127)
|
|
// [6...] - Actual payload data
|
|
//
|
|
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
|
|
// [0-1] - Unused padding
|
|
// [2] - 0x00 indicator byte
|
|
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
|
|
// [5] - Message type varint (1 byte, for types 0-127)
|
|
// [6...] - Actual payload data
|
|
//
|
|
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
|
// [0] - 0x00 indicator byte
|
|
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
|
|
// [4-5] - Message type varint (2 bytes, for types 128-32767)
|
|
// [6...] - Actual payload data
|
|
//
|
|
// The message starts at offset + frame_header_padding_
|
|
// So we write the header starting at offset + frame_header_padding_ - total_header_len
|
|
uint8_t *buf_start = buffer_data + msg.offset;
|
|
uint32_t header_offset = frame_header_padding_ - total_header_len;
|
|
|
|
// Write the plaintext header
|
|
buf_start[header_offset] = 0x00; // indicator
|
|
|
|
// Encode varints directly into buffer
|
|
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
|
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
|
|
|
// Add iovec for this message (header + payload)
|
|
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
|
iovs.push_back({buf_start + header_offset, msg_len});
|
|
total_write_len += msg_len;
|
|
}
|
|
|
|
// Send all messages in one writev call
|
|
return write_raw_(iovs.data(), iovs.size(), total_write_len);
|
|
}
|
|
|
|
} // namespace esphome::api
|
|
#endif // USE_API_PLAINTEXT
|
|
#endif // USE_API
|