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