mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add OTA support to ESP-IDF webserver
This commit is contained in:
		| @@ -71,12 +71,6 @@ def validate_local(config): | |||||||
|     return config |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_ota(config): |  | ||||||
|     if CORE.using_esp_idf and config[CONF_OTA]: |  | ||||||
|         raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet") |  | ||||||
|     return config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_sorting_groups(config): | def validate_sorting_groups(config): | ||||||
|     if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: |     if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
| @@ -178,7 +172,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 CONF_OTA, |                 CONF_OTA, | ||||||
|                 esp8266=True, |                 esp8266=True, | ||||||
|                 esp32_arduino=True, |                 esp32_arduino=True, | ||||||
|                 esp32_idf=False, |                 esp32_idf=True, | ||||||
|                 bk72xx=True, |                 bk72xx=True, | ||||||
|                 rtl87xx=True, |                 rtl87xx=True, | ||||||
|             ): cv.boolean, |             ): cv.boolean, | ||||||
| @@ -190,7 +184,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), |     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), | ||||||
|     default_url, |     default_url, | ||||||
|     validate_local, |     validate_local, | ||||||
|     validate_ota, |  | ||||||
|     validate_sorting_groups, |     validate_sorting_groups, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,10 @@ | |||||||
| #endif | #endif | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  | #include "esphome/components/ota/ota_backend.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace web_server_base { | namespace web_server_base { | ||||||
|  |  | ||||||
| @@ -93,6 +97,67 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  |   // ESP-IDF implementation | ||||||
|  |   if (index == 0) { | ||||||
|  |     ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); | ||||||
|  |     this->ota_read_length_ = 0; | ||||||
|  |     this->ota_started_ = false; | ||||||
|  |  | ||||||
|  |     // Create OTA backend | ||||||
|  |     this->ota_backend_ = ota::make_ota_backend(); | ||||||
|  |  | ||||||
|  |     // Begin OTA with unknown size | ||||||
|  |     auto result = this->ota_backend_->begin(0); | ||||||
|  |     if (result != ota::OTA_RESPONSE_OK) { | ||||||
|  |       ESP_LOGE(TAG, "OTA begin failed: %d", result); | ||||||
|  |       this->ota_backend_.reset(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this->ota_started_ = true; | ||||||
|  |   } else if (!this->ota_started_ || !this->ota_backend_) { | ||||||
|  |     // Begin failed or was aborted | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Write data | ||||||
|  |   if (len > 0) { | ||||||
|  |     auto result = this->ota_backend_->write(data, len); | ||||||
|  |     if (result != ota::OTA_RESPONSE_OK) { | ||||||
|  |       ESP_LOGE(TAG, "OTA write failed: %d", result); | ||||||
|  |       this->ota_backend_->abort(); | ||||||
|  |       this->ota_backend_.reset(); | ||||||
|  |       this->ota_started_ = false; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this->ota_read_length_ += len; | ||||||
|  |  | ||||||
|  |     const uint32_t now = millis(); | ||||||
|  |     if (now - this->last_ota_progress_ > 1000) { | ||||||
|  |       if (request->contentLength() != 0) { | ||||||
|  |         float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); | ||||||
|  |         ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); | ||||||
|  |       } else { | ||||||
|  |         ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); | ||||||
|  |       } | ||||||
|  |       this->last_ota_progress_ = now; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (final) { | ||||||
|  |     auto result = this->ota_backend_->end(); | ||||||
|  |     if (result == ota::OTA_RESPONSE_OK) { | ||||||
|  |       ESP_LOGI(TAG, "OTA update successful!"); | ||||||
|  |       this->parent_->set_timeout(100, []() { App.safe_reboot(); }); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "OTA end failed: %d", result); | ||||||
|  |     } | ||||||
|  |     this->ota_backend_.reset(); | ||||||
|  |     this->ota_started_ = false; | ||||||
|  |   } | ||||||
|  | #endif | ||||||
| } | } | ||||||
| void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { | void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { | ||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
| @@ -108,10 +173,20 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { | |||||||
|   response->addHeader("Connection", "close"); |   response->addHeader("Connection", "close"); | ||||||
|   request->send(response); |   request->send(response); | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  |   AsyncWebServerResponse *response; | ||||||
|  |   if (this->ota_started_ && this->ota_backend_) { | ||||||
|  |     response = request->beginResponse(200, "text/plain", "Update Successful!"); | ||||||
|  |   } else { | ||||||
|  |     response = request->beginResponse(200, "text/plain", "Update Failed!"); | ||||||
|  |   } | ||||||
|  |   response->addHeader("Connection", "close"); | ||||||
|  |   request->send(response); | ||||||
|  | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| void WebServerBase::add_ota_handler() { | void WebServerBase::add_ota_handler() { | ||||||
| #ifdef USE_ARDUINO | #if defined(USE_ARDUINO) || defined(USE_ESP_IDF) | ||||||
|   this->add_handler(new OTARequestHandler(this));  // NOLINT |   this->add_handler(new OTARequestHandler(this));  // NOLINT | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
|   | |||||||
| @@ -142,6 +142,10 @@ class OTARequestHandler : public AsyncWebHandler { | |||||||
|   uint32_t last_ota_progress_{0}; |   uint32_t last_ota_progress_{0}; | ||||||
|   uint32_t ota_read_length_{0}; |   uint32_t ota_read_length_{0}; | ||||||
|   WebServerBase *parent_; |   WebServerBase *parent_; | ||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  |   std::unique_ptr<ota::OTABackend> ota_backend_; | ||||||
|  |   bool ota_started_{false}; | ||||||
|  | #endif | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace web_server_base | }  // namespace web_server_base | ||||||
|   | |||||||
							
								
								
									
										226
									
								
								esphome/components/web_server_idf/multipart_parser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								esphome/components/web_server_idf/multipart_parser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | |||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  | #include "multipart_parser.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace web_server_idf { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "multipart_parser"; | ||||||
|  |  | ||||||
|  | bool MultipartParser::parse(const uint8_t *data, size_t len) { | ||||||
|  |   // Append new data to buffer | ||||||
|  |   buffer_.insert(buffer_.end(), data, data + len); | ||||||
|  |  | ||||||
|  |   while (state_ != DONE && state_ != ERROR && !buffer_.empty()) { | ||||||
|  |     switch (state_) { | ||||||
|  |       case BOUNDARY_SEARCH: | ||||||
|  |         if (!find_boundary()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         state_ = HEADERS; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case HEADERS: | ||||||
|  |         if (!parse_headers()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         state_ = CONTENT; | ||||||
|  |         content_start_ = 0;  // Content starts at current buffer position | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case CONTENT: | ||||||
|  |         if (!extract_content()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       default: | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return part_ready_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool MultipartParser::get_current_part(Part &part) const { | ||||||
|  |   if (!part_ready_ || content_length_ == 0) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   part.name = current_name_; | ||||||
|  |   part.filename = current_filename_; | ||||||
|  |   part.content_type = current_content_type_; | ||||||
|  |   part.data = buffer_.data() + content_start_; | ||||||
|  |   part.length = content_length_; | ||||||
|  |  | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MultipartParser::consume_part() { | ||||||
|  |   if (!part_ready_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Remove consumed data from buffer | ||||||
|  |   if (content_start_ + content_length_ < buffer_.size()) { | ||||||
|  |     buffer_.erase(buffer_.begin(), buffer_.begin() + content_start_ + content_length_); | ||||||
|  |   } else { | ||||||
|  |     buffer_.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Reset for next part | ||||||
|  |   part_ready_ = false; | ||||||
|  |   content_start_ = 0; | ||||||
|  |   content_length_ = 0; | ||||||
|  |   current_name_.clear(); | ||||||
|  |   current_filename_.clear(); | ||||||
|  |   current_content_type_.clear(); | ||||||
|  |  | ||||||
|  |   // Look for next boundary | ||||||
|  |   state_ = BOUNDARY_SEARCH; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MultipartParser::reset() { | ||||||
|  |   buffer_.clear(); | ||||||
|  |   state_ = BOUNDARY_SEARCH; | ||||||
|  |   part_ready_ = false; | ||||||
|  |   content_start_ = 0; | ||||||
|  |   content_length_ = 0; | ||||||
|  |   current_name_.clear(); | ||||||
|  |   current_filename_.clear(); | ||||||
|  |   current_content_type_.clear(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool MultipartParser::find_boundary() { | ||||||
|  |   // Look for boundary in buffer | ||||||
|  |   size_t boundary_pos = find_pattern(reinterpret_cast<const uint8_t *>(boundary_.c_str()), boundary_.length()); | ||||||
|  |  | ||||||
|  |   if (boundary_pos == std::string::npos) { | ||||||
|  |     // Keep some data for next iteration to handle split boundaries | ||||||
|  |     if (buffer_.size() > boundary_.length() + 4) { | ||||||
|  |       buffer_.erase(buffer_.begin(), buffer_.end() - boundary_.length() - 4); | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Remove everything up to and including the boundary | ||||||
|  |   buffer_.erase(buffer_.begin(), buffer_.begin() + boundary_pos + boundary_.length()); | ||||||
|  |  | ||||||
|  |   // Skip CRLF after boundary | ||||||
|  |   if (buffer_.size() >= 2 && buffer_[0] == '\r' && buffer_[1] == '\n') { | ||||||
|  |     buffer_.erase(buffer_.begin(), buffer_.begin() + 2); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Check if this is the end boundary | ||||||
|  |   if (buffer_.size() >= 2 && buffer_[0] == '-' && buffer_[1] == '-') { | ||||||
|  |     state_ = DONE; | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool MultipartParser::parse_headers() { | ||||||
|  |   while (true) { | ||||||
|  |     std::string line = read_line(); | ||||||
|  |     if (line.empty()) { | ||||||
|  |       // Check if we have enough data for a line | ||||||
|  |       auto crlf_pos = find_pattern(reinterpret_cast<const uint8_t *>("\r\n"), 2); | ||||||
|  |       if (crlf_pos == std::string::npos) { | ||||||
|  |         return false;  // Need more data | ||||||
|  |       } | ||||||
|  |       // Empty line means headers are done | ||||||
|  |       buffer_.erase(buffer_.begin(), buffer_.begin() + 2); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Parse Content-Disposition header | ||||||
|  |     if (line.find("Content-Disposition:") == 0) { | ||||||
|  |       // Extract name | ||||||
|  |       size_t name_pos = line.find("name=\""); | ||||||
|  |       if (name_pos != std::string::npos) { | ||||||
|  |         name_pos += 6; | ||||||
|  |         size_t name_end = line.find("\"", name_pos); | ||||||
|  |         if (name_end != std::string::npos) { | ||||||
|  |           current_name_ = line.substr(name_pos, name_end - name_pos); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Extract filename if present | ||||||
|  |       size_t filename_pos = line.find("filename=\""); | ||||||
|  |       if (filename_pos != std::string::npos) { | ||||||
|  |         filename_pos += 10; | ||||||
|  |         size_t filename_end = line.find("\"", filename_pos); | ||||||
|  |         if (filename_end != std::string::npos) { | ||||||
|  |           current_filename_ = line.substr(filename_pos, filename_end - filename_pos); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // Parse Content-Type header | ||||||
|  |     else if (line.find("Content-Type:") == 0) { | ||||||
|  |       current_content_type_ = line.substr(14); | ||||||
|  |       // Trim whitespace | ||||||
|  |       size_t start = current_content_type_.find_first_not_of(" \t"); | ||||||
|  |       if (start != std::string::npos) { | ||||||
|  |         current_content_type_ = current_content_type_.substr(start); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool MultipartParser::extract_content() { | ||||||
|  |   // Look for next boundary | ||||||
|  |   std::string search_boundary = "\r\n" + boundary_; | ||||||
|  |   size_t boundary_pos = | ||||||
|  |       find_pattern(reinterpret_cast<const uint8_t *>(search_boundary.c_str()), search_boundary.length()); | ||||||
|  |  | ||||||
|  |   if (boundary_pos != std::string::npos) { | ||||||
|  |     // Found complete part | ||||||
|  |     content_length_ = boundary_pos - content_start_; | ||||||
|  |     part_ready_ = true; | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // No boundary found yet, but we might have partial content | ||||||
|  |   // Keep enough bytes to ensure we don't split a boundary | ||||||
|  |   size_t safe_length = buffer_.size(); | ||||||
|  |   if (safe_length > search_boundary.length() + 4) { | ||||||
|  |     safe_length -= search_boundary.length() + 4; | ||||||
|  |     if (safe_length > content_start_) { | ||||||
|  |       content_length_ = safe_length - content_start_; | ||||||
|  |       // We have partial content but not complete yet | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string MultipartParser::read_line() { | ||||||
|  |   auto crlf_pos = find_pattern(reinterpret_cast<const uint8_t *>("\r\n"), 2); | ||||||
|  |   if (crlf_pos == std::string::npos) { | ||||||
|  |     return ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::string line(buffer_.begin(), buffer_.begin() + crlf_pos); | ||||||
|  |   buffer_.erase(buffer_.begin(), buffer_.begin() + crlf_pos + 2); | ||||||
|  |   return line; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | size_t MultipartParser::find_pattern(const uint8_t *pattern, size_t pattern_len, size_t start) const { | ||||||
|  |   if (buffer_.size() < pattern_len + start) { | ||||||
|  |     return std::string::npos; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   for (size_t i = start; i <= buffer_.size() - pattern_len; ++i) { | ||||||
|  |     if (memcmp(buffer_.data() + i, pattern, pattern_len) == 0) { | ||||||
|  |       return i; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return std::string::npos; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace web_server_idf | ||||||
|  | }  // namespace esphome | ||||||
|  | #endif | ||||||
							
								
								
									
										67
									
								
								esphome/components/web_server_idf/multipart_parser.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								esphome/components/web_server_idf/multipart_parser.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | #pragma once | ||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  |  | ||||||
|  | #include <string> | ||||||
|  | #include <vector> | ||||||
|  | #include <cstring> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace web_server_idf { | ||||||
|  |  | ||||||
|  | // Multipart form data parser for ESP-IDF | ||||||
|  | class MultipartParser { | ||||||
|  |  public: | ||||||
|  |   enum State { BOUNDARY_SEARCH, HEADERS, CONTENT, DONE, ERROR }; | ||||||
|  |  | ||||||
|  |   struct Part { | ||||||
|  |     std::string name; | ||||||
|  |     std::string filename; | ||||||
|  |     std::string content_type; | ||||||
|  |     const uint8_t *data; | ||||||
|  |     size_t length; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   explicit MultipartParser(const std::string &boundary) : boundary_("--" + boundary), state_(BOUNDARY_SEARCH) {} | ||||||
|  |  | ||||||
|  |   // Process incoming data chunk | ||||||
|  |   // Returns true if a complete part is available | ||||||
|  |   bool parse(const uint8_t *data, size_t len); | ||||||
|  |  | ||||||
|  |   // Get the current part if available | ||||||
|  |   bool get_current_part(Part &part) const; | ||||||
|  |  | ||||||
|  |   // Consume the current part and move to next | ||||||
|  |   void consume_part(); | ||||||
|  |  | ||||||
|  |   State get_state() const { return state_; } | ||||||
|  |   bool is_done() const { return state_ == DONE; } | ||||||
|  |   bool has_error() const { return state_ == ERROR; } | ||||||
|  |  | ||||||
|  |   // Reset parser for reuse | ||||||
|  |   void reset(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   bool find_boundary(); | ||||||
|  |   bool parse_headers(); | ||||||
|  |   bool extract_content(); | ||||||
|  |  | ||||||
|  |   std::string read_line(); | ||||||
|  |   size_t find_pattern(const uint8_t *pattern, size_t pattern_len, size_t start = 0) const; | ||||||
|  |  | ||||||
|  |   std::string boundary_; | ||||||
|  |   std::string end_boundary_; | ||||||
|  |   State state_; | ||||||
|  |   std::vector<uint8_t> buffer_; | ||||||
|  |  | ||||||
|  |   // Current part info | ||||||
|  |   std::string current_name_; | ||||||
|  |   std::string current_filename_; | ||||||
|  |   std::string current_content_type_; | ||||||
|  |   size_t content_start_{0}; | ||||||
|  |   size_t content_length_{0}; | ||||||
|  |   bool part_ready_{false}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace web_server_idf | ||||||
|  | }  // namespace esphome | ||||||
|  | #endif | ||||||
| @@ -8,6 +8,7 @@ | |||||||
| #include "esp_tls_crypto.h" | #include "esp_tls_crypto.h" | ||||||
|  |  | ||||||
| #include "utils.h" | #include "utils.h" | ||||||
|  | #include "multipart_parser.h" | ||||||
|  |  | ||||||
| #include "web_server_idf.h" | #include "web_server_idf.h" | ||||||
|  |  | ||||||
| @@ -72,11 +73,25 @@ void AsyncWebServer::begin() { | |||||||
| esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | ||||||
|   ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri); |   ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri); | ||||||
|   auto content_type = request_get_header(r, "Content-Type"); |   auto content_type = request_get_header(r, "Content-Type"); | ||||||
|   if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") { |  | ||||||
|     ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request"); |   // Check if this is a multipart form data request (for OTA updates) | ||||||
|  |   bool is_multipart = false; | ||||||
|  |   std::string boundary; | ||||||
|  |   if (content_type.has_value()) { | ||||||
|  |     std::string ct = content_type.value(); | ||||||
|  |     if (ct.find("multipart/form-data") != std::string::npos) { | ||||||
|  |       is_multipart = true; | ||||||
|  |       // Extract boundary | ||||||
|  |       size_t boundary_pos = ct.find("boundary="); | ||||||
|  |       if (boundary_pos != std::string::npos) { | ||||||
|  |         boundary = ct.substr(boundary_pos + 9); | ||||||
|  |       } | ||||||
|  |     } else if (ct != "application/x-www-form-urlencoded") { | ||||||
|  |       ESP_LOGW(TAG, "Unsupported content type for POST: %s", ct.c_str()); | ||||||
|       // fallback to get handler to support backward compatibility |       // fallback to get handler to support backward compatibility | ||||||
|       return AsyncWebServer::request_handler(r); |       return AsyncWebServer::request_handler(r); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (!request_has_header(r, "Content-Length")) { |   if (!request_has_header(r, "Content-Length")) { | ||||||
|     ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri); |     ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri); | ||||||
| @@ -84,6 +99,76 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | |||||||
|     return ESP_OK; |     return ESP_OK; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Handle multipart form data | ||||||
|  |   if (is_multipart && !boundary.empty()) { | ||||||
|  |     // Create request object | ||||||
|  |     AsyncWebServerRequest req(r); | ||||||
|  |     auto *server = static_cast<AsyncWebServer *>(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; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!found_handler) { | ||||||
|  |       httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr); | ||||||
|  |       return ESP_OK; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Handle multipart upload | ||||||
|  |     MultipartParser parser(boundary); | ||||||
|  |     static constexpr size_t CHUNK_SIZE = 1024; | ||||||
|  |     uint8_t *chunk_buf = new uint8_t[CHUNK_SIZE]; | ||||||
|  |     size_t total_len = r->content_len; | ||||||
|  |     size_t remaining = total_len; | ||||||
|  |     bool first_part = true; | ||||||
|  |  | ||||||
|  |     while (remaining > 0) { | ||||||
|  |       size_t to_read = std::min(remaining, CHUNK_SIZE); | ||||||
|  |       int recv_len = httpd_req_recv(r, reinterpret_cast<char *>(chunk_buf), to_read); | ||||||
|  |  | ||||||
|  |       if (recv_len <= 0) { | ||||||
|  |         delete[] chunk_buf; | ||||||
|  |         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; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Parse multipart data | ||||||
|  |       if (parser.parse(chunk_buf, recv_len)) { | ||||||
|  |         MultipartParser::Part part; | ||||||
|  |         if (parser.get_current_part(part) && !part.filename.empty()) { | ||||||
|  |           // This is a file upload | ||||||
|  |           found_handler->handleUpload(&req, part.filename, first_part ? 0 : 1, const_cast<uint8_t *>(part.data), | ||||||
|  |                                       part.length, false); | ||||||
|  |           first_part = false; | ||||||
|  |           parser.consume_part(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       remaining -= recv_len; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Final call to handler | ||||||
|  |     if (!first_part) { | ||||||
|  |       found_handler->handleUpload(&req, "", 2, nullptr, 0, true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     delete[] chunk_buf; | ||||||
|  |  | ||||||
|  |     // Let handler send response | ||||||
|  |     found_handler->handleRequest(&req); | ||||||
|  |     return ESP_OK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Handle regular form data | ||||||
|   if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { |   if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { | ||||||
|     ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); |     ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); | ||||||
|     httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); |     httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user