ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
api_frame_helper_plaintext.cpp
Go to the documentation of this file.
2#ifdef USE_API
3#ifdef USE_API_PLAINTEXT
5#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
8#include "api_pb2.h"
9#include "proto.h"
10#include <cstring>
11#include <cinttypes>
12
13#ifdef USE_ESP8266
14#include <pgmspace.h>
15#endif
16
17namespace esphome::api {
18
19static const char *const TAG = "api.plaintext";
20
21// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
22static constexpr size_t API_MAX_LOG_BYTES = 168;
23
24#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
25#define HELPER_LOG(msg, ...) \
26 do { \
27 char peername_buf[socket::SOCKADDR_STR_LEN]; \
28 this->get_peername_to(peername_buf); \
29 ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
30 } while (0)
31#else
32#define HELPER_LOG(msg, ...) ((void) 0)
33#endif
34
35#ifdef HELPER_LOG_PACKETS
36#define LOG_PACKET_RECEIVED(buffer) \
37 do { \
38 char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
39 ESP_LOGVV(TAG, "Received frame: %s", \
40 format_hex_pretty_to(hex_buf_, (buffer).data(), \
41 (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
42 } while (0)
43#else
44#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
45#endif
46
49 APIError err = init_common_();
50 if (err != APIError::OK) {
51 return err;
52 }
53
55 return APIError::OK;
56}
58 if (state_ != State::DATA) {
60 }
61 if (!this->overflow_buf_.empty()) [[unlikely]] {
63 }
64 return APIError::OK;
65}
66
74 // read header
75 while (!rx_header_parsed_) {
76 // Now that we know when the socket is ready, we can read up to 3 bytes
77 // into the rx_header_buf_ before we have to switch back to reading
78 // one byte at a time to ensure we don't read past the message and
79 // into the next one.
80
81 // Read directly into rx_header_buf_ at the current position
82 // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
83 ssize_t received =
84 this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
86 if (err != APIError::OK) {
87 return err;
88 }
89
90 // If this was the first read, validate the indicator byte
91 if (rx_header_buf_pos_ == 0 && received > 0) {
92 if (rx_header_buf_[0] != 0x00) {
93#ifdef USE_API_NOISE
94 // Dual build (encryption supported but no key set): a 0x01 first byte
95 // is a Noise client hello. Hand the connection off to a Noise helper
96 // running the all-zeros provisioning PSK so the encryption key can be
97 // set without crossing the wire in plaintext. Preserve the bytes we
98 // already consumed; they are the start of the Noise 3-byte header.
99 if (rx_header_buf_[0] == 0x01) {
100 rx_header_buf_pos_ = static_cast<uint8_t>(received);
102 }
103#endif
105 HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
107 }
108 }
109
110 rx_header_buf_pos_ += received;
111
112 // Check for buffer overflow
113 if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
115 HELPER_LOG("Header buffer overflow");
117 }
118
119 // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
120 if (rx_header_buf_pos_ < 3) {
121 continue;
122 }
123
124 // At this point, we have at least 3 bytes total:
125 // - Validated indicator byte (0x00) stored at position 0
126 // - At least 2 bytes in the buffer for the varints
127 // Buffer layout:
128 // [0]: indicator byte (0x00)
129 // [1-3]: Message size varint (variable length)
130 // - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
131 // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
132 // [2-5]: Message type varint (variable length)
133 // We now attempt to parse both varints. If either is incomplete,
134 // we'll continue reading more bytes.
135
136 // Skip indicator byte at position 0
137 uint8_t varint_pos = 1;
138
139 // rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2
140 auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
141 if (!msg_size_varint.has_value()) {
142 // not enough data there yet
143 continue;
144 }
145
146 if (msg_size_varint.value > MAX_MESSAGE_SIZE) {
148 HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u",
149 static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE);
151 }
152 rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value);
153
154 // Move to next varint position
155 varint_pos += msg_size_varint.consumed;
156
157 auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
158 if (!msg_type_varint.has_value()) {
159 // not enough data there yet
160 continue;
161 }
162 if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) {
164 HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u",
165 static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max());
167 }
168 rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value);
169 rx_header_parsed_ = true;
170 }
171 // header reading done
172
173 // Reserve space for body (+ null terminator so protobuf StringRef fields
174 // can be safely null-terminated in-place after decode)
175 if (!this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR)) [[unlikely]] {
178 }
179
181 // more data to read
182 uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
183 ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
184 APIError err = handle_socket_read_result_(received);
185 if (err != APIError::OK) {
186 return err;
187 }
188 rx_buf_len_ += static_cast<uint16_t>(received);
189 if (static_cast<uint16_t>(received) != to_read) {
190 // not all read
192 }
193 }
194
195 LOG_PACKET_RECEIVED(this->rx_buf_);
196
197 // Clear state for next frame (rx_buf_ still contains data for caller)
198 this->rx_buf_len_ = 0;
199 this->rx_header_buf_pos_ = 0;
200 this->rx_header_parsed_ = false;
201
202 return APIError::OK;
203}
204
206 APIError aerr = this->check_data_state_();
207 if (aerr != APIError::OK)
208 return aerr;
209
210 aerr = this->try_read_frame_();
211 if (aerr != APIError::OK) {
212 if (aerr == APIError::BAD_INDICATOR) {
213 // Make sure to tell the remote that we don't
214 // understand the indicator byte so it knows
215 // we do not support it.
216 // The \x00 first byte is the marker for plaintext.
217 //
218 // The remote will know how to handle the indicator byte,
219 // but it likely won't understand the rest of the message.
220 //
221 // We must send at least 3 bytes to be read, so we add
222 // a message after the indicator byte to ensures its long
223 // enough and can aid in debugging.
224 static constexpr uint8_t INDICATOR_MSG_SIZE = 19;
225#ifdef USE_ESP8266
226 static const char MSG_PROGMEM[] PROGMEM = "\x00"
227 "Bad indicator byte";
228 char msg[INDICATOR_MSG_SIZE];
229 memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
230 this->write_raw_buf_(msg, INDICATOR_MSG_SIZE);
231#else
232 static const char MSG[] = "\x00"
233 "Bad indicator byte";
234 this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE);
235#endif
236 }
237 return aerr;
238 }
239
240 buffer->data = this->rx_buf_.data();
241 buffer->data_len = this->rx_header_parsed_len_;
242 buffer->type = this->rx_header_parsed_type_;
243 return APIError::OK;
244}
245
246// Encode a 16-bit varint (1-3 bytes) using pre-computed length.
247ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) {
248 if (varint_len >= 2) {
249 *p++ = static_cast<uint8_t>(value | 0x80);
250 value >>= 7;
251 if (varint_len == 3) {
252 *p++ = static_cast<uint8_t>(value | 0x80);
253 value >>= 7;
254 }
255 }
256 *p = static_cast<uint8_t>(value);
257}
258
259// The generator rejects message IDs above MAX_MESSAGE_TYPE, so the type varint
260// can never outgrow the 2 bytes HEADER_PADDING budgets for it. Without this
261// bound, write_plaintext_header's header_offset would underflow for the first
262// message in a batch and the header write would land outside the buffer.
263static_assert(1 + 3 + ProtoSize::varint16(MAX_MESSAGE_TYPE) <= APIPlaintextFrameHelper::HEADER_PADDING,
264 "HEADER_PADDING cannot fit the type varint of the largest message ID");
265
266// Write plaintext header into pre-allocated padding before payload.
267// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
268// actual header size for contiguous batch messages).
269// Returns the total header length (indicator + varints).
270ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
271 uint16_t message_type, uint8_t padding_size) {
272 uint8_t size_varint_len = ProtoSize::varint16(payload_size);
273 uint8_t type_varint_len = ProtoSize::varint16(message_type);
274 uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
275
276 // The header is right-justified within the padding so it sits immediately before payload.
277 //
278 // Single/first message (padding_size = HEADER_PADDING = 6):
279 // Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload
280 // Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload
281 // Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload
282 //
283 // Batch messages 2+ (padding_size = actual header size, no unused bytes):
284 // Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload
285 // Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload
286#ifdef ESPHOME_DEBUG_API
287 assert(padding_size >= total_header_len);
288#endif
289 uint32_t header_offset = padding_size - total_header_len;
290
291 // Write the plaintext header
292 buf_start[header_offset] = 0x00; // indicator
293
294 // Encode varints directly into buffer using pre-computed lengths
295 encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
296 encode_varint_16(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
297
298 return total_header_len;
299}
300
302#ifdef ESPHOME_DEBUG_API
303 assert(this->state_ == State::DATA);
304#endif
305
306 uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING);
307 uint8_t *buffer_data = buffer.get_buffer()->data();
308 uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING);
309 return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len,
310 static_cast<uint16_t>(header_len + payload_size));
311}
312
314 std::span<const MessageInfo> messages) {
315#ifdef ESPHOME_DEBUG_API
316 assert(this->state_ == State::DATA);
317 assert(!messages.empty());
318#endif
319 uint8_t *buffer_data = buffer.get_buffer()->data();
320
321 // First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes.
322 // Subsequent messages were encoded with exact header sizes (header_size = actual header len).
323 // write_plaintext_header right-justifies the header within header_size bytes of padding.
324 const auto &first = messages[0];
325 uint8_t *first_start = buffer_data + first.offset;
326 uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING);
327 uint8_t *write_start = first_start + HEADER_PADDING - header_len;
328 uint16_t total_len = header_len + first.payload_size;
329
330 for (size_t i = 1; i < messages.size(); i++) {
331 const auto &msg = messages[i];
332 header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size);
333 total_len += header_len + msg.payload_size;
334 }
335
336 return this->write_raw_fast_buf_(write_start, total_len);
337}
338
339} // namespace esphome::api
340#endif // USE_API_PLAINTEXT
341#endif // USE_API
bool resize(size_t n) ESPHOME_ALWAYS_INLINE
Returns false if allocation fails; the buffer is left unchanged. No zero-fill.
Definition api_buffer.h:32
APIError handle_socket_read_result_(ssize_t received)
APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len)
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent=WRITE_NOT_ATTEMPTED)
std::unique_ptr< socket::Socket > socket_
APIError ESPHOME_ALWAYS_INLINE check_data_state_() const
bool empty() const
True when no backlogged data is waiting.
APIError try_read_frame_()
Read a packet into the rx_buf_.
APIError init() override
Initialize the frame helper, returns OK if successful.
APIError read_packet(ReadPacketBuffer *buffer) override
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span< const MessageInfo > messages) override
static constexpr uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value)
Definition proto.h:683
static ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len)
Parse a varint from buffer.
Definition proto.h:140
static ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len)
Parse a varint from buffer (safe for empty buffers).
Definition proto.h:153
APIBuffer * get_buffer() const
Definition proto.h:271
uint16_t type
__int64 ssize_t
Definition httplib.h:178
uint16_t size
Definition helpers.cpp:25
static void uint32_t
uint32_t payload_size()