ESPHome 2026.1.4
Loading...
Searching...
No Matches
web_server_idf.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include <cstdarg>
4#include <memory>
5#include <cstring>
6#include <cctype>
7#include <cinttypes>
8
10#include "esphome/core/log.h"
11
12#include "esp_tls_crypto.h"
13#include <freertos/FreeRTOS.h>
14#include <freertos/task.h>
15
16#include "utils.h"
17#include "web_server_idf.h"
18
19#ifdef USE_WEBSERVER_OTA
20#include <multipart_parser.h>
21#include "multipart.h" // For parse_multipart_boundary and other utils
22#endif
23
24#ifdef USE_WEBSERVER
27#endif // USE_WEBSERVER
28
29// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
30#include <cerrno>
31#include <sys/socket.h>
32
33namespace esphome {
34namespace web_server_idf {
35
36#ifndef HTTPD_409
37#define HTTPD_409 "409 Conflict"
38#endif
39
40#define CRLF_STR "\r\n"
41#define CRLF_LEN (sizeof(CRLF_STR) - 1)
42
43static const char *const TAG = "web_server_idf";
44
45// Global instance to avoid guard variable (saves 8 bytes)
46// This is initialized at program startup before any threads
47namespace {
48// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
49DefaultHeaders default_headers_instance;
50} // namespace
51
52DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
53
54namespace {
55// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
70int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
71 if (buf == nullptr) {
72 return HTTPD_SOCK_ERR_INVALID;
73 }
74
75 // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
76 int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
77 if (ret < 0) {
78 if (errno == EAGAIN || errno == EWOULDBLOCK) {
79 // Buffer full - retry later
80 return HTTPD_SOCK_ERR_TIMEOUT;
81 }
82 // Real error
83 ESP_LOGD(TAG, "send error: errno %d", errno);
84 return HTTPD_SOCK_ERR_FAIL;
85 }
86 return ret;
87}
88} // namespace
89
90void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
91 // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions
92 //
93 // The race condition occurs because close() initiates lwIP teardown while
94 // the TCP/IP thread can still receive packets, causing assertions when
95 // recv_tcp() sees partially-torn-down state.
96 //
97 // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE
98 // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR)
99 // to allow the FIN packet to be sent cleanly during close().
100 //
101 // Note: This function may be called with an already-closed socket if the network
102 // stack closed it. In that case, shutdown() will fail but close() is safe to call.
103 //
104 // See: https://github.com/esphome/esphome-webserver/issues/163
105
106 // Attempt shutdown - ignore errors as socket may already be closed
107 shutdown(sockfd, SHUT_RD);
108
109 // Always close - safe even if socket is already closed by network stack
110 close(sockfd);
111}
112
114 if (this->server_) {
115 httpd_stop(this->server_);
116 this->server_ = nullptr;
117 }
118}
119
121 if (this->server_) {
122 this->end();
123 }
124 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
125 config.server_port = this->port_;
126 config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
127 // Always enable LRU purging to handle socket exhaustion gracefully.
128 // When max sockets is reached, the oldest connection is closed to make room for new ones.
129 // This prevents "httpd_accept_conn: error in accept (23)" errors.
130 // See: https://github.com/esphome/esphome/issues/12464
131 config.lru_purge_enable = true;
132 // Use custom close function that shuts down before closing to prevent lwIP race conditions
134 if (httpd_start(&this->server_, &config) == ESP_OK) {
135 const httpd_uri_t handler_get = {
136 .uri = "",
137 .method = HTTP_GET,
139 .user_ctx = this,
140 };
141 httpd_register_uri_handler(this->server_, &handler_get);
142
143 const httpd_uri_t handler_post = {
144 .uri = "",
145 .method = HTTP_POST,
147 .user_ctx = this,
148 };
149 httpd_register_uri_handler(this->server_, &handler_post);
150
151 const httpd_uri_t handler_options = {
152 .uri = "",
153 .method = HTTP_OPTIONS,
155 .user_ctx = this,
156 };
157 httpd_register_uri_handler(this->server_, &handler_options);
158 }
159}
160
161esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
162 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
163 auto content_type = request_get_header(r, "Content-Type");
164
165 if (!request_has_header(r, "Content-Length")) {
166 ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
167 httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
168 return ESP_OK;
169 }
170
171 if (content_type.has_value()) {
172 const char *content_type_char = content_type.value().c_str();
173
174 // Check most common case first
175 if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
176 // Normal form data - proceed with regular handling
177#ifdef USE_WEBSERVER_OTA
178 } else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
179 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
180 return server->handle_multipart_upload_(r, content_type_char);
181#endif
182 } else {
183 ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char);
184 // fallback to get handler to support backward compatibility
186 }
187 }
188
189 // Handle regular form data
190 if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
191 ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
192 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
193 return ESP_FAIL;
194 }
195
196 std::string post_query;
197 if (r->content_len > 0) {
198 post_query.resize(r->content_len);
199 const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1);
200 if (ret <= 0) { // 0 return value indicates connection closed
201 if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
202 httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr);
203 return ESP_ERR_TIMEOUT;
204 }
205 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
206 return ESP_FAIL;
207 }
208 }
209
210 AsyncWebServerRequest req(r, std::move(post_query));
211 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
212}
213
214esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) {
215 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri);
217 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
218}
219
221 for (auto *handler : this->handlers_) {
222 if (handler->canHandle(request)) {
223 // At now process only basic requests.
224 // OTA requires multipart request support and handleUpload for it
225 handler->handleRequest(request);
226 return ESP_OK;
227 }
228 }
229 if (this->on_not_found_) {
230 this->on_not_found_(request);
231 return ESP_OK;
232 }
233 return ESP_ERR_NOT_FOUND;
234}
235
237 delete this->rsp_;
238 for (auto *param : this->params_) {
239 delete param; // NOLINT(cppcoreguidelines-owning-memory)
240 }
241}
242
243bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); }
244
246 return request_get_header(*this, name);
247}
248
249std::string AsyncWebServerRequest::url() const {
250 auto *query_start = strchr(this->req_->uri, '?');
251 std::string result;
252 if (query_start == nullptr) {
253 result = this->req_->uri;
254 } else {
255 result = std::string(this->req_->uri, query_start - this->req_->uri);
256 }
257 // Decode URL-encoded characters in-place (e.g., %20 -> space)
258 // This matches AsyncWebServer behavior on Arduino
259 if (!result.empty()) {
260 size_t new_len = url_decode(&result[0]);
261 result.resize(new_len);
262 }
263 return result;
264}
265
266std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); }
267
269 httpd_resp_send(*this, response->get_content_data(), response->get_content_size());
270}
271
272void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) {
273 this->init_response_(nullptr, code, content_type);
274 if (content) {
275 httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN);
276 } else {
277 httpd_resp_send(*this, nullptr, 0);
278 }
279}
280
281void AsyncWebServerRequest::redirect(const std::string &url) {
282 httpd_resp_set_status(*this, "302 Found");
283 httpd_resp_set_hdr(*this, "Location", url.c_str());
284 httpd_resp_set_hdr(*this, "Connection", "close");
285 httpd_resp_send(*this, nullptr, 0);
286}
287
288void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
289 // Set status code - use constants for common codes, default to 500 for unknown codes
290 const char *status;
291 switch (code) {
292 case 200:
293 status = HTTPD_200;
294 break;
295 case 404:
296 status = HTTPD_404;
297 break;
298 case 409:
299 status = HTTPD_409;
300 break;
301 default:
302 status = HTTPD_500;
303 break;
304 }
305 httpd_resp_set_status(*this, status);
306
307 if (content_type && *content_type) {
308 httpd_resp_set_type(*this, content_type);
309 }
310 httpd_resp_set_hdr(*this, "Accept-Ranges", "none");
311
312 for (const auto &header : DefaultHeaders::Instance().headers_) {
313 httpd_resp_set_hdr(*this, header.name, header.value);
314 }
315
316 delete this->rsp_;
317 this->rsp_ = rsp;
318}
319
320#ifdef USE_WEBSERVER_AUTH
321bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
322 if (username == nullptr || password == nullptr || *username == 0) {
323 return true;
324 }
325 auto auth = this->get_header("Authorization");
326 if (!auth.has_value()) {
327 return false;
328 }
329
330 auto *auth_str = auth.value().c_str();
331
332 const auto auth_prefix_len = sizeof("Basic ") - 1;
333 if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) {
334 ESP_LOGW(TAG, "Only Basic authorization supported yet");
335 return false;
336 }
337
338 // Build user:pass in stack buffer to avoid heap allocation
339 constexpr size_t max_user_info_len = 256;
340 char user_info[max_user_info_len];
341 size_t user_len = strlen(username);
342 size_t pass_len = strlen(password);
343 size_t user_info_len = user_len + 1 + pass_len;
344
345 if (user_info_len >= max_user_info_len) {
346 ESP_LOGW(TAG, "Credentials too long for authentication");
347 return false;
348 }
349
350 memcpy(user_info, username, user_len);
351 user_info[user_len] = ':';
352 memcpy(user_info + user_len + 1, password, pass_len);
353 user_info[user_info_len] = '\0';
354
355 size_t n = 0, out;
356 esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast<const uint8_t *>(user_info), user_info_len);
357
358 auto digest = std::unique_ptr<char[]>(new char[n + 1]);
359 esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out,
360 reinterpret_cast<const uint8_t *>(user_info), user_info_len);
361
362 return strcmp(digest.get(), auth_str + auth_prefix_len) == 0;
363}
364
365void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
366 httpd_resp_set_hdr(*this, "Connection", "keep-alive");
367 // Note: realm is never configured in ESPHome, always nullptr -> "Login Required"
368 (void) realm; // Unused - always use default
369 httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\"");
370 httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
371}
372#endif
373
375 // Check cache first - only successful lookups are cached
376 for (auto *param : this->params_) {
377 if (param->name() == name) {
378 return param;
379 }
380 }
381
382 // Look up value from query strings
384 if (!val.has_value()) {
385 auto url_query = request_get_url_query(*this);
386 if (url_query.has_value()) {
387 val = query_key_value(url_query.value(), name);
388 }
389 }
390
391 // Don't cache misses to avoid wasting memory when handlers check for
392 // optional parameters that don't exist in the request
393 if (!val.has_value()) {
394 return nullptr;
395 }
396
397 auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
398 this->params_.push_back(param);
399 return param;
400}
401
402void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
403 httpd_resp_set_hdr(*this->req_, name, value);
404}
405
406void AsyncResponseStream::print(float value) {
407 // Use stack buffer to avoid temporary string allocation
408 // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
409 char buf[32];
410 int len = snprintf(buf, sizeof(buf), "%f", value);
411 this->content_.append(buf, len);
412}
413
414void AsyncResponseStream::printf(const char *fmt, ...) {
415 va_list args;
416
417 va_start(args, fmt);
418 const int length = vsnprintf(nullptr, 0, fmt, args);
419 va_end(args);
420
421 std::string str;
422 str.resize(length);
423
424 va_start(args, fmt);
425 vsnprintf(&str[0], length + 1, fmt, args);
426 va_end(args);
427
428 this->print(str);
429}
430
431#ifdef USE_WEBSERVER
433 for (auto *ses : this->sessions_) {
434 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
435 }
436}
437
439 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
440 auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
441 if (this->on_connect_) {
442 this->on_connect_(rsp);
443 }
444 this->sessions_.push_back(rsp);
445}
446
448 // Clean up dead sessions safely
449 // This follows the ESP-IDF pattern where free_ctx marks resources as dead
450 // and the main loop handles the actual cleanup to avoid race conditions
451 for (size_t i = 0; i < this->sessions_.size();) {
452 auto *ses = this->sessions_[i];
453 // If the session has a dead socket (marked by destroy callback)
454 if (ses->fd_.load() == 0) {
455 ESP_LOGD(TAG, "Removing dead event source session");
456 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
457 // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
458 this->sessions_[i] = this->sessions_.back();
459 this->sessions_.pop_back();
460 } else {
461 ses->loop();
462 ++i;
463 }
464 }
465}
466
467void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
468 for (auto *ses : this->sessions_) {
469 if (ses->fd_.load() != 0) { // Skip dead sessions
470 ses->try_send_nodefer(message, event, id, reconnect);
471 }
472 }
473}
474
475void AsyncEventSource::deferrable_send_state(void *source, const char *event_type,
476 message_generator_t *message_generator) {
477 // Skip if no connected clients to avoid unnecessary processing
478 if (this->empty())
479 return;
480 for (auto *ses : this->sessions_) {
481 if (ses->fd_.load() != 0) { // Skip dead sessions
482 ses->deferrable_send_state(source, event_type, message_generator);
483 }
484 }
485}
486
490 : server_(server), web_server_(ws), entities_iterator_(new esphome::web_server::ListEntitiesIterator(ws, server)) {
491 httpd_req_t *req = *request;
492
493 httpd_resp_set_status(req, HTTPD_200);
494 httpd_resp_set_type(req, "text/event-stream");
495 httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
496 httpd_resp_set_hdr(req, "Connection", "keep-alive");
497
498 for (const auto &header : DefaultHeaders::Instance().headers_) {
499 httpd_resp_set_hdr(req, header.name, header.value);
500 }
501
502 httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN);
503
504 req->sess_ctx = this;
505 req->free_ctx = AsyncEventSourceResponse::destroy;
506
507 this->hd_ = req->handle;
508 this->fd_.store(httpd_req_to_sockfd(req));
509
510 // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
511 httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
512
513 // Configure reconnect timeout and send config
514 // this should always go through since the tcp send buffer is empty on connect
515 std::string message = ws->get_config_json();
516 this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
517
518#ifdef USE_WEBSERVER_SORTING
519 for (auto &group : ws->sorting_groups_) {
520 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
521 json::JsonBuilder builder;
522 JsonObject root = builder.root();
523 root["name"] = group.second.name;
524 root["sorting_weight"] = group.second.weight;
525 message = builder.serialize();
526 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
527
528 // a (very) large number of these should be able to be queued initially without defer
529 // since the only thing in the send buffer at this point is the initial ping/config
530 this->try_send_nodefer(message.c_str(), "sorting_group");
531 }
532#endif
533
535
536 // just dump them all up-front and take advantage of the deferred queue
537 // on second thought that takes too long, but leaving the commented code here for debug purposes
538 // while(!this->entities_iterator_->completed()) {
539 // this->entities_iterator_->advance();
540 //}
541}
542
544 auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
545 int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
546 ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
547 // Mark as dead - will be cleaned up in the main loop
548 // Note: We don't delete or remove from set here to avoid race conditions
549 // httpd will call our custom close_fn (safe_close_with_shutdown) which handles
550 // shutdown() before close() to prevent lwIP race conditions
551}
552
553// helper for allowing only unique entries in the queue
555 DeferredEvent item(source, message_generator);
556
557 // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
558 for (auto &event : this->deferred_queue_) {
559 if (event == item) {
560 return; // Already in queue, no need to update since items are equal
561 }
562 }
563 this->deferred_queue_.push_back(item);
564}
565
567 while (!deferred_queue_.empty()) {
568 DeferredEvent &de = deferred_queue_.front();
569 std::string message = de.message_generator_(web_server_, de.source_);
570 if (this->try_send_nodefer(message.c_str(), "state")) {
571 // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
572 deferred_queue_.erase(deferred_queue_.begin());
573 } else {
574 break;
575 }
576 }
577}
578
580 if (event_buffer_.empty()) {
581 return;
582 }
583 if (event_bytes_sent_ == event_buffer_.size()) {
584 event_buffer_.resize(0);
586 return;
587 }
588
589 size_t remaining = event_buffer_.size() - event_bytes_sent_;
590 int bytes_sent =
591 httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
592 if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
593 // EAGAIN/EWOULDBLOCK - socket buffer full, try again later
594 // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
595 // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
596 // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
597 // update the Arduino implementation.
600 // Too many failures, connection is likely dead
601 ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
603 this->fd_.store(0); // Mark for cleanup
604 this->deferred_queue_.clear();
605 }
606 return;
607 }
608 if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
609 // Real socket error - connection will be closed by httpd and destroy callback will be called
610 return;
611 }
612 if (bytes_sent <= 0) {
613 // Unexpected error or zero bytes sent
614 ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
615 return;
616 }
617
618 // Successful send - reset failure counter
620 event_bytes_sent_ += bytes_sent;
621
622 // Log partial sends for debugging
623 if (event_bytes_sent_ < event_buffer_.size()) {
624 ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
625 event_buffer_.size());
626 }
627
628 if (event_bytes_sent_ == event_buffer_.size()) {
629 event_buffer_.resize(0);
631 }
632}
633
640
641bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
642 uint32_t reconnect) {
643 if (this->fd_.load() == 0) {
644 return false;
645 }
646
648 if (!event_buffer_.empty()) {
649 // there is still pending event data to send first
650 return false;
651 }
652
653 // 8 spaces are standing in for the hexidecimal chunk length to print later
654 const char chunk_len_header[] = " " CRLF_STR;
655 const int chunk_len_header_len = sizeof(chunk_len_header) - 1;
656
657 event_buffer_.append(chunk_len_header);
658
659 // Use stack buffer for formatting numeric fields to avoid temporary string allocations
660 // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety
661 constexpr size_t num_buf_size = 32;
662 char num_buf[num_buf_size];
663
664 if (reconnect) {
665 int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect);
666 event_buffer_.append(num_buf, len);
667 }
668
669 if (id) {
670 int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id);
671 event_buffer_.append(num_buf, len);
672 }
673
674 if (event && *event) {
675 event_buffer_.append("event: ", sizeof("event: ") - 1);
676 event_buffer_.append(event);
677 event_buffer_.append(CRLF_STR, CRLF_LEN);
678 }
679
680 // Match ESPAsyncWebServer: null message means no data lines and no terminating blank line
681 if (message) {
682 // SSE spec requires each line of a multi-line message to have its own "data:" prefix
683 // Handle \n, \r, and \r\n line endings (matching ESPAsyncWebServer behavior)
684
685 // Fast path: check if message contains any newlines at all
686 // Most SSE messages (JSON state updates) have no newlines
687 const char *first_n = strchr(message, '\n');
688 const char *first_r = strchr(message, '\r');
689
690 if (first_n == nullptr && first_r == nullptr) {
691 // No newlines - fast path (most common case)
692 event_buffer_.append("data: ", sizeof("data: ") - 1);
693 event_buffer_.append(message);
694 event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator
695 } else {
696 // Has newlines - handle multi-line message
697 const char *line_start = message;
698 size_t msg_len = strlen(message);
699 const char *msg_end = message + msg_len;
700
701 // Reuse the first search results
702 const char *next_n = first_n;
703 const char *next_r = first_r;
704
705 while (line_start <= msg_end) {
706 const char *line_end;
707 const char *next_line;
708
709 if (next_n == nullptr && next_r == nullptr) {
710 // No more line breaks - output remaining text as final line
711 event_buffer_.append("data: ", sizeof("data: ") - 1);
712 event_buffer_.append(line_start);
713 event_buffer_.append(CRLF_STR, CRLF_LEN);
714 break;
715 }
716
717 // Determine line ending type and next line start
718 if (next_n != nullptr && next_r != nullptr) {
719 if (next_r + 1 == next_n) {
720 // \r\n sequence
721 line_end = next_r;
722 next_line = next_n + 1;
723 } else {
724 // Mixed \n and \r - use whichever comes first
725 line_end = (next_r < next_n) ? next_r : next_n;
726 next_line = line_end + 1;
727 }
728 } else if (next_n != nullptr) {
729 // Unix LF
730 line_end = next_n;
731 next_line = next_n + 1;
732 } else {
733 // Old Mac CR
734 line_end = next_r;
735 next_line = next_r + 1;
736 }
737
738 // Output this line
739 event_buffer_.append("data: ", sizeof("data: ") - 1);
740 event_buffer_.append(line_start, line_end - line_start);
741 event_buffer_.append(CRLF_STR, CRLF_LEN);
742
743 line_start = next_line;
744
745 // Check if we've consumed all content
746 if (line_start >= msg_end) {
747 break;
748 }
749
750 // Search for next newlines only in remaining string
751 next_n = strchr(line_start, '\n');
752 next_r = strchr(line_start, '\r');
753 }
754
755 // Terminate message with blank line
756 event_buffer_.append(CRLF_STR, CRLF_LEN);
757 }
758 }
759
760 if (event_buffer_.size() == static_cast<size_t>(chunk_len_header_len)) {
761 // Nothing was added, reset buffer
762 event_buffer_.resize(0);
763 return true;
764 }
765
766 event_buffer_.append(CRLF_STR, CRLF_LEN);
767
768 // chunk length header itself and the final chunk terminating CRLF are not counted as part of the chunk
769 int chunk_len = event_buffer_.size() - CRLF_LEN - chunk_len_header_len;
770 char chunk_len_str[9];
771 snprintf(chunk_len_str, 9, "%08x", chunk_len);
772 std::memcpy(&event_buffer_[0], chunk_len_str, 8);
773
776
777 return true;
778}
779
780void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *event_type,
781 message_generator_t *message_generator) {
782 // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
783 // up in the web GUI and reduces event load during initial connect
784 if (!entities_iterator_->completed() && 0 != strcmp(event_type, "state_detail_all"))
785 return;
786
787 if (source == nullptr)
788 return;
789 if (event_type == nullptr)
790 return;
791 if (message_generator == nullptr)
792 return;
793
794 if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) {
795 ESP_LOGE(TAG, "Can't defer non-state event");
796 }
797
800
801 if (!event_buffer_.empty() || !deferred_queue_.empty()) {
802 // outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
803 // trying to send first
804 deq_push_back_with_dedup_(source, message_generator);
805 } else {
806 std::string message = message_generator(web_server_, source);
807 if (!this->try_send_nodefer(message.c_str(), "state")) {
808 deq_push_back_with_dedup_(source, message_generator);
809 }
810 }
811}
812#endif
813
814#ifdef USE_WEBSERVER_OTA
815esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
816 static constexpr size_t MULTIPART_CHUNK_SIZE = 1460; // Match Arduino AsyncWebServer buffer size
817 static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog
818
819 // Parse boundary and create reader
820 const char *boundary_start;
821 size_t boundary_len;
822 if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
823 ESP_LOGE(TAG, "Failed to parse multipart boundary");
824 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
825 return ESP_FAIL;
826 }
827
829 AsyncWebHandler *handler = nullptr;
830 for (auto *h : this->handlers_) {
831 if (h->canHandle(&req)) {
832 handler = h;
833 break;
834 }
835 }
836
837 if (!handler) {
838 ESP_LOGW(TAG, "No handler found for OTA request");
839 httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
840 return ESP_OK;
841 }
842
843 // Upload state
844 std::string filename;
845 size_t index = 0;
846 // Create reader on heap to reduce stack usage
847 auto reader = std::make_unique<MultipartReader>("--" + std::string(boundary_start, boundary_len));
848
849 // Configure callbacks
850 reader->set_data_callback([&](const uint8_t *data, size_t len) {
851 if (!reader->has_file() || !len)
852 return;
853
854 if (filename.empty()) {
855 filename = reader->get_current_part().filename;
856 ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
857 handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start
858 }
859
860 handler->handleUpload(&req, filename, index, const_cast<uint8_t *>(data), len, false);
861 index += len;
862 });
863
864 reader->set_part_complete_callback([&]() {
865 if (index > 0) {
866 handler->handleUpload(&req, filename, index, nullptr, 0, true); // End
867 filename.clear();
868 index = 0;
869 }
870 });
871
872 // Process data
873 std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]);
874 size_t bytes_since_yield = 0;
875
876 for (size_t remaining = r->content_len; remaining > 0;) {
877 int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
878
879 if (recv_len <= 0) {
880 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
881 nullptr);
882 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
883 }
884
885 if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
886 ESP_LOGW(TAG, "Multipart parser error");
887 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
888 return ESP_FAIL;
889 }
890
891 remaining -= recv_len;
892 bytes_since_yield += recv_len;
893
894 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
895 vTaskDelay(1);
896 bytes_since_yield = 0;
897 }
898 }
899
900 handler->handleRequest(&req);
901 return ESP_OK;
902}
903#endif // USE_WEBSERVER_OTA
904
905} // namespace web_server_idf
906} // namespace esphome
907
908#endif // !defined(USE_ESP32)
uint8_t h
Definition bl0906.h:2
uint8_t status
Definition bl0942.h:8
void begin(bool include_internal=false)
Builder class for creating JSON documents without lambdas.
Definition json_util.h:62
value_type const & value() const
Definition optional.h:94
This class allows users to create a web server with their ESP nodes.
Definition web_server.h:187
std::string get_config_json()
Return the webserver configuration as JSON.
std::map< uint64_t, SortingGroup > sorting_groups_
Definition web_server.h:487
std::vector< AsyncEventSourceResponse * > sessions_
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
esphome::web_server::WebServer * web_server_
void try_send_nodefer(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
void handleRequest(AsyncWebServerRequest *request) override
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)
std::unique_ptr< esphome::web_server::ListEntitiesIterator > entities_iterator_
bool try_send_nodefer(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
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)
std::function< void(AsyncWebServerRequest *request)> on_not_found_
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 std::string &name)
optional< std::string > get_header(const char *name) const
void send(AsyncWebServerResponse *response)
void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type)
void requestAuthentication(const char *realm=nullptr) const
bool authenticate(const char *username, const char *password) const
std::vector< AsyncWebParameter * > params_
virtual const char * get_content_data() const =0
void addHeader(const char *name, const char *value)
const char * message
Definition component.cpp:38
uint16_t flags
mopeka_std_values val[4]
const char *const TAG
Definition spi.cpp:7
optional< std::string > request_get_url_query(httpd_req_t *req)
Definition utils.cpp:58
optional< std::string > request_get_header(httpd_req_t *req, const char *name)
Definition utils.cpp:41
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len)
std::string(esphome::web_server::WebServer *, void *) message_generator_t
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:16
optional< std::string > query_key_value(const std::string &query_url, const std::string &key)
Definition utils.cpp:76
const char * stristr(const char *haystack, const char *needle)
Definition utils.cpp:106
bool request_has_header(httpd_req_t *req, const char *name)
Definition utils.cpp:39
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:595
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
std::string print()
uint16_t length
Definition tt21100.cpp:0