ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
ota_esphome.cpp
Go to the documentation of this file.
1#include "ota_esphome.h"
2#ifdef USE_OTA_ENCRYPTION_FROM_API
4#endif
5#ifdef USE_OTA
6#ifdef USE_OTA_PASSWORD
8#endif
17#include "esphome/core/hal.h"
19#include "esphome/core/log.h"
20#include "esphome/core/util.h"
21#ifdef USE_LWIP_FAST_SELECT
23#endif
24
25#include <cerrno>
26#include <cstdio>
27#include <sys/time.h>
28
29namespace esphome {
30
31static const char *const TAG = "esphome.ota";
32
33#ifdef USE_OTA_ENCRYPTION
35#ifdef USE_OTA_ENCRYPTION_FROM_API
37#else
38 return this->noise_ctx_;
39#endif
40}
41#endif
42static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
43static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
44// Milliseconds for data transfer. Covers the lwIP retransmit run seen in
45// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
46// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
47static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
48
49// Single-instance pointer — multi-port configs are rejected in final_validate.
50// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
51static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
52
53// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ).
55 if (global_esphome_ota_component != nullptr) {
56 global_esphome_ota_component->enable_loop_soon_any_context();
57 }
58}
59
61 this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
62 if (this->server_ == nullptr) {
63 this->server_failed_(LOG_STR("creation"));
64 return;
65 }
66 int enable = 1;
67 int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
68 if (err != 0) {
69 this->log_socket_error_(LOG_STR("reuseaddr"));
70 // we can still continue
71 }
72 err = this->server_->setblocking(false);
73 if (err != 0) {
74 this->server_failed_(LOG_STR("nonblocking"));
75 return;
76 }
77
78 struct sockaddr_storage server;
79
80 socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
81 if (sl == 0) {
82 this->server_failed_(LOG_STR("set sockaddr"));
83 return;
84 }
85
86 err = this->server_->bind((struct sockaddr *) &server, sl);
87 if (err != 0) {
88 this->server_failed_(LOG_STR("bind"));
89 return;
90 }
91
92 err = this->server_->listen(1); // Only one client at a time
93 if (err != 0) {
94 this->server_failed_(LOG_STR("listen"));
95 return;
96 }
97
98 // loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
99 global_esphome_ota_component = this;
100#ifdef USE_LWIP_FAST_SELECT
101 // Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
102 // no wakes fire and loop() falls back to the self-disable safety net.
104#endif
105
106#ifdef USE_OTA_PARTITIONS
108#endif
109}
110
112 char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
113 ESP_LOGCONFIG(TAG,
114 "Over-The-Air updates:\n"
115 " Address: %s:%u\n"
116 " Version: %d"
117#ifdef USE_OTA_ENCRYPTION
118 "\n Encryption: %s"
119#endif
120 ,
121 network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION
122#ifdef USE_OTA_ENCRYPTION_REQUIRED
123 ,
124 LOG_STR_LITERAL("required")
125#elif defined(USE_OTA_ENCRYPTION_PROVISIONED)
126 // A runtime provisioned key may not exist yet
127 ,
128 this->noise_context_().has_psk() ? LOG_STR_LITERAL("offered, plaintext accepted")
129 : LOG_STR_LITERAL("offered once the api key is provisioned")
130#elif defined(USE_OTA_ENCRYPTION)
131 ,
132 LOG_STR_LITERAL("offered, plaintext accepted")
133#endif
134 );
135#ifdef USE_OTA_PASSWORD
136 if (!this->password_.empty()) {
137 ESP_LOGCONFIG(TAG, " Password configured");
138 }
139#endif
140#ifdef USE_OTA_PARTITIONS
141 ESP_LOGCONFIG(TAG,
142 " Partition access allowed\n"
143 " Running app:\n"
144 " Partition address: 0x%" PRIX32 "\n"
145 " Used size: %zu bytes (0x%zX)",
147
148#ifdef USE_ESP32
149 ESP_LOGCONFIG(TAG,
150 " Partition table:\n"
151 " %-12s %-4s %-8s %-10s %-10s",
152 "Name", "Type", "Subtype", "Address", "Size");
153 esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, nullptr);
154 while (it != nullptr) {
155 const esp_partition_t *partition = esp_partition_get(it);
156 ESP_LOGCONFIG(TAG, " %-12s 0x%-2X 0x%-6X 0x%-8" PRIX32 " 0x%-8" PRIX32, partition->label, partition->type,
157 partition->subtype, partition->address, partition->size);
158 it = esp_partition_next(it);
159 }
160 esp_partition_iterator_release(it);
161 esp_bootloader_desc_t bootloader_desc;
162 esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
163 ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
164 (err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
165#endif // USE_ESP32
166#endif // USE_OTA_PARTITIONS
167}
168
170 // Self-disable idle loop where a wake path re-enables on listener readiness
171 // (fast-select, raw-TCP accept_fn_). Host BSD select doesn't, so stay enabled.
172 if (this->client_ == nullptr && !this->server_->ready()) {
173#ifndef USE_HOST
174 this->disable_loop();
175#endif
176 return;
177 }
178 this->handle_handshake_();
179}
180
181static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
182static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
183static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
184static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
185// Noise needs the extended protocol: the prologue binds the 2-byte feature ack
186static constexpr uint8_t CLIENT_NOISE_FEATURES =
187 CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
188static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
189static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
190static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
191
193#ifdef USE_OTA_ENCRYPTION_REQUIRED
194 // FEATURE_READ already refused every client without the extended protocol
195 return true;
196#else
197 return (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0;
198#endif
199}
200
207
208 if (this->client_ == nullptr) {
209 // We already checked server_->ready() in loop(), so we can accept directly
210 struct sockaddr_storage source_addr;
211 socklen_t addr_len = sizeof(source_addr);
212 int enable = 1;
213
214 this->client_ = this->server_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
215 if (this->client_ == nullptr)
216 return;
217 int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
218 if (err != 0) {
219 this->log_socket_error_(LOG_STR("nodelay"));
220 this->cleanup_connection_();
221 return;
222 }
223 err = this->client_->setblocking(false);
224 if (err != 0) {
225 this->log_socket_error_(LOG_STR("non-blocking"));
226 this->cleanup_connection_();
227 return;
228 }
229 this->log_start_(LOG_STR("handshake"));
231 this->handshake_buf_pos_ = 0; // Reset handshake buffer position
233 }
234
235 // Check for handshake timeout
237 if (now - this->client_connect_time_ > OTA_SOCKET_TIMEOUT_HANDSHAKE) {
238 ESP_LOGW(TAG, "Handshake timeout");
239 this->cleanup_connection_();
240 return;
241 }
242
243 switch (this->ota_state_) {
245 // Try to read remaining magic bytes (5 total)
246 if (!this->try_read_(5, LOG_STR("read magic"))) {
247 return;
248 }
249
250 // Validate magic bytes
251 if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) {
252 ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
253 this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
255 return;
256 }
257
258 // Magic bytes valid, move to next state
261 this->handshake_buf_[1] = USE_OTA_VERSION;
262 [[fallthrough]];
263 }
264
265 case OTAState::MAGIC_ACK: {
266 // Send OK and version - 2 bytes
267 if (!this->try_write_(2, LOG_STR("ack magic"))) {
268 return;
269 }
270 // All bytes sent, create backend and move to next state
273 [[fallthrough]];
274 }
275
277 // Read features - 1 byte
278 if (!this->try_read_(1, LOG_STR("read feature"))) {
279 return;
280 }
281 this->ota_features_ = this->handshake_buf_[0];
282 ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
283
284#ifdef USE_OTA_ENCRYPTION_REQUIRED
285 // `ota: encryption:` requires the client to negotiate encryption
286 if ((this->ota_features_ & CLIENT_NOISE_FEATURES) != CLIENT_NOISE_FEATURES) {
287 ESP_LOGW(TAG, "Client does not support encryption");
289 return;
290 }
291#endif
292
294
295 const bool supports_compression =
296 (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression();
297
298 // Compose the feature-ack response. When the client negotiates the extended protocol we emit
299 // a 2-byte response (marker + server feature flags); otherwise we emit the single-byte
300 // legacy response.
301 if (this->extended_proto_()) {
302 static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
304 this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
305#ifdef USE_OTA_PARTITIONS
306 this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
307#endif
308#ifdef USE_OTA_ENCRYPTION_PROVISIONED
309 // A runtime provisioned key may not exist yet
310 if (this->noise_context_().has_psk()) {
311 this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
312 }
313#elif defined(USE_OTA_ENCRYPTION)
314 // A yaml key always exists: validation rejects the all-zeros key
315 this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
316#endif
317 } else {
318 this->handshake_buf_[0] =
320 }
321 [[fallthrough]];
322 }
323
325 static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1;
326 static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2;
327 const size_t ack_size = this->extended_proto_() ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE;
328 if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
329 return;
330 }
331#ifdef USE_OTA_ENCRYPTION
332 // Latch the offer actually sent: a key activating between the two
333 // states must not start a session the client never expects
334 if ((this->handshake_buf_[1] & SERVER_FEATURE_SUPPORTS_NOISE) != 0 &&
335 (this->ota_features_ & CLIENT_NOISE_FEATURES) == CLIENT_NOISE_FEATURES) {
336 // handshake_buf_ still holds the feature ack composed above; a
337 // would-block re-entry lands here without rebuilding it
338 if (!this->noise_start_session_(this->handshake_buf_[1])) {
339 return;
340 }
342 return;
343 }
344#endif
345#ifdef USE_OTA_PASSWORD
346 // If password is set, move to auth phase
347 if (!this->password_.empty()) {
349 } else
350#endif
351 {
352 // No password, move directly to data phase
354 }
355 [[fallthrough]];
356 }
357
358#ifdef USE_OTA_PASSWORD
359 case OTAState::AUTH_SEND: {
360 // Non-blocking authentication send
361 if (!this->handle_auth_send_()) {
362 return;
363 }
365 [[fallthrough]];
366 }
367
368 case OTAState::AUTH_READ: {
369 // Non-blocking authentication read & verify
370 if (!this->handle_auth_read_()) {
371 return;
372 }
374 [[fallthrough]];
375 }
376#endif
377
378 case OTAState::DATA:
379 this->handle_data_();
380 return;
381
382#ifdef USE_OTA_ENCRYPTION
384 if (!this->handle_noise_handshake_()) {
385 return;
386 }
388 this->handle_data_();
389 return;
390#endif
391
392 default:
393 break;
394 }
395}
396
430 // Backend calls overwrite this with OK; reset to UNKNOWN before any
431 // goto error that follows a successful begin()/write()
433 size_t total = 0;
434 uint32_t last_progress = 0;
435 uint32_t last_data_ms = 0;
436 uint8_t buf[OTA_BUFFER_SIZE];
437 char *sbuf = reinterpret_cast<char *>(buf);
438 size_t ota_size;
440#if USE_OTA_VERSION == 2
441 size_t size_acknowledged = 0;
442#endif
443
444 // Set socket timeouts and blocking mode (see strategy table above)
445 struct timeval tv;
446 tv.tv_sec = 2;
447 tv.tv_usec = 0;
448 this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
449 this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
450 this->client_->setblocking(true);
451
452 // Acknowledge auth OK - 1 byte
454
455 if (this->extended_proto_()) {
456 // Read ota type, 1 byte
457 if (!this->data_readall_(buf, 1)) {
458 this->log_read_error_(LOG_STR("OTA type"));
459 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
460 }
461 ota_type = static_cast<ota::OTAType>(buf[0]);
462 }
463 ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
464
465 // Read size, 4 bytes MSB first
466 if (!this->data_readall_(buf, 4)) {
467 this->log_read_error_(LOG_STR("size"));
468 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
469 }
470 ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
471 (static_cast<size_t>(buf[2]) << 8) | buf[3];
472 ESP_LOGV(TAG, "Size is %zu bytes", ota_size);
473
474#ifndef USE_OTA_PARTITIONS
475 if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
477 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
478 }
479#endif
480
481 // Now that we've passed authentication and are actually
482 // starting the update, set the warning status and notify
483 // listeners. This ensures that port scanners do not
484 // accidentally trigger the update process.
485 this->log_start_(LOG_STR("update"));
486 this->status_set_warning();
487#ifdef USE_OTA_STATE_LISTENER
488 this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
489#endif
490
491 // begin() returns quickly; flash sectors are erased incrementally during write().
492 error_code = this->backend_->begin(ota_size, ota_type);
493 if (error_code != ota::OTA_RESPONSE_OK)
494 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
495
496 // Acknowledge prepare OK - 1 byte
498
499 // Read binary MD5, 32 bytes
500 if (!this->data_readall_(buf, 32)) {
501 this->log_read_error_(LOG_STR("MD5 checksum"));
503 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
504 }
505 sbuf[32] = '\0';
506 ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf);
507 this->backend_->set_update_md5(sbuf);
508
509 // Acknowledge MD5 OK - 1 byte
511
512 // Track when we last received data so a silently-vanished peer (no FIN/RST
513 // delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
514 // can't wedge the device indefinitely. Without this, the loop only exits
515 // on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
516 // TCP keepalive isn't enabled here.
517 last_data_ms = millis();
518 while (total < ota_size) {
519 if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
520 ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
522 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
523 }
524 size_t remaining = ota_size - total;
525 size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
526 ssize_t read;
527#ifdef USE_OTA_ENCRYPTION
528 if (this->noise_ != nullptr) {
529 // One frame per call; noise_read_data_ waits internally (readall_), so
530 // there is no would-block retry here and failures are already logged.
531 read = this->noise_read_data_(buf, requested);
532 if (read <= 0) {
534 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
535 }
536 } else
537#endif
538 {
539 read = this->client_->read(buf, requested);
540 if (read == -1) {
541 const int err = errno;
542 if (this->would_block_(err)) {
543 // read() already waited up to SO_RCVTIMEO for data, just feed WDT
544 App.feed_wdt();
545 continue;
546 }
547 ESP_LOGW(TAG, "Read err %d", err);
549 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
550 } else if (read == 0) {
551 ESP_LOGW(TAG, "Remote closed");
553 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
554 }
555 }
556
557 last_data_ms = millis();
558 error_code = this->backend_->write(buf, read);
559 if (error_code != ota::OTA_RESPONSE_OK) {
560 ESP_LOGW(TAG, "Flash write err %d", error_code);
561 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
562 }
563 total += read;
564#if USE_OTA_VERSION == 2
565 while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
567 size_acknowledged += OTA_BLOCK_SIZE;
568 }
569#endif
570
571 uint32_t now = millis();
572 if (now - last_progress > 1000) {
573 last_progress = now;
574 float percentage = (total * 100.0f) / ota_size;
575 ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
576#ifdef USE_OTA_STATE_LISTENER
577 this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
578#endif
579 // feed watchdog and give other tasks a chance to run
581 }
582 }
583
584 // Acknowledge receive OK - 1 byte
586
587 error_code = this->backend_->end();
588 if (error_code != ota::OTA_RESPONSE_OK) {
589 ESP_LOGW(TAG, "End update err %d", error_code);
590 goto error; // NOLINT(cppcoreguidelines-avoid-goto)
591 }
592
593 // Acknowledge Update end OK - 1 byte
595
596 // Read ACK
597 if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
598 this->log_read_error_(LOG_STR("ack"));
599 // do not go to error, this is not fatal
600 }
601
602 this->cleanup_connection_();
603 delay(10);
604 ESP_LOGI(TAG, "Update complete");
605 this->status_clear_warning();
606#ifdef USE_OTA_STATE_LISTENER
607 this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0);
608#endif
609 delay(100); // NOLINT
610#ifdef USE_OTA_PARTITIONS
611 if (ota_type == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
612 // Skip on_safe_shutdown: nvs_flash_deinit() has already invalidated open NVS handles, so
613 // preferences flush would emit ESP_ERR_NVS_INVALID_HANDLE for every entry. Reboot directly.
614 App.reboot();
615 }
616#endif
618
619error:
620 this->data_write_byte_(static_cast<uint8_t>(error_code));
621
622 // Abort backend before cleanup - cleanup_connection_() destroys the backend.
623 // Always call abort() unconditionally: backends register external partitions before
624 // esp_ota_begin (partition table / bootloader paths), and abort() is responsible for
625 // releasing those even if begin() failed before an OTA handle was opened. The IDF
626 // backend's esp_ota_abort(0) is documented as harmless.
627 if (this->backend_ != nullptr) {
628 this->backend_->abort();
629 }
630
631 this->cleanup_connection_();
632
633 this->status_momentary_error("err", 5000);
634#ifdef USE_OTA_STATE_LISTENER
635 this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
636#endif
637}
638
639bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
640 uint32_t start = millis();
641 uint32_t at = 0;
642 while (len - at > 0) {
643 uint32_t now = millis();
644 if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
645 ESP_LOGW(TAG, "Timeout reading %zu bytes", len);
646 return false;
647 }
648
649 ssize_t read = this->client_->read(buf + at, len - at);
650 if (read == -1) {
651 const int err = errno;
652 if (!this->would_block_(err)) {
653 ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err);
654 return false;
655 }
656 } else if (read == 0) {
657 ESP_LOGW(TAG, "Remote closed");
658 return false;
659 } else {
660 at += read;
661 }
662 // read() already waited via SO_RCVTIMEO, just yield without 1ms stall
663 App.feed_wdt();
664 delay(0);
665 }
666
667 return true;
668}
669bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
670 uint32_t start = millis();
671 uint32_t at = 0;
672 while (len - at > 0) {
673 uint32_t now = millis();
674 if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
675 ESP_LOGW(TAG, "Timeout writing %zu bytes", len);
676 return false;
677 }
678
679 ssize_t written = this->client_->write(buf + at, len - at);
680 if (written == -1) {
681 const int err = errno;
682 if (!this->would_block_(err)) {
683 ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err);
684 return false;
685 }
686 // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning
688 } else {
689 at += written;
690 // write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT
691 App.feed_wdt();
692 }
693 }
694 return true;
695}
696
698
699void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
700 ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
701}
702
703void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); }
704
705void ESPHomeOTAComponent::log_start_(const LogString *phase) {
706 char peername[socket::SOCKADDR_STR_LEN];
707 this->client_->getpeername_to(peername);
708 ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername);
709}
710
711void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) {
712 ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during));
713}
714
715void ESPHomeOTAComponent::server_failed_(const LogString *msg) {
716 this->log_socket_error_(msg);
717 // No explicit close() needed — listen sockets have no active connections on
718 // failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
719 delete this->server_;
720 this->server_ = nullptr;
721 this->mark_failed();
722}
723
724bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) {
725 if (read == -1 && this->would_block_(errno)) {
726 return false; // No data yet, try again next loop
727 }
728
729 if (read <= 0) {
730 read == 0 ? this->log_remote_closed_(desc) : this->log_socket_error_(desc);
731 this->cleanup_connection_();
732 return false;
733 }
734 return true;
735}
736
738 if (written == -1) {
739 if (this->would_block_(errno)) {
740 return false; // Try again next loop
741 }
742 this->log_socket_error_(desc);
743 this->cleanup_connection_();
744 return false;
745 }
746 return true;
747}
748
749bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *desc) {
750 // Read bytes into handshake buffer, starting at handshake_buf_pos_
751 size_t bytes_to_read = to_read - this->handshake_buf_pos_;
752 ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read);
753
754 if (!this->handle_read_error_(read, desc)) {
755 return false;
756 }
757
758 this->handshake_buf_pos_ += read;
759 // Return true only if we have all the requested bytes
760 return this->handshake_buf_pos_ >= to_read;
761}
762
763bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) {
764 // Write bytes from handshake buffer, starting at handshake_buf_pos_
765 size_t bytes_to_write = to_write - this->handshake_buf_pos_;
766 ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write);
767
768 if (!this->handle_write_error_(written, desc)) {
769 return false;
770 }
771
772 this->handshake_buf_pos_ += written;
773 // Return true only if we have written all the requested bytes
774 return this->handshake_buf_pos_ >= to_write;
775}
776
778 this->client_->close();
779 this->client_ = nullptr;
780 this->client_connect_time_ = 0;
781 this->handshake_buf_pos_ = 0;
783 this->ota_features_ = 0;
784 this->backend_ = nullptr;
785#ifdef USE_OTA_PASSWORD
786 this->cleanup_auth_();
787#endif
788#ifdef USE_OTA_ENCRYPTION
789 this->noise_ = nullptr;
790#endif
791 // Intentionally no disable_loop() — letting loop() run one more iteration catches
792 // any connection that queued on the listener mid-session (otherwise the wake flag,
793 // set while we were in LOOP state, would be lost to enable_pending_loops_()).
794}
795
800
801#ifdef USE_OTA_PASSWORD
802void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
803
805 bool client_supports_sha256 = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_SHA256_AUTH) != 0;
806
807 // Require SHA256
808 if (!client_supports_sha256) {
809 this->log_auth_warning_(LOG_STR("SHA256 required"));
811 return false;
812 }
814 return true;
815}
816
818 // Initialize auth buffer if not already done
819 if (!this->auth_buf_) {
820 // Select auth type based on client capabilities and configuration
821 if (!this->select_auth_type_()) {
822 return false;
823 }
824
825 // Generate nonce - hasher must be created and used in same stack frame
826 // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
827 // 1. Hash objects must NEVER be passed to another function (different stack frame)
828 // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
829 // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
830 // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
831 //
832 // Buffer layout after AUTH_READ completes:
833 // [0]: auth_type (1 byte)
834 // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
835 // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
836 // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
837
838 // CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
839 // (no passing to other functions). All hash operations must happen in this function.
840 sha256::SHA256 hasher;
841
842 const size_t hex_size = hasher.get_size() * 2;
843 const size_t nonce_len = hasher.get_size() / 4;
844 const size_t auth_buf_size = 1 + 3 * hex_size;
845 this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
846 this->auth_buf_pos_ = 0;
847
848 char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
849 if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
850 this->log_auth_warning_(LOG_STR("Random failed"));
852 return false;
853 }
854
855 hasher.init();
856 hasher.add(buf, nonce_len);
857 hasher.calculate();
858 this->auth_buf_[0] = this->auth_type_;
859 hasher.get_hex(buf);
860
861 ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf);
862 }
863
864 // Try to write auth_type + nonce
865 constexpr size_t hex_size = SHA256_HEX_SIZE;
866 const size_t to_write = 1 + hex_size;
867 size_t remaining = to_write - this->auth_buf_pos_;
868
869 ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
870 if (!this->handle_write_error_(written, LOG_STR("ack auth"))) {
871 return false;
872 }
873
874 this->auth_buf_pos_ += written;
875
876 // Check if we still have more to write
877 if (this->auth_buf_pos_ < to_write) {
878 return false; // More to write, try again next loop
879 }
880
881 // All written, prepare for reading phase
882 this->auth_buf_pos_ = 0;
883 return true;
884}
885
887 constexpr size_t hex_size = SHA256_HEX_SIZE;
888 const size_t to_read = hex_size * 2; // CNonce + Response
889
890 // Try to read remaining bytes (CNonce + Response)
891 // We read cnonce+response starting at offset 1+hex_size (after auth_type and our nonce)
892 size_t cnonce_offset = 1 + hex_size; // Offset where cnonce should be stored in buffer
893 size_t remaining = to_read - this->auth_buf_pos_;
894 ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining);
895
896 if (!this->handle_read_error_(read, LOG_STR("read auth"))) {
897 return false;
898 }
899
900 this->auth_buf_pos_ += read;
901
902 // Check if we still need more data
903 if (this->auth_buf_pos_ < to_read) {
904 return false; // More to read, try again next loop
905 }
906
907 // We have all the data, verify it
908 const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
909 const char *cnonce = nonce + hex_size;
910 const char *response = cnonce + hex_size;
911
912 // CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
913 // (no passing to other functions). All hash operations must happen in this function.
914 sha256::SHA256 hasher;
915
916 hasher.init();
917 hasher.add(this->password_.c_str(), this->password_.length());
918 hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
919 hasher.calculate();
920
921 ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce);
922#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
923 char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator)
924 hasher.get_hex(computed_hash);
925 ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash);
926#endif
927 ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response);
928
929 // Compare response
930 bool matches = hasher.equals_hex(response);
931
932 if (!matches) {
933 this->log_auth_warning_(LOG_STR("Password mismatch"));
935 return false;
936 }
937
938 // Authentication successful - clean up auth state
939 this->cleanup_auth_();
940
941 return true;
942}
943
945 this->auth_buf_ = nullptr;
946 this->auth_buf_pos_ = 0;
947 this->auth_type_ = 0;
948}
949#endif // USE_OTA_PASSWORD
950
951} // namespace esphome
952#endif
void feed_wdt()
Feed the task watchdog.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:289
ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using Ardui...
Definition ota_esphome.h:18
static constexpr size_t OTA_BUFFER_SIZE
bool handle_noise_handshake_()
Drive the non-blocking handshake from loop(); returns true once the transport ciphers are ready.
bool would_block_(int error_code) const
static constexpr size_t SHA256_HEX_SIZE
Definition ota_esphome.h:66
static constexpr uint8_t MAGIC_BYTES[5]
bool writeall_(const uint8_t *buf, size_t len)
bool try_read_(size_t to_read, const LogString *desc)
bool data_readall_(uint8_t *buf, size_t len)
noise::NoiseContext noise_ctx_
bool noise_start_session_(uint8_t server_feature_flags)
Allocate the session and start the responder handshake.
ota::OTABackendPtr backend_
bool try_write_(size_t to_write, const LogString *desc)
std::unique_ptr< uint8_t[]> auth_buf_
bool handle_write_error_(ssize_t written, const LogString *desc)
bool data_write_byte_(uint8_t byte)
void log_auth_warning_(const LogString *msg)
float get_setup_priority() const override
void send_error_and_cleanup_(ota::OTAResponseTypes error)
bool handle_read_error_(ssize_t read, const LogString *desc)
ssize_t noise_read_data_(uint8_t *buf, size_t capacity)
Blocking read of one data-phase frame, decrypted in place; returns the plaintext size,...
void log_read_error_(const LogString *what)
bool readall_(uint8_t *buf, size_t len)
std::unique_ptr< NoiseSession > noise_
uint8_t handshake_buf_[HANDSHAKE_BUF_SIZE]
static constexpr size_t HANDSHAKE_BUF_SIZE
const noise::NoiseContext & noise_context_() const
void server_failed_(const LogString *msg)
void transition_ota_state_(OTAState next_state)
socket::ListenSocket * server_
void log_remote_closed_(const LogString *during)
std::unique_ptr< socket::Socket > client_
void log_start_(const LogString *phase)
void log_socket_error_(const LogString *msg)
void get_hex(char *output)
Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes.
Definition hash_base.h:29
bool equals_hex(const char *expected)
Compare the hash against a provided hex-encoded hash.
Definition hash_base.h:35
noise::NoiseContext & get_noise_ctx()
Definition api_server.h:87
void notify_state_(OTAState state, float progress, uint8_t error)
SHA256 hash implementation.
Definition sha256.h:51
void calculate() override
Definition sha256.cpp:27
size_t get_size() const override
Get the size of the hash in bytes (32 for SHA256)
Definition sha256.h:64
void add(const uint8_t *data, size_t len) override
Definition sha256.cpp:25
void init() override
Definition sha256.cpp:19
bool ready() const
Check if the socket has buffered data ready to read.
Definition socket.h:85
int bind(const struct sockaddr *addr, socklen_t addrlen)
int setsockopt(int level, int optname, const void *optval, socklen_t optlen)
std::unique_ptr< BSDSocketImpl > accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen)
uint16_t addr_len
uint32_t socklen_t
Definition headers.h:99
__int64 ssize_t
Definition httplib.h:178
struct lwip_sock * esphome_lwip_get_sock(int fd)
Look up a LwIP socket struct from a file descriptor.
void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock)
Set the listener netconn that the fast-select callback filters OTA wakes against.
APIServer * global_api_server
const char * get_use_address_to(std::span< char, USE_ADDRESS_BUFFER_SIZE > buf)
Get the active network address for logging.
Definition util.cpp:42
@ OTA_TYPE_UPDATE_PARTITION_TABLE
Definition ota_backend.h:93
void get_running_app_position(uint32_t &offset, size_t &size)
@ OTA_RESPONSE_UPDATE_PREPARE_OK
Definition ota_backend.h:24
@ OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED
Definition ota_backend.h:52
@ OTA_RESPONSE_SUPPORTS_COMPRESSION
Definition ota_backend.h:28
@ OTA_RESPONSE_BIN_MD5_OK
Definition ota_backend.h:25
@ OTA_RESPONSE_UPDATE_END_OK
Definition ota_backend.h:27
@ OTA_RESPONSE_RECEIVE_OK
Definition ota_backend.h:26
@ OTA_RESPONSE_CHUNK_OK
Definition ota_backend.h:29
@ OTA_RESPONSE_FEATURE_FLAGS
Definition ota_backend.h:30
@ OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE
Definition ota_backend.h:46
@ OTA_RESPONSE_ERROR_AUTH_INVALID
Definition ota_backend.h:34
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:53
@ OTA_RESPONSE_REQUEST_SHA256_AUTH
Definition ota_backend.h:20
@ OTA_RESPONSE_ERROR_MAGIC
Definition ota_backend.h:32
@ OTA_RESPONSE_HEADER_OK
Definition ota_backend.h:22
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port)
Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
Definition socket.cpp:194
std::unique_ptr< ListenSocket > socket_ip_loop_monitored(int type, int protocol)
Definition socket.cpp:130
bool random_bytes(uint8_t *data, size_t len)
Generate len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG).
Definition helpers.cpp:20
void esphome_wake_ota_component_any_context()
const void size_t len
Definition hal.h:64
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
int written
Definition helpers.h:1099
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t