ESPHome 2026.3.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 "proto.h"
9#include <cstring>
10#include <cinttypes>
11
12#ifdef USE_ESP8266
13#include <pgmspace.h>
14#endif
15
16namespace esphome::api {
17
18static const char *const TAG = "api.plaintext";
19
20// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
21static constexpr size_t API_MAX_LOG_BYTES = 168;
22
23#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
24#define HELPER_LOG(msg, ...) \
25 do { \
26 char peername_buf[socket::SOCKADDR_STR_LEN]; \
27 this->get_peername_to(peername_buf); \
28 ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
29 } while (0)
30#else
31#define HELPER_LOG(msg, ...) ((void) 0)
32#endif
33
34#ifdef HELPER_LOG_PACKETS
35#define LOG_PACKET_RECEIVED(buffer) \
36 do { \
37 char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
38 ESP_LOGVV(TAG, "Received frame: %s", \
39 format_hex_pretty_to(hex_buf_, (buffer).data(), \
40 (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
41 } while (0)
42#define LOG_PACKET_SENDING(data, len) \
43 do { \
44 char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
45 ESP_LOGVV(TAG, "Sending raw: %s", \
46 format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
47 } while (0)
48#else
49#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
50#define LOG_PACKET_SENDING(data, len) ((void) 0)
51#endif
52
55 APIError err = init_common_();
56 if (err != APIError::OK) {
57 return err;
58 }
59
61 return APIError::OK;
62}
64 if (state_ != State::DATA) {
66 }
67 // Use base class implementation for buffer sending
68 return APIFrameHelper::loop();
69}
70
78 // read header
79 while (!rx_header_parsed_) {
80 // Now that we know when the socket is ready, we can read up to 3 bytes
81 // into the rx_header_buf_ before we have to switch back to reading
82 // one byte at a time to ensure we don't read past the message and
83 // into the next one.
84
85 // Read directly into rx_header_buf_ at the current position
86 // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
87 ssize_t received =
88 this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
90 if (err != APIError::OK) {
91 return err;
92 }
93
94 // If this was the first read, validate the indicator byte
95 if (rx_header_buf_pos_ == 0 && received > 0) {
96 if (rx_header_buf_[0] != 0x00) {
98 HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
100 }
101 }
102
103 rx_header_buf_pos_ += received;
104
105 // Check for buffer overflow
106 if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
108 HELPER_LOG("Header buffer overflow");
110 }
111
112 // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
113 if (rx_header_buf_pos_ < 3) {
114 continue;
115 }
116
117 // At this point, we have at least 3 bytes total:
118 // - Validated indicator byte (0x00) stored at position 0
119 // - At least 2 bytes in the buffer for the varints
120 // Buffer layout:
121 // [0]: indicator byte (0x00)
122 // [1-3]: Message size varint (variable length)
123 // - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
124 // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
125 // [2-5]: Message type varint (variable length)
126 // We now attempt to parse both varints. If either is incomplete,
127 // we'll continue reading more bytes.
128
129 // Skip indicator byte at position 0
130 uint8_t varint_pos = 1;
131 uint32_t consumed = 0;
132
133 auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
134 if (!msg_size_varint.has_value()) {
135 // not enough data there yet
136 continue;
137 }
138
139 if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
141 HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
142 MAX_MESSAGE_SIZE);
144 }
145 rx_header_parsed_len_ = msg_size_varint->as_uint16();
146
147 // Move to next varint position
148 varint_pos += consumed;
149
150 auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
151 if (!msg_type_varint.has_value()) {
152 // not enough data there yet
153 continue;
154 }
155 if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
157 HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
158 std::numeric_limits<uint16_t>::max());
160 }
161 rx_header_parsed_type_ = msg_type_varint->as_uint16();
162 rx_header_parsed_ = true;
163 }
164 // header reading done
165
166 // Reserve space for body (+ null terminator so protobuf StringRef fields
167 // can be safely null-terminated in-place after decode)
168 if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) {
169 this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
170 }
171
173 // more data to read
174 uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
175 ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
176 APIError err = handle_socket_read_result_(received);
177 if (err != APIError::OK) {
178 return err;
179 }
180 rx_buf_len_ += static_cast<uint16_t>(received);
181 if (static_cast<uint16_t>(received) != to_read) {
182 // not all read
184 }
185 }
186
187 LOG_PACKET_RECEIVED(this->rx_buf_);
188
189 // Clear state for next frame (rx_buf_ still contains data for caller)
190 this->rx_buf_len_ = 0;
191 this->rx_header_buf_pos_ = 0;
192 this->rx_header_parsed_ = false;
193
194 return APIError::OK;
195}
196
198 if (this->state_ != State::DATA) {
200 }
201
202 APIError aerr = this->try_read_frame_();
203 if (aerr != APIError::OK) {
204 if (aerr == APIError::BAD_INDICATOR) {
205 // Make sure to tell the remote that we don't
206 // understand the indicator byte so it knows
207 // we do not support it.
208 struct iovec iov[1];
209 // The \x00 first byte is the marker for plaintext.
210 //
211 // The remote will know how to handle the indicator byte,
212 // but it likely won't understand the rest of the message.
213 //
214 // We must send at least 3 bytes to be read, so we add
215 // a message after the indicator byte to ensures its long
216 // enough and can aid in debugging.
217 static constexpr uint8_t INDICATOR_MSG_SIZE = 19;
218#ifdef USE_ESP8266
219 static const char MSG_PROGMEM[] PROGMEM = "\x00"
220 "Bad indicator byte";
221 char msg[INDICATOR_MSG_SIZE];
222 memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
223 iov[0].iov_base = (void *) msg;
224#else
225 static const char MSG[] = "\x00"
226 "Bad indicator byte";
227 iov[0].iov_base = (void *) MSG;
228#endif
229 iov[0].iov_len = INDICATOR_MSG_SIZE;
230 this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
231 }
232 return aerr;
233 }
234
235 buffer->data = this->rx_buf_.data();
236 buffer->data_len = this->rx_header_parsed_len_;
237 buffer->type = this->rx_header_parsed_type_;
238 return APIError::OK;
239}
241 MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
242 return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
243}
244
246 std::span<const MessageInfo> messages) {
247 if (state_ != State::DATA) {
248 return APIError::BAD_STATE;
249 }
250
251 if (messages.empty()) {
252 return APIError::OK;
253 }
254
255 uint8_t *buffer_data = buffer.get_buffer()->data();
256
257 // Stack-allocated iovec array - no heap allocation
259 uint16_t total_write_len = 0;
260
261 for (const auto &msg : messages) {
262 // Calculate varint sizes for header layout
263 uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
264 uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
265 uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
266
267 // Calculate where to start writing the header
268 // The header starts at the latest possible position to minimize unused padding
269 //
270 // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
271 // [0-2] - Unused padding
272 // [3] - 0x00 indicator byte
273 // [4] - Payload size varint (1 byte, for sizes 0-127)
274 // [5] - Message type varint (1 byte, for types 0-127)
275 // [6...] - Actual payload data
276 //
277 // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
278 // [0-1] - Unused padding
279 // [2] - 0x00 indicator byte
280 // [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
281 // [5] - Message type varint (1 byte, for types 0-127)
282 // [6...] - Actual payload data
283 //
284 // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
285 // [0] - 0x00 indicator byte
286 // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
287 // [4-5] - Message type varint (2 bytes, for types 128-32767)
288 // [6...] - Actual payload data
289 //
290 // The message starts at offset + frame_header_padding_
291 // So we write the header starting at offset + frame_header_padding_ - total_header_len
292 uint8_t *buf_start = buffer_data + msg.offset;
293 uint32_t header_offset = frame_header_padding_ - total_header_len;
294
295 // Write the plaintext header
296 buf_start[header_offset] = 0x00; // indicator
297
298 // Encode varints directly into buffer
299 encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
300 encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
301
302 // Add iovec for this message (header + payload)
303 size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
304 iovs.push_back({buf_start + header_offset, msg_len});
305 total_write_len += msg_len;
306 }
307
308 // Send all messages in one writev call
309 return write_raw_(iovs.data(), iovs.size(), total_write_len);
310}
311
312} // namespace esphome::api
313#endif // USE_API_PLAINTEXT
314#endif // USE_API
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:209
size_t size() const
Definition helpers.h:269
void push_back(const T &value)
Definition helpers.h:242
APIError handle_socket_read_result_(ssize_t received)
std::vector< uint8_t > rx_buf_
std::unique_ptr< socket::Socket > socket_
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len)
APIError try_read_frame_()
Read a packet into the rx_buf_.
APIError init() override
Initialize the frame helper, returns OK if successful.
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override
APIError read_packet(ReadPacketBuffer *buffer) override
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span< const MessageInfo > messages) override
static constexpr uint32_t varint(uint32_t value)
Calculates the size in bytes needed to encode a uint32_t value as a varint.
Definition proto.h:529
static optional< ProtoVarInt > parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed)
Parse a varint from buffer. consumed must be a valid pointer (not null).
Definition proto.h:108
std::vector< uint8_t > * get_buffer() const
Definition proto.h:368
uint16_t type
__int64 ssize_t
Definition httplib.h:178
void encode_varint_to_buffer(uint32_t val, uint8_t *buffer)
Encode a varint directly into a pre-allocated buffer.
Definition proto.h:63
size_t size
Definition helpers.h:854
void * iov_base
Definition headers.h:101
size_t iov_len
Definition headers.h:102
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM
Definition web_server.h:28