ESPHome 2026.10.2
Loading...
Searching...
No Matches
web_server_idf.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include <algorithm>
4#include <cstdarg>
5#include <memory>
6#include <cstdio>
7#include <cstring>
8#include <cctype>
9#include <cinttypes>
10
13#include "esphome/core/log.h"
14
15#include <mbedtls/base64.h>
16#include <freertos/FreeRTOS.h>
17#include <freertos/task.h>
18
19#include "utils.h"
20#include "sse_chunk.h"
21#include "web_server_idf.h"
22
23#ifdef USE_WEBSERVER_AUTH_DIGEST
24#include <esp_random.h>
25#include <esp_rom_md5.h>
26#endif
27
28#ifdef USE_WEBSERVER_OTA
29#include <multipart_parser.h>
30#include "multipart.h" // For parse_multipart_boundary and other utils
31#endif
32
33#ifdef USE_WEBSERVER
36#endif // USE_WEBSERVER
37
38// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
39#include <cerrno>
40#include <sys/socket.h>
41
43
44// Status strings not provided by esp_http_server.h
45#ifndef HTTPD_401
46#define HTTPD_401 "401 Unauthorized"
47#endif
48#ifndef HTTPD_409
49#define HTTPD_409 "409 Conflict"
50#endif
51#ifndef HTTPD_422
52#define HTTPD_422 "422 Unprocessable Entity"
53#endif
54
55#define CRLF_STR "\r\n"
56#define CRLF_LEN (sizeof(CRLF_STR) - 1)
57
58static const char *const TAG = "web_server_idf";
59
60// Only prepare_json_() may hold the JSON arena: every other frame in this file is capped below one
61// arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
62#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
63#pragma GCC diagnostic error "-Wstack-usage=2048"
64#endif
65
66// Chunk size for streaming request bodies; matches the Arduino AsyncWebServer buffer size.
67// Buffers of this size must live on the heap - the httpd task stack is too small.
68static constexpr size_t RECV_CHUNK_SIZE = 1460;
69static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog
70
71// Global instance to avoid guard variable (saves 8 bytes)
72// This is initialized at program startup before any threads
73namespace {
74// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
75DefaultHeaders default_headers_instance;
76} // namespace
77
78DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
79
80namespace {
81// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
96[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
97 if (buf == nullptr) {
98 return HTTPD_SOCK_ERR_INVALID;
99 }
100
101 // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
102 int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
103 if (ret < 0) {
104 const int err = errno;
105 if (err == EAGAIN || err == EWOULDBLOCK) {
106 // Buffer full - retry later
107 return HTTPD_SOCK_ERR_TIMEOUT;
108 }
109 // Real error
110 ESP_LOGD(TAG, "send error: errno %d", err);
111 return HTTPD_SOCK_ERR_FAIL;
112 }
113 return ret;
114}
115} // namespace
116
117void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
118 // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions
119 //
120 // The race condition occurs because close() initiates lwIP teardown while
121 // the TCP/IP thread can still receive packets, causing assertions when
122 // recv_tcp() sees partially-torn-down state.
123 //
124 // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE
125 // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR)
126 // to allow the FIN packet to be sent cleanly during close().
127 //
128 // Note: This function may be called with an already-closed socket if the network
129 // stack closed it. In that case, shutdown() will fail but close() is safe to call.
130 //
131 // See: https://github.com/esphome/esphome-webserver/issues/163
132
133 // Attempt shutdown - ignore errors as socket may already be closed
134 shutdown(sockfd, SHUT_RD);
135
136 // Always close - safe even if socket is already closed by network stack
137 close(sockfd);
138}
139
141 if (this->server_) {
142 httpd_stop(this->server_);
143 this->server_ = nullptr;
144 }
145}
146
148 if (this->server_) {
149 this->end();
150 }
151 // Allow room for SerializationBuffer's 640-byte stack buffer and HTTP request handling.
152 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
153 config.stack_size = 4512;
154 config.server_port = this->port_;
155 config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
156 // Always enable LRU purging to handle socket exhaustion gracefully.
157 // When max sockets is reached, the oldest connection is closed to make room for new ones.
158 // This prevents "httpd_accept_conn: error in accept (23)" errors.
159 // See: https://github.com/esphome/esphome/issues/12464
160 config.lru_purge_enable = true;
161 // Use custom close function that shuts down before closing to prevent lwIP race conditions
163 if (httpd_start(&this->server_, &config) == ESP_OK) {
164 const httpd_uri_t handler_get = {
165 .uri = "",
166 .method = HTTP_GET,
168 .user_ctx = this,
169 };
170 httpd_register_uri_handler(this->server_, &handler_get);
171
172 const httpd_uri_t handler_post = {
173 .uri = "",
174 .method = HTTP_POST,
176 .user_ctx = this,
177 };
178 httpd_register_uri_handler(this->server_, &handler_post);
179
180 const httpd_uri_t handler_options = {
181 .uri = "",
182 .method = HTTP_OPTIONS,
184 .user_ctx = this,
185 };
186 httpd_register_uri_handler(this->server_, &handler_options);
187 }
188}
189
190esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
191 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
192 auto content_type = request_get_header(r, "Content-Type");
193
194 if (!request_has_header(r, "Content-Length")) {
195 ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
196 httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
197 return ESP_OK;
198 }
199
200 if (content_type.has_value()) {
201 const char *content_type_char = content_type.value().c_str();
202
203 // Check most common case first
204 size_t content_type_len = strlen(content_type_char);
205 if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
206 // Normal form data - proceed with regular handling
207#ifdef USE_WEBSERVER_OTA
208 } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
209 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
210 return server->handle_multipart_upload_(r, content_type_char);
211#endif
212 } else {
213 // Other content types (e.g. application/json) are delivered raw to a matching
214 // custom handler via handleBody(), like the Arduino AsyncWebServer does
215 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
216 return server->handle_raw_body_(r, content_type_char);
217 }
218 }
219
220 // Handle regular form data
221 if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
222 ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
223 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
224 return ESP_FAIL;
225 }
226
227 std::string post_query;
228 if (r->content_len > 0) {
229 post_query.resize(r->content_len);
230 const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1);
231 if (ret <= 0) { // 0 return value indicates connection closed
232 if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
233 httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr);
234 return ESP_ERR_TIMEOUT;
235 }
236 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
237 return ESP_FAIL;
238 }
239 }
240
241 AsyncWebServerRequest req(r, std::move(post_query));
242 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
243}
244
245esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) {
246 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri);
248 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
249}
250
252 for (auto *handler : this->handlers_) {
253 if (handler->canHandle(request)) {
254 // At now process only basic requests.
255 // OTA requires multipart request support and handleUpload for it
256 handler->handleRequest(request);
257 return ESP_OK;
258 }
259 }
260 if (this->on_not_found_) {
261 this->on_not_found_(request);
262 return ESP_OK;
263 }
264 return ESP_ERR_NOT_FOUND;
265}
266
267esp_err_t AsyncWebServer::handle_raw_body_(httpd_req_t *r, const char *content_type) {
269 AsyncWebHandler *handler = nullptr;
270 for (auto *h : this->handlers_) {
271 if (h->canHandle(&req)) {
272 handler = h;
273 break;
274 }
275 }
276
277 if (handler == nullptr) {
278 ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type);
279 // fallback to get handler to support backward compatibility
280 return this->request_handler_(&req);
281 }
282
283 const size_t total = r->content_len;
284 if (total > 0) {
285 auto buffer = std::make_unique_for_overwrite<char[]>(RECV_CHUNK_SIZE);
286 size_t bytes_since_yield = 0;
287
288 for (size_t index = 0; index < total;) {
289 int recv_len = httpd_req_recv(r, buffer.get(), std::min(total - index, RECV_CHUNK_SIZE));
290
291 if (recv_len <= 0) {
292 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
293 nullptr);
294 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
295 }
296
297 handler->handleBody(&req, reinterpret_cast<uint8_t *>(buffer.get()), recv_len, index, total);
298 index += recv_len;
299 bytes_since_yield += recv_len;
300
301 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
302 vTaskDelay(1);
303 bytes_since_yield = 0;
304 }
305 }
306 }
307
308 handler->handleRequest(&req);
309 return ESP_OK;
310}
311
313 delete this->rsp_;
314 for (auto *param : this->params_) {
315 delete param; // NOLINT(cppcoreguidelines-owning-memory)
316 }
317}
318
319bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); }
320
321optional<std::string> AsyncWebServerRequest::get_header(const char *name) const {
322 return request_get_header(*this, name);
323}
324
325StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) const {
326 const char *uri = this->req_->uri;
327 const char *query_start = strchr(uri, '?');
328 size_t uri_len = query_start ? static_cast<size_t>(query_start - uri) : strlen(uri);
329 size_t copy_len = std::min(uri_len, URL_BUF_SIZE - 1);
330 memcpy(buffer.data(), uri, copy_len);
331 buffer[copy_len] = '\0';
332 // Decode URL-encoded characters in-place (e.g., %20 -> space)
333 size_t decoded_len = url_decode(buffer.data());
334 return StringRef(buffer.data(), decoded_len);
335}
336
337void AsyncWebServerRequest::redirect(const std::string &url) {
338 httpd_resp_set_status(*this, "302 Found");
339 httpd_resp_set_hdr(*this, "Location", url.c_str());
340 httpd_resp_set_hdr(*this, "Connection", "close");
341 httpd_resp_send(*this, nullptr, 0);
342}
343
344void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
345 // Set status code - use constants for common codes, default to 500 for unknown codes
346 const char *status;
347 switch (code) {
348 case 200:
349 status = HTTPD_200;
350 break;
351 case 204:
352 status = HTTPD_204;
353 break;
354 case 400:
355 status = HTTPD_400;
356 break;
357 case 401:
358 status = HTTPD_401;
359 break;
360 case 404:
361 status = HTTPD_404;
362 break;
363 case 409:
364 status = HTTPD_409;
365 break;
366 case 422:
367 status = HTTPD_422;
368 break;
369 default:
370 status = HTTPD_500;
371 break;
372 }
373 httpd_resp_set_status(*this, status);
374
375 if (content_type && *content_type) {
376 httpd_resp_set_type(*this, content_type);
377 }
378 httpd_resp_set_hdr(*this, "Accept-Ranges", "none");
379
380 for (const auto &header : DefaultHeaders::Instance().headers_) {
381 httpd_resp_set_hdr(*this, header.name, header.value);
382 }
383
384 delete this->rsp_;
385 this->rsp_ = rsp;
386}
387
388#ifdef USE_WEBSERVER_AUTH
389
390#ifdef USE_WEBSERVER_AUTH_DIGEST
391namespace {
392
393// Extract the value of a Digest auth parameter (e.g. "nonce") from the comma-separated
394// parameter list. Values may be quoted or bare. Returns an empty ref when the key is absent.
395// Only whole parameter names match, so "nc" does not match inside "cnonce".
396StringRef digest_param(StringRef params, const char *key) {
397 size_t key_len = strlen(key);
398 const char *base = params.c_str();
399 size_t n = params.size();
400 size_t i = 0;
401 while (i < n) {
402 while (i < n && (base[i] == ' ' || base[i] == ','))
403 i++;
404 size_t name_start = i;
405 while (i < n && base[i] != '=' && base[i] != ',')
406 i++;
407 if (i >= n || base[i] == ',')
408 continue; // token without a '=', skip it
409 size_t name_len = i - name_start;
410 while (name_len > 0 && base[name_start + name_len - 1] == ' ')
411 name_len--;
412 i++; // consume '='
413 const char *val_start;
414 size_t val_len;
415 if (i < n && base[i] == '"') {
416 i++;
417 val_start = base + i;
418 while (i < n && base[i] != '"')
419 i++;
420 val_len = (base + i) - val_start;
421 if (i < n)
422 i++; // consume closing quote
423 } else {
424 val_start = base + i;
425 while (i < n && base[i] != ',')
426 i++;
427 val_len = (base + i) - val_start;
428 }
429 if (name_len == key_len && memcmp(base + name_start, key, key_len) == 0)
430 return StringRef(val_start, val_len);
431 while (i < n && base[i] != ',')
432 i++;
433 }
434 return StringRef();
435}
436
437// Verify an RFC 2617 Digest response. Stateless (the nonce we issued is not tracked), which
438// matches the ESPAsyncWebServer backend used on the Arduino platforms.
439bool check_digest_auth(const char *username, const char *password, const std::string &header, const char *method) {
440 const size_t prefix_len = sizeof("Digest ") - 1;
441 StringRef params(header.c_str() + prefix_len, header.size() - prefix_len);
442
443 if (digest_param(params, "username") != username)
444 return false;
445
446 StringRef realm = digest_param(params, "realm");
447 StringRef nonce = digest_param(params, "nonce");
448 StringRef uri = digest_param(params, "uri");
449 StringRef qop = digest_param(params, "qop");
450 StringRef nc = digest_param(params, "nc");
451 StringRef cnonce = digest_param(params, "cnonce");
452 StringRef response = digest_param(params, "response");
453 if (response.size() != 32)
454 return false;
455
456 // Compute the three MD5 hashes by streaming the pieces straight into the ROM MD5 engine, so
457 // nothing is concatenated on the heap. Each hash is emitted as 32 lowercase hex characters.
458 md5_context_t ctx;
459 uint8_t digest[16];
460
461 // HA1 = MD5(username:realm:password) -- uses the realm the client echoed back.
462 char ha1[33];
463 esp_rom_md5_init(&ctx);
464 esp_rom_md5_update(&ctx, username, strlen(username));
465 esp_rom_md5_update(&ctx, ":", 1);
466 esp_rom_md5_update(&ctx, realm.c_str(), realm.size());
467 esp_rom_md5_update(&ctx, ":", 1);
468 esp_rom_md5_update(&ctx, password, strlen(password));
469 esp_rom_md5_final(digest, &ctx);
470 format_hex_to(ha1, digest, sizeof(digest));
471
472 // HA2 = MD5(method:uri) -- uses the uri the client echoed back.
473 char ha2[33];
474 esp_rom_md5_init(&ctx);
475 esp_rom_md5_update(&ctx, method, strlen(method));
476 esp_rom_md5_update(&ctx, ":", 1);
477 esp_rom_md5_update(&ctx, uri.c_str(), uri.size());
478 esp_rom_md5_final(digest, &ctx);
479 format_hex_to(ha2, digest, sizeof(digest));
480
481 // expected = MD5(HA1:nonce:nc:cnonce:qop:HA2)
482 char expected[33];
483 esp_rom_md5_init(&ctx);
484 esp_rom_md5_update(&ctx, ha1, 32);
485 esp_rom_md5_update(&ctx, ":", 1);
486 esp_rom_md5_update(&ctx, nonce.c_str(), nonce.size());
487 esp_rom_md5_update(&ctx, ":", 1);
488 esp_rom_md5_update(&ctx, nc.c_str(), nc.size());
489 esp_rom_md5_update(&ctx, ":", 1);
490 esp_rom_md5_update(&ctx, cnonce.c_str(), cnonce.size());
491 esp_rom_md5_update(&ctx, ":", 1);
492 esp_rom_md5_update(&ctx, qop.c_str(), qop.size());
493 esp_rom_md5_update(&ctx, ":", 1);
494 esp_rom_md5_update(&ctx, ha2, 32);
495 esp_rom_md5_final(digest, &ctx);
496 format_hex_to(expected, digest, sizeof(digest));
497
498 // Constant-time comparison of the two 32-char hex digests.
499 uint8_t result = 0;
500 for (size_t i = 0; i < 32; i++)
501 result |= static_cast<uint8_t>(expected[i] ^ response[i]);
502 return result == 0;
503}
504
505} // namespace
506#endif // USE_WEBSERVER_AUTH_DIGEST
507
508bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
509 if (username == nullptr || password == nullptr || *username == 0) {
510 return true;
511 }
512 auto auth = this->get_header("Authorization");
513 if (!auth.has_value()) {
514 return false;
515 }
516
517 auto *auth_str = auth.value().c_str();
518
519#ifdef USE_WEBSERVER_AUTH_DIGEST
520 // The build fixed the scheme to Digest, so the Basic path is compiled out entirely.
521 const auto auth_prefix_len = sizeof("Digest ") - 1;
522 if (strncmp("Digest ", auth_str, auth_prefix_len) != 0) {
523 ESP_LOGW(TAG, "Only Digest authorization supported");
524 return false;
525 }
526 return check_digest_auth(username, password, auth.value(), http_method_str(this->method()));
527#else
528 const auto auth_prefix_len = sizeof("Basic ") - 1;
529 if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) {
530 ESP_LOGW(TAG, "Only Basic authorization supported");
531 return false;
532 }
533
534 // Build user:pass in stack buffer to avoid heap allocation
535 constexpr size_t max_user_info_len = 256;
536 char user_info[max_user_info_len];
537 size_t user_len = strlen(username);
538 size_t pass_len = strlen(password);
539 size_t user_info_len = user_len + 1 + pass_len;
540
541 if (user_info_len >= max_user_info_len) {
542 ESP_LOGW(TAG, "Credentials too long for authentication");
543 return false;
544 }
545
546 memcpy(user_info, username, user_len);
547 user_info[user_len] = ':';
548 memcpy(user_info + user_len + 1, password, pass_len);
549 user_info[user_info_len] = '\0';
550
551 // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
552 // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
553 constexpr size_t max_digest_len = 350;
554 char digest[max_digest_len];
555 size_t out;
556 // The buffer bound above makes failure unreachable; reject rather than
557 // compare against an unwritten digest if that ever changes.
558 if (mbedtls_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
559 reinterpret_cast<const uint8_t *>(user_info), user_info_len) != 0) {
560 return false;
561 }
562
563 // Constant-time comparison to avoid timing side channels.
564 // No early return on length mismatch — the length difference is folded
565 // into the accumulator so any mismatch is rejected.
566 const char *provided = auth_str + auth_prefix_len;
567 size_t digest_len = out;
568 // Derive provided_len from the already-sized std::string rather than
569 // rescanning with strlen (avoids attacker-controlled scan length).
570 size_t provided_len = auth.value().size() - auth_prefix_len;
571 // Use full-width XOR so any bit difference in the lengths is preserved
572 // (uint8_t truncation would miss differences in higher bytes, e.g.
573 // digest_len vs digest_len + 256).
574 volatile size_t result = digest_len ^ provided_len;
575 // Iterate over the expected digest length only — the full-width length
576 // XOR above already rejects any length mismatch, and bounding the loop
577 // prevents a long Authorization header from forcing extra work.
578 for (size_t i = 0; i < digest_len; i++) {
579 char provided_ch = (i < provided_len) ? provided[i] : 0;
580 result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
581 }
582 return result == 0;
583#endif // USE_WEBSERVER_AUTH_DIGEST
584}
585
587 httpd_resp_set_hdr(*this, "Connection", "keep-alive");
588#ifdef USE_WEBSERVER_AUTH_DIGEST
589 // Issue a fresh random nonce and opaque. The nonce is not stored, so this is stateless and
590 // does not defend against replay -- its purpose is to keep the password off the wire.
591 // The header value must stay alive until httpd_resp_send_err() below sends it, so the buffer
592 // lives on this stack frame (httpd_resp_set_hdr stores the pointer, it does not copy).
593 uint8_t random_bytes[16];
594 char nonce[33];
595 char opaque[33];
596 char header[160];
597 esp_fill_random(random_bytes, sizeof(random_bytes));
598 format_hex_to(nonce, random_bytes, sizeof(random_bytes));
599 esp_fill_random(random_bytes, sizeof(random_bytes));
600 format_hex_to(opaque, random_bytes, sizeof(random_bytes));
601 snprintf(header, sizeof(header), R"(Digest realm="Login Required", qop="auth", nonce="%s", opaque="%s")", nonce,
602 opaque);
603 httpd_resp_set_hdr(*this, "WWW-Authenticate", header);
604#else
605 httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\"");
606#endif // USE_WEBSERVER_AUTH_DIGEST
607 httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
608}
609#endif // USE_WEBSERVER_AUTH
610
612 // Check cache first - only successful lookups are cached
613 for (auto *param : this->params_) {
614 if (param->name() == name) {
615 return param;
616 }
617 }
618
619 // Look up value from query strings
620 auto val = this->find_query_value_(name);
621
622 // Don't cache misses to avoid wasting memory when handlers check for
623 // optional parameters that don't exist in the request
624 if (!val.has_value()) {
625 return nullptr;
626 }
627
628 auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
629 this->params_.push_back(param);
630 return param;
631}
632
636template<typename Func>
637static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
638 -> decltype(func(nullptr, size_t{0}, name)) {
639 if (!post_query.empty()) {
640 auto result = func(post_query.c_str(), post_query.size(), name);
641 if (result) {
642 return result;
643 }
644 }
645 // Use httpd API for query length, then access string directly from URI.
646 // http_parser identifies components by offset/length without modifying the URI string.
647 // This is the same pattern used by url_to().
648 auto len = httpd_req_get_url_query_len(req);
649 if (len == 0) {
650 return {};
651 }
652 const char *query = strchr(req->uri, '?');
653 if (query == nullptr) {
654 return {};
655 }
656 query++; // skip '?'
657 return func(query, len, name);
658}
659
660optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
661 return search_query_sources(this->req_, this->post_query_, name,
662 [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
663}
664
665bool AsyncWebServerRequest::hasArg(const char *name) {
666 return search_query_sources(this->req_, this->post_query_, name, query_has_key);
667}
668
669std::string AsyncWebServerRequest::arg(const char *name) {
670 auto val = this->find_query_value_(name);
671 if (val.has_value()) {
672 return std::move(val.value());
673 }
674 return {};
675}
676
677void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
678 httpd_resp_set_hdr(*this->req_, name, value);
679}
680
681void AsyncResponseStream::print(float value) {
682 // Use stack buffer to avoid temporary string allocation
683 // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
684 char buf[32];
685 int len = snprintf(buf, sizeof(buf), "%f", value);
686 this->content_.append(buf, len);
687}
688
689void AsyncResponseStream::printf(const char *fmt, ...) {
690 va_list args;
691
692 va_start(args, fmt);
693 const int length = vsnprintf(nullptr, 0, fmt, args);
694 va_end(args);
695
696 std::string str;
697 str.resize(length);
698
699 va_start(args, fmt);
700 vsnprintf(&str[0], length + 1, fmt, args);
701 va_end(args);
702
703 this->print(str);
704}
705
706#ifdef USE_WEBSERVER
708 LockGuard guard{this->pending_mutex_};
709 for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
710 for (auto *ses : *vec) {
711 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
712 }
713 }
714}
715
717 // Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
718 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
719 auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
720 {
721 LockGuard guard{this->pending_mutex_};
722 this->pending_sessions_.push_back(rsp);
723 this->has_pending_sessions_.store(true, std::memory_order_release);
724 }
726}
727
728// clang-analyzer traces a false-positive leak path from loop() through
729// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and
730// finally ArduinoJson. Suppress along the entire in-our-code call chain.
731// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
733 // Fast path: one atomic load per tick. Slow path is out-of-line on connect.
734 if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
735 this->adopt_pending_sessions_main_loop_();
736 }
737
738 // Clean up dead sessions safely
739 // This follows the ESP-IDF pattern where free_ctx marks resources as dead
740 // and the main loop handles the actual cleanup to avoid race conditions
741 for (size_t i = 0; i < this->sessions_.size();) {
742 auto *ses = this->sessions_[i];
743 // If the session has a dead socket (marked by destroy callback)
744 if (ses->safe_to_delete_()) {
745 // destroy() already logged the close with the fd; don't double-log here.
746 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
747 // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
748 this->sessions_[i] = this->sessions_.back();
749 this->sessions_.pop_back();
750 } else {
751 ses->loop();
752 ++i;
753 }
754 }
755 // A session still waiting for httpd to commit its context keeps the loop alive too
756 return !this->sessions_.empty() || this->has_pending_sessions_.load(std::memory_order_acquire);
757}
758
759void AsyncEventSource::adopt_pending_sessions_main_loop_() {
760 std::vector<AsyncEventSourceResponse *> incoming;
761 {
762 LockGuard guard{this->pending_mutex_};
763 incoming.swap(this->pending_sessions_);
764 this->has_pending_sessions_.store(false, std::memory_order_relaxed);
765 }
766 for (auto *rsp : incoming) {
767 // Already disconnected? Drop it; skip session start on a dead session.
768 if (rsp->safe_to_delete_()) {
769 delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
770 continue;
771 }
772 // httpd commits the session context only after the creating handler returns, so stay
773 // pending until then; httpd_req_cleanup() always commits it or calls destroy()
774 if (httpd_sess_get_ctx(rsp->hd_, rsp->fd_.load()) != rsp) {
775 LockGuard guard{this->pending_mutex_};
776 this->pending_sessions_.push_back(rsp);
777 this->has_pending_sessions_.store(true, std::memory_order_release);
778 continue;
779 }
780 this->sessions_.push_back(rsp);
781 rsp->start_session_main_loop_();
782 }
783}
784// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
785
786void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
787 uint32_t reconnect) {
788 for (auto *ses : this->sessions_) {
789 if (ses->fd_.load() != 0) { // Skip dead sessions
790 ses->try_send_nodefer(message, message_len, event, id, reconnect);
791 }
792 }
793}
794
795void AsyncEventSource::deferrable_send_state(void *source, const char *event_type,
796 message_generator_t *message_generator) {
797 // Skip if no connected clients to avoid unnecessary processing
798 if (this->empty())
799 return;
800 for (auto *ses : this->sessions_) {
801 if (ses->fd_.load() != 0) { // Skip dead sessions
802 ses->deferrable_send_state(source, event_type, message_generator);
803 }
804 }
805}
806
810 : server_(server), web_server_(ws), entities_iterator_(ws, server) {
811 // Httpd task only. start_session_main_loop_() sends the greeting and starts the iterator.
812 httpd_req_t *req = *request;
813
814 httpd_resp_set_status(req, HTTPD_200);
815 httpd_resp_set_type(req, "text/event-stream");
816 httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
817 httpd_resp_set_hdr(req, "Connection", "keep-alive");
818
819 for (const auto &header : DefaultHeaders::Instance().headers_) {
820 httpd_resp_set_hdr(req, header.name, header.value);
821 }
822
823 httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN);
824
825 req->sess_ctx = this;
826 req->free_ctx = AsyncEventSourceResponse::destroy;
827
828 this->hd_ = req->handle;
829 this->fd_.store(httpd_req_to_sockfd(req));
830
831 // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
832 httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
833}
834
835// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
837 auto *ws = this->web_server_;
838
839 // The tcp send buffer is empty on connect. A refusal is a closing session or a failed tail
840 // allocation; nothing retries the greeting, so close and let the client reconnect.
841 auto message = ws->get_config_json();
842 if (!this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000)) {
843 ESP_LOGW(TAG, "Config not sent to fd %d", this->fd_.load());
844 this->request_close_();
845 return;
846 }
847
848#ifdef USE_WEBSERVER_SORTING
849 for (auto &group : ws->sorting_groups_) {
850 json::JsonBuilder builder;
851 JsonObject root = builder.root();
852 root["name"] = group.second.name;
853 root["sorting_weight"] = group.second.weight;
854 message = builder.serialize();
855
856 // a (very) large number of these should be able to be queued initially without defer
857 // since the only thing in the send buffer at this point is the initial ping/config.
858 // A refusal means the socket is full or closing; the remaining groups are not sent. The
859 // session stays up on purpose: partial grouping beats a reconnect loop on a slow link.
860 if (!this->try_send_nodefer(message.c_str(), message.size(), "sorting_group")) {
861 ESP_LOGW(TAG, "Sorting groups not sent to fd %d", this->fd_.load());
862 break;
863 }
864 }
865#endif
866
867 this->entities_iterator_.begin(ws->include_internal_);
868}
869// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
870
872 auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
873 int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
874 ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
875 // Mark as dead - will be cleaned up in the main loop
876 // Note: We don't delete or remove from set here to avoid race conditions
877 // httpd will call our custom close_fn (safe_close_with_shutdown) which handles
878 // shutdown() before close() to prevent lwIP race conditions
879}
880
881// helper for allowing only unique entries in the queue
883 DeferredEvent item(source, message_generator);
884
885 // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
886 for (auto &event : this->deferred_queue_) {
887 if (event == item) {
888 return; // Already in queue, no need to update since items are equal
889 }
890 }
891 this->deferred_queue_.push_back(item);
892}
893
895 if (this->close_requested_) {
896 return;
897 }
898 while (!deferred_queue_.empty()) {
899 DeferredEvent &de = deferred_queue_.front();
900 if (this->send_json_(de.source_, de.message_generator_)) {
901 if (this->close_requested_ || deferred_queue_.empty()) {
902 return;
903 }
904 // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
905 deferred_queue_.erase(deferred_queue_.begin());
906 } else {
907 break;
908 }
909 }
910}
911
913 if (!this->close_requested_) {
914 this->close_requested_ = true;
915 this->deferred_queue_.clear();
916 this->tail_.reset();
917 this->tail_cap_ = 0;
918 this->tail_len_ = 0;
919 this->tail_sent_ = 0;
921 }
922
923 this->process_close_();
924}
925
927 if (!this->close_requested_ || this->close_work_queued_.load(std::memory_order_acquire)) {
928 return;
929 }
930 const int fd = this->fd_.load();
931 if (fd == 0) {
932 return;
933 }
934
936 if (static_cast<int32_t>(now - this->next_close_attempt_ms_) < 0) {
937 return;
938 }
939
940 // Queue an identity-checked shutdown on the HTTPD task. The public
941 // httpd_sess_trigger_close() queues only a reusable fd/session slot and can
942 // therefore close a new client if the original peer disconnects meanwhile.
943 this->close_work_queued_.store(true, std::memory_order_release);
944 const esp_err_t err = httpd_queue_work(this->hd_, &AsyncEventSourceResponse::close_session_work, this);
946 if (err == ESP_OK) {
947 return;
948 }
949
950 this->close_work_queued_.store(false, std::memory_order_release);
951 if (!this->close_retry_warning_logged_) {
952 ESP_LOGW(TAG, "Failed to queue EventSource close (%s); retrying", esp_err_to_name(err));
953 this->close_retry_warning_logged_ = true;
954 }
955}
956
958 // httpd only advances a session's LRU counter when a request arrives, and an event stream receives one
959 // request ever, so the purge that frees a socket for a new client would evict the live streams first.
960 // The HTTPD task owns the session table, so the refresh runs there.
961 // A stalled stream is the socket the purge should take, and a zeroed fd has no session left to refresh.
962 if (this->send_failure_started_ms_ != 0 || this->fd_.load() == 0)
963 return;
966 this->lru_work_queued_.load(std::memory_order_acquire)) {
967 return;
968 }
969 this->last_lru_refresh_ms_ = now; // a full queue also waits for the next interval
970 this->lru_work_queued_.store(true, std::memory_order_release);
971 if (httpd_queue_work(this->hd_, &AsyncEventSourceResponse::refresh_lru_work, this) != ESP_OK) {
972 this->lru_work_queued_.store(false, std::memory_order_release);
973 }
974}
975
977 auto *response = static_cast<AsyncEventSourceResponse *>(arg);
978 const int fd = response->fd_.load();
979 if (response->owns_session_(fd)) {
980 httpd_sess_update_lru_counter(response->hd_, fd);
981 }
982 // Release self only after the HTTPD-task callback has finished every access.
983 response->lru_work_queued_.store(false, std::memory_order_release);
984}
985
987 auto *response = static_cast<AsyncEventSourceResponse *>(arg);
988 const int fd = response->fd_.load();
989 if (response->owns_session_(fd)) {
990 // The HTTPD task remains the session owner. Shutting the socket down makes
991 // its next select/recv path delete the session and invoke destroy().
992 shutdown(fd, SHUT_RDWR);
993 }
994
995 // Release self only after the HTTPD-task callback has finished every access.
996 response->close_work_queued_.store(false, std::memory_order_release);
997}
998
1000 // httpd frees a session before closing its socket, so the fd may already be a new client's.
1001 // Treated as would-block; the stall timer ends a session that never becomes ours again.
1002 const int fd = this->fd_.load();
1003 if (!this->owns_session_(fd)) {
1004 return 0;
1005 }
1006 struct msghdr msg {};
1007 msg.msg_iov = iov;
1008 msg.msg_iovlen = iovcnt;
1009 const ssize_t sent = sendmsg(fd, &msg, MSG_DONTWAIT);
1010 if (sent >= 0) {
1011 return sent;
1012 }
1013 const int err = errno;
1014 if (err == EAGAIN || err == EWOULDBLOCK) {
1015 return 0;
1016 }
1017 ESP_LOGD(TAG, "send error: errno %d", err);
1018 this->request_close_();
1019 return -1;
1020}
1021
1023 if (this->sending_ || this->close_requested_ || this->tail_len_ == 0) {
1024 return;
1025 }
1026 SendGuard guard{*this};
1027
1028 const size_t remaining = this->tail_len_ - this->tail_sent_;
1029 struct iovec iov = {this->tail_.get() + this->tail_sent_, remaining};
1030 const ssize_t sent = this->send_(&iov, 1);
1031 if (sent < 0) {
1032 return;
1033 }
1034 if (sent == 0) {
1035 // Socket buffer full, try again later
1036 // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_().
1037 // The IDF path is intentionally time-based and closes through HTTPD to preserve session ownership.
1039 if (this->send_failure_started_ms_ == 0) {
1040 this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
1041 }
1042 if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
1043 ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu32 " ms without send progress",
1044 now - this->send_failure_started_ms_);
1045 this->request_close_();
1046 }
1047 return;
1048 }
1049
1050 this->send_failure_started_ms_ = 0;
1051 this->tail_sent_ += sent;
1052 if (this->tail_sent_ < this->tail_len_) {
1053 ESP_LOGV(TAG, "Partial send: %zd/%zu bytes (total: %u/%u)", sent, remaining, this->tail_sent_, this->tail_len_);
1054 return;
1055 }
1056 // Fully sent; the storage stays for the next stall
1057 this->tail_len_ = 0;
1058}
1059
1061 if (this->tail_cap_ >= len) {
1062 return true;
1063 }
1064 if (len > TAIL_MAX_SIZE) {
1065 return false;
1066 }
1067 // Nothing is pending while the tail grows, so free the old block first. PREFER_INTERNAL keeps
1068 // the tail where plain new put it.
1069 this->tail_.reset();
1071 this->tail_cap_ = this->tail_ ? len : 0;
1072 return this->tail_cap_ != 0;
1073}
1074
1075bool AsyncEventSourceResponse::stash_chunk_(const char *prefix, size_t prefix_len, const char *message,
1076 size_t message_len, size_t total, size_t sent) {
1077 // A log event of nothing but line breaks is the largest chunk; reserve_tail_() must never
1078 // refuse it for size, only for memory
1080 "the log cut in web_server.h must keep a worst case log event inside the tail ceiling");
1081 if (!this->reserve_tail_(total)) {
1082 if (sent != 0) {
1083 // Part of the chunk is on the wire, so the stream is broken and the client has to go
1084 ESP_LOGW(TAG, "Cannot buffer a %zu byte chunk, closing", total);
1085 this->request_close_();
1086 } else {
1087 this->tail_alloc_failed_(total); // nothing on the wire, the caller retries on the stall clock
1088 }
1089 return false;
1090 }
1091 uint8_t *dst = this->tail_.get();
1092 std::memcpy(dst, prefix, prefix_len);
1093 dst += prefix_len;
1095 message, message_len,
1096 [](void *ctx, const char *piece, size_t len) {
1097 auto &out = *static_cast<uint8_t **>(ctx);
1098 std::memcpy(out, piece, len);
1099 out += len;
1100 },
1101 &dst);
1102 this->tail_len_ = total;
1103 this->tail_sent_ = sent;
1104 return true;
1105}
1106
1108 if (this->close_requested_) {
1109 this->process_close_();
1110 return;
1111 }
1112 this->refresh_lru_();
1113 drain_tail_();
1115 if (this->close_requested_)
1116 return;
1117 // One step per loop; refusals retry next pass
1119}
1120
1121#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
1122#pragma GCC diagnostic push
1123#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the JSON arena
1124#endif
1125AsyncEventSourceResponse::JsonPrepareResult AsyncEventSourceResponse::prepare_json_(void *source,
1126 message_generator_t *generator,
1127 char *buffer, size_t *size_out) {
1128 // Log lines emitted while the generator runs are dropped for this session: on the large path they would fill
1129 // tail_ before it is overwritten, and on the small path they would go out ahead of the event being built.
1130 SendGuard guard{*this};
1132 json::JsonBuilder builder(&arena);
1133 generator(this->web_server_, source, builder);
1134 *size_out = builder.serialize_to(buffer, JSON_BUF_SIZE);
1135 if (*size_out < JSON_BUF_SIZE) {
1137 }
1138
1139 char prefix[PREFIX_BUF_SIZE];
1140 const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
1141
1142 // Nothing reaches the wire until the complete document and its framing are owned by the caller.
1143 size_t json_len = 0;
1144 size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
1145 for (;;) {
1146 if (!this->reserve_tail_(cap)) {
1147 *size_out = cap;
1149 }
1150 const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
1151 json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
1152 if (json_len < room) {
1153 break;
1154 }
1155 if (cap >= TAIL_MAX_SIZE) {
1156 *size_out = room;
1158 }
1159 cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
1160 }
1161
1162 const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
1164 uint8_t *dst = this->tail_.get();
1165 std::memcpy(dst, prefix, prefix_len);
1166 std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
1167 this->tail_len_ = total;
1168 this->tail_sent_ = 0;
1170}
1171#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
1172#pragma GCC diagnostic pop
1173#endif
1174
1176 if (!this->ready_to_send_()) {
1177 return false;
1178 }
1179 char buffer[JSON_BUF_SIZE];
1180 size_t size;
1181 const auto result = this->prepare_json_(source, generator, buffer, &size);
1182 switch (result) {
1184 return this->try_send_nodefer(buffer, size, "state");
1186 this->drain_tail_();
1187 return true;
1189 SendGuard guard{*this};
1190 this->tail_alloc_failed_(size);
1191 return false;
1192 }
1194 SendGuard guard{*this};
1195 ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", size);
1196 this->tail_.reset();
1197 this->tail_cap_ = 0;
1198 this->send_failure_started_ms_ = 0;
1199 return true;
1200 }
1201 }
1202 return false;
1203}
1204
1206 // Same stall clock as a socket that stops draining, so a session cannot retry forever
1208 if (this->send_failure_started_ms_ == 0) {
1209 this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
1210 ESP_LOGW(TAG, "No memory for a %zu byte chunk", cap);
1211 return;
1212 }
1213 if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
1214 ESP_LOGW(TAG, "Closing EventSource after %" PRIu32 " ms without memory", now - this->send_failure_started_ms_);
1215 this->request_close_();
1216 }
1217}
1218
1220 if (this->sending_ || this->fd_.load() == 0 || this->close_requested_) {
1221 return false;
1222 }
1223 drain_tail_();
1224 return !this->close_requested_ && this->tail_len_ == 0;
1225}
1226
1227bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
1228 uint32_t reconnect) {
1229 if (!this->ready_to_send_()) {
1230 return false;
1231 }
1232 SendGuard guard{*this};
1233
1234 // Everything after the prefix goes out straight from the caller's buffer
1235 char prefix[PREFIX_BUF_SIZE];
1236 const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), event, id, reconnect, message != nullptr);
1237 if (message == nullptr && prefix_len == CHUNK_HDR_LEN) {
1238 return true; // Match ESPAsyncWebServer: nothing to send
1239 }
1240 if (prefix_len >= PREFIX_BUF_SIZE - 1) {
1241 // The appenders truncate silently, which would put a malformed event on the wire
1242 ESP_LOGW(TAG, "Event name too long, dropped");
1243 return true;
1244 }
1245
1246 // Gather list: the prefix, then the data lines and their separators from the message
1247 struct Gather {
1248 struct iovec iov[MAX_SEND_IOV]; // left uninitialized on purpose
1249 int iovcnt{1};
1250 size_t total{0};
1251 bool fits{true};
1252 } g;
1253 g.total = prefix_len;
1255 message, message_len,
1256 [](void *ctx, const char *piece, size_t len) {
1257 auto &g = *static_cast<Gather *>(ctx);
1258 g.total += len;
1259 if (len == 0) {
1260 return;
1261 }
1262 if (g.iovcnt == MAX_SEND_IOV) {
1263 g.fits = false;
1264 return;
1265 }
1266 g.iov[g.iovcnt++] = {const_cast<char *>(piece), len};
1267 },
1268 &g);
1269 // The header and the terminator are not part of the chunk length
1270 write_chunk_header(prefix, g.total - CHUNK_HDR_LEN - CHUNK_END_LEN);
1271 g.iov[0] = {prefix, prefix_len};
1272
1273 // A message with more lines than the list holds skips straight to the tail
1274 const ssize_t sent = g.fits ? this->send_(g.iov, g.iovcnt) : 0;
1275 if (sent < 0) {
1276 return false;
1277 }
1278 if (static_cast<size_t>(sent) == g.total) {
1279 this->send_failure_started_ms_ = 0; // progress, whichever stall clock was running
1280 return true;
1281 }
1282 // The caller's buffers do not outlive this call, so keep the chunk and continue from loop()
1283 return this->stash_chunk_(prefix, prefix_len, message, message_len, g.total, sent);
1284}
1285
1286void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *event_type,
1287 message_generator_t *message_generator) {
1288 // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
1289 // up in the web GUI and reduces event load during initial connect
1290 if (!this->entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
1291 return;
1292
1293 if (source == nullptr)
1294 return;
1295 if (event_type == nullptr)
1296 return;
1297 if (message_generator == nullptr)
1298 return;
1299
1300 if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) {
1301 ESP_LOGE(TAG, "Can't defer non-state event");
1302 }
1303
1304 drain_tail_();
1306
1307 if (this->close_requested_) {
1308 return;
1309 }
1310
1311 if (this->tail_len_ != 0 || !deferred_queue_.empty()) {
1312 // outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
1313 // trying to send first
1314 deq_push_back_with_dedup_(source, message_generator);
1315 } else {
1316 // A send error closes the session and clears the queue; nothing is queued after that
1317 if (!this->send_json_(source, message_generator) && !this->close_requested_) {
1318 deq_push_back_with_dedup_(source, message_generator);
1319 }
1320 }
1321}
1322#endif
1323
1324#ifdef USE_WEBSERVER_OTA
1325esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
1326 // Parse boundary and create reader
1327 const char *boundary_start;
1328 size_t boundary_len;
1329 if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
1330 ESP_LOGE(TAG, "Failed to parse multipart boundary");
1331 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
1332 return ESP_FAIL;
1333 }
1334
1335 AsyncWebServerRequest req(r);
1336 AsyncWebHandler *handler = nullptr;
1337 for (auto *h : this->handlers_) {
1338 if (h->canHandle(&req)) {
1339 handler = h;
1340 break;
1341 }
1342 }
1343
1344 if (!handler) {
1345 ESP_LOGW(TAG, "No handler found for OTA request");
1346 httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
1347 return ESP_OK;
1348 }
1349
1350 // Upload state
1351 std::string filename;
1352 size_t index = 0;
1353 // Create reader on heap to reduce stack usage
1354 auto reader = std::make_unique<MultipartReader>("--" + std::string(boundary_start, boundary_len));
1355
1356 // Configure callbacks
1357 reader->set_data_callback([&](const uint8_t *data, size_t len) {
1358 if (!reader->has_file() || !len)
1359 return;
1360
1361 if (filename.empty()) {
1362 filename = reader->get_current_part().filename;
1363 ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
1364 handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start
1365 }
1366
1367 handler->handleUpload(&req, filename, index, const_cast<uint8_t *>(data), len, false);
1368 index += len;
1369 });
1370
1371 reader->set_part_complete_callback([&]() {
1372 if (index > 0) {
1373 handler->handleUpload(&req, filename, index, nullptr, 0, true); // End
1374 filename.clear();
1375 index = 0;
1376 }
1377 });
1378
1379 auto buffer = std::make_unique_for_overwrite<char[]>(RECV_CHUNK_SIZE);
1380 size_t bytes_since_yield = 0;
1381
1382 for (size_t remaining = r->content_len; remaining > 0;) {
1383 int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, RECV_CHUNK_SIZE));
1384
1385 if (recv_len <= 0) {
1386 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
1387 nullptr);
1388 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
1389 }
1390
1391 if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
1392 ESP_LOGW(TAG, "Multipart parser error");
1393 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
1394 return ESP_FAIL;
1395 }
1396
1397 remaining -= recv_len;
1398 bytes_since_yield += recv_len;
1399
1400 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
1401 vTaskDelay(1);
1402 bytes_since_yield = 0;
1403 }
1404 }
1405
1406 handler->handleRequest(&req);
1407 return ESP_OK;
1408}
1409#endif // USE_WEBSERVER_OTA
1410
1411} // namespace esphome::web_server_idf
1412
1413#endif // !defined(USE_ESP32)
uint8_t h
Definition bl0906.h:2
uint8_t status
Definition bl0942.h:8
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 enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void begin(bool include_internal=false)
ESPHOME_ALWAYS_INLINE void try_advance(size_t max_steps)
Run up to max_steps iteration steps; stops early when iteration completes or a callback refuses (that...
Helper class that wraps a mutex with a RAII-style API.
Definition helpers.h:2066
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2231
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:2304
StringRef is a reference to a string owned by something else.
Definition string_ref.h:30
constexpr const char * c_str() const
Definition string_ref.h:77
constexpr size_type size() const
Definition string_ref.h:78
Bump allocator over a fixed buffer for a document built and serialized in one scope.
Definition json_util.h:182
Builder class for creating JSON documents without lambdas.
Definition json_util.h:244
SerializationBuffer serialize()
Serialize the JSON document to a SerializationBuffer (stack-first allocation) Uses 512-byte stack buf...
Definition json_util.cpp:96
This class allows users to create a web server with their ESP nodes.
Definition web_server.h:194
json::SerializationBuffer get_config_json()
Return the webserver configuration as JSON.
std::vector< AsyncEventSourceResponse * > pending_sessions_
bool loop()
Returns true if there are sessions remaining (including pending cleanup).
std::vector< AsyncEventSourceResponse * > sessions_
void try_send_nodefer(const char *message, size_t message_len, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
esphome::web_server::WebServer * web_server_
void handleRequest(AsyncWebServerRequest *request) override
bool try_send_nodefer(const char *message, size_t message_len, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
JsonPrepareResult message_generator_t char * buffer
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
esphome::web_server::WebServer * web_server_
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator)
AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server, esphome::web_server::WebServer *ws)
JsonPrepareResult message_generator_t * generator
JsonPrepareResult message_generator_t char size_t * size_out
ssize_t send_(struct iovec *iov, int iovcnt)
bool stash_chunk_(const char *prefix, size_t prefix_len, const char *message, size_t message_len, size_t total, size_t sent)
esphome::web_server::ListEntitiesIterator entities_iterator_
bool send_json_(void *source, message_generator_t *generator)
void printf(const char *fmt,...) __attribute__((format(printf
virtual void handleRequest(AsyncWebServerRequest *request)
virtual void handleUpload(AsyncWebServerRequest *request, const std::string &filename, size_t index, uint8_t *data, size_t len, bool final)
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
std::function< void(AsyncWebServerRequest *request)> on_not_found_
esp_err_t handle_raw_body_(httpd_req_t *r, const char *content_type)
static esp_err_t request_post_handler(httpd_req_t *r)
std::vector< AsyncWebHandler * > handlers_
esp_err_t request_handler_(AsyncWebServerRequest *request) const
esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type)
static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd)
static esp_err_t request_handler(httpd_req_t *r)
AsyncWebParameter * getParam(const char *name)
optional< std::string > get_header(const char *name) const
StringRef url_to(std::span< char, URL_BUF_SIZE > buffer) const
Write URL (without query string) to buffer, returns StringRef pointing to buffer.
void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type)
static constexpr size_t URL_BUF_SIZE
Buffer size for url_to()
optional< std::string > find_query_value_(const char *name) const
bool authenticate(const char *username, const char *password) const
std::vector< AsyncWebParameter * > params_
void addHeader(const char *name, const char *value)
const LogString * message
Definition component.cpp:35
uint16_t flags
std::vector< uint8_t > & out
Definition gif.cpp:49
uint32_t buffer
Definition gif.cpp:50
__int64 ssize_t
Definition httplib.h:178
int ret
mopeka_std_values val[3]
const char *const TAG
Definition spi.cpp:7
constexpr size_t CHUNK_END_LEN
Definition sse_chunk.h:17
size_t build_chunk_prefix(char *buf, size_t size, const char *event, uint32_t id, uint32_t reconnect, bool with_data)
Definition sse_chunk.cpp:38
bool query_has_key(const char *query_url, size_t query_len, const char *key)
Definition utils.cpp:70
void(esphome::web_server::WebServer *, void *, json::JsonBuilder &) message_generator_t
optional< std::string > request_get_header(httpd_req_t *req, const char *name)
Definition utils.cpp:36
constexpr size_t SSE_SUFFIX_LEN
Definition sse_chunk.h:23
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len)
optional< std::string > query_key_value(const char *query_url, size_t query_len, const char *key)
Definition utils.cpp:53
const char * strcasestr_n(const char *haystack, size_t haystack_len, const char *needle)
Definition utils.cpp:93
constexpr size_t SSE_SEP_LEN
Definition sse_chunk.h:20
void write_chunk_header(char *buf, size_t chunk_len)
Definition sse_chunk.cpp:57
constexpr size_t CHUNK_HDR_LEN
Definition sse_chunk.h:14
constexpr char SSE_SUFFIX[]
Definition sse_chunk.h:22
size_t url_decode(char *str)
Decode URL-encoded string in-place (e.g., %20 -> space, + -> space) Returns the new length of the dec...
Definition utils.cpp:11
bool request_has_header(httpd_req_t *req, const char *name)
Definition utils.cpp:34
void for_each_chunk_piece(const char *message, size_t message_len, ChunkPieceSink sink, void *ctx)
Definition sse_chunk.cpp:10
constexpr size_t LOG_EVENT_MAX_LEN
Definition web_server.h:42
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
va_end(args)
const void size_t len
Definition hal.h:64
uint16_t size
Definition helpers.cpp:33
size_t size_t const char va_start(args, fmt)
size_t size_t const char * fmt
Definition helpers.h:1187
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
char * format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length)
Format byte array as lowercase hex to buffer (base implementation).
Definition helpers.cpp:361
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
std::string print()
uint16_t length
Definition tt21100.cpp:0