diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 6897c9d7d6..7fbc79afe0 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -86,10 +86,11 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri); auto content_type = request_get_header(r, "Content-Type"); -#ifdef USE_WEBSERVER_OTA - // Check if this is a multipart form data request (for OTA updates) - bool is_multipart = false; -#endif + if (!request_has_header(r, "Content-Length")) { + ESP_LOGW(TAG, "Content length is required for post: %s", r->uri); + httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr); + return ESP_OK; + } if (content_type.has_value()) { const char *content_type_char = content_type.value().c_str(); @@ -99,7 +100,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { // Normal form data - proceed with regular handling #ifdef USE_WEBSERVER_OTA } else if (stristr(content_type_char, "multipart/form-data") != nullptr) { - is_multipart = true; + return this->handle_multipart_upload_(r, content_type_char); #endif } else { ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char); @@ -108,165 +109,6 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { } } - if (!request_has_header(r, "Content-Length")) { - ESP_LOGW(TAG, "Content length is required for post: %s", r->uri); - httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr); - return ESP_OK; - } - -#ifdef USE_WEBSERVER_OTA - // Handle multipart form data - if (is_multipart) { - // Parse the boundary from the content type - const char *boundary_start = nullptr; - size_t boundary_len = 0; - - if (!parse_multipart_boundary(content_type.value().c_str(), &boundary_start, &boundary_len)) { - ESP_LOGE(TAG, "Failed to parse multipart boundary"); - httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); - return ESP_FAIL; - } - - std::string boundary(boundary_start, boundary_len); - ESP_LOGV(TAG, "Multipart upload boundary: '%s'", boundary.c_str()); - // Create request object - AsyncWebServerRequest req(r); - auto *server = static_cast(r->user_ctx); - - // Find handler that can handle this request - AsyncWebHandler *found_handler = nullptr; - for (auto *handler : server->handlers_) { - if (handler->canHandle(&req)) { - found_handler = handler; - ESP_LOGD(TAG, "Found handler for OTA request"); - break; - } - } - - if (!found_handler) { - ESP_LOGW(TAG, "No handler found for OTA request"); - httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr); - return ESP_OK; - } - - // Handle multipart upload using the multipart-parser library - // The multipart data starts with "--" + boundary, so we need to prepend it - std::string full_boundary = "--" + boundary; - ESP_LOGVV(TAG, "Initializing multipart reader with full boundary: '%s'", full_boundary.c_str()); - MultipartReader reader(full_boundary); - static constexpr size_t CHUNK_SIZE = 1460; // Match Arduino AsyncWebServer buffer size - // IMPORTANT: chunk_buf is reused for each chunk read from the socket. - // The multipart parser will pass pointers into this buffer to callbacks. - // Those pointers are only valid during the callback execution! - std::unique_ptr chunk_buf(new char[CHUNK_SIZE]); - size_t total_len = r->content_len; - size_t remaining = total_len; - std::string current_filename; - - // Upload state machine - enum class UploadState : uint8_t { - IDLE = 0, - FILE_FOUND, // Found file in multipart data - UPLOAD_STARTED, // Called handleUpload with index=0 - UPLOAD_COMPLETE // Called handleUpload with final=true - }; - UploadState upload_state = UploadState::IDLE; - - // Set up callbacks for the multipart reader - reader.set_data_callback([&](const uint8_t *data, size_t len) { - // CRITICAL: The data pointer is only valid during this callback! - // The multipart parser passes pointers into the chunk_buf buffer, which will be - // overwritten when we read the next chunk. We MUST process the data immediately - // within this callback - any deferred processing will result in use-after-free bugs - // where the data pointer points to corrupted/overwritten memory. - - // By the time on_part_data is called, on_headers_complete has already been called - // so we can check for filename - if (reader.has_file()) { - if (current_filename.empty()) { - // First time we see data for this file - current_filename = reader.get_current_part().filename; - ESP_LOGV(TAG, "Processing file part: '%s'", current_filename.c_str()); - upload_state = UploadState::FILE_FOUND; - } - - if (upload_state == UploadState::FILE_FOUND) { - // Initialize the upload with index=0 - ESP_LOGV(TAG, "Starting upload for: '%s'", current_filename.c_str()); - found_handler->handleUpload(&req, current_filename, 0, nullptr, 0, false); - upload_state = UploadState::UPLOAD_STARTED; - } - - // Process the data chunk immediately - the pointer won't be valid after this callback returns! - // DO NOT store the data pointer for later use or pass it to any async/deferred operations. - if (len > 0) { - found_handler->handleUpload(&req, current_filename, 1, const_cast(data), len, false); - } - } - }); - - reader.set_part_complete_callback([&]() { - if (upload_state == UploadState::UPLOAD_STARTED) { - ESP_LOGV(TAG, "Part complete callback called for: '%s'", current_filename.c_str()); - // Signal end of this part - final=true signals completion - found_handler->handleUpload(&req, current_filename, 2, nullptr, 0, true); - upload_state = UploadState::UPLOAD_COMPLETE; - current_filename.clear(); - } - }); - - while (remaining > 0) { - size_t to_read = std::min(remaining, CHUNK_SIZE); - int recv_len = httpd_req_recv(r, chunk_buf.get(), to_read); - - if (recv_len <= 0) { - if (recv_len == HTTPD_SOCK_ERR_TIMEOUT) { - httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr); - return ESP_ERR_TIMEOUT; - } - httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); - return ESP_FAIL; - } - - size_t parsed = reader.parse(chunk_buf.get(), recv_len); - if (parsed != recv_len) { - ESP_LOGW(TAG, "Multipart parser error at byte %zu (parsed %zu of %d bytes)", total_len - remaining + parsed, - parsed, recv_len); - httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); - return ESP_FAIL; - } - - remaining -= recv_len; - - // Yield periodically to allow the main loop task to run and reset its watchdog - // The httpd thread doesn't need to reset the watchdog, but it needs to yield - // so the loopTask can run and reset its own watchdog - static int bytes_since_yield = 0; - bytes_since_yield += recv_len; - if (bytes_since_yield > 16 * 1024) { // Yield every 16KB - // Use vTaskDelay(1) to yield to other tasks - // This allows the main loop task to run and reset its watchdog - vTaskDelay(1); - bytes_since_yield = 0; - } - } - - // Final cleanup - send final signal if upload was in progress - // This should not be needed as part_complete_callback should handle it - if (upload_state == UploadState::UPLOAD_STARTED) { - ESP_LOGW(TAG, "Upload was not properly closed by part_complete_callback"); - found_handler->handleUpload(&req, current_filename, 2, nullptr, 0, true); - upload_state = UploadState::UPLOAD_COMPLETE; - } - - // Let handler send response - ESP_LOGV(TAG, "Calling handleRequest for OTA response"); - found_handler->handleRequest(&req); - ESP_LOGV(TAG, "handleRequest completed"); - return ESP_OK; - } -#endif // USE_WEBSERVER_OTA - // Handle regular form data if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); @@ -727,6 +569,110 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e } #endif +#ifdef USE_WEBSERVER_OTA +esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) { + // Parse boundary from content type + const char *boundary_start = nullptr; + size_t boundary_len = 0; + if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) { + ESP_LOGE(TAG, "Failed to parse multipart boundary"); + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + // Create request and find handler + AsyncWebServerRequest req(r); + AsyncWebHandler *handler = nullptr; + for (auto *h : this->handlers_) { + if (h->canHandle(&req)) { + handler = h; + break; + } + } + + if (!handler) { + ESP_LOGW(TAG, "No handler found for OTA request"); + httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr); + return ESP_OK; + } + + // Initialize multipart reader + std::string boundary(boundary_start, boundary_len); + MultipartReader reader("--" + boundary); + + // Upload handling state + struct UploadContext { + AsyncWebHandler *handler; + AsyncWebServerRequest *req; + std::string filename; + bool started = false; + } ctx{handler, &req}; + + // Configure callbacks + reader.set_data_callback([&ctx, &reader](const uint8_t *data, size_t len) { + if (!reader.has_file() || len == 0) + return; + + if (ctx.filename.empty()) { + ctx.filename = reader.get_current_part().filename; + ESP_LOGV(TAG, "Processing file: '%s'", ctx.filename.c_str()); + } + + if (!ctx.started) { + ctx.handler->handleUpload(ctx.req, ctx.filename, 0, nullptr, 0, false); + ctx.started = true; + } + + ctx.handler->handleUpload(ctx.req, ctx.filename, 1, const_cast(data), len, false); + }); + + reader.set_part_complete_callback([&ctx]() { + if (ctx.started) { + ctx.handler->handleUpload(ctx.req, ctx.filename, 2, nullptr, 0, true); + ctx.filename.clear(); + ctx.started = false; + } + }); + + // Process chunks + static constexpr size_t CHUNK_SIZE = 1460; + std::unique_ptr buffer(new char[CHUNK_SIZE]); + size_t remaining = r->content_len; + size_t bytes_since_yield = 0; + + while (remaining > 0) { + size_t to_read = std::min(remaining, CHUNK_SIZE); + int recv_len = httpd_req_recv(r, buffer.get(), to_read); + + if (recv_len <= 0) { + httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, + nullptr); + return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; + } + + size_t parsed = reader.parse(buffer.get(), recv_len); + if (parsed != recv_len) { + ESP_LOGW(TAG, "Multipart parser error"); + httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); + return ESP_FAIL; + } + + remaining -= recv_len; + bytes_since_yield += recv_len; + + // Yield periodically to let main loop run + if (bytes_since_yield > 16 * 1024) { + vTaskDelay(1); + bytes_since_yield = 0; + } + } + + // Let handler send response + handler->handleRequest(&req); + return ESP_OK; +} +#endif // USE_WEBSERVER_OTA + } // namespace web_server_idf } // namespace esphome diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 7547117224..8de25c8e96 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -204,6 +204,9 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; +#ifdef USE_WEBSERVER_OTA + esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); +#endif std::vector handlers_; std::function on_not_found_{}; };