mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	merg3
This commit is contained in:
		| @@ -1,5 +1,7 @@ | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_OTA | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@dentra"] | ||||
|  | ||||
| @@ -12,3 +14,9 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server | ||||
|     add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) | ||||
|  | ||||
|     # Check if web_server component has OTA enabled | ||||
|     web_server_config = CORE.config.get("web_server", {}) | ||||
|     if web_server_config.get(CONF_OTA, True):  # OTA is enabled by default | ||||
|         # Add multipart parser component for OTA support | ||||
|         add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") | ||||
|   | ||||
| @@ -1,253 +0,0 @@ | ||||
| #ifdef USE_ESP_IDF | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
| #include "multipart_parser.h" | ||||
| #include "multipart_parser_utils.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace web_server_idf { | ||||
|  | ||||
| static const char *const TAG = "multipart_parser"; | ||||
|  | ||||
| // Constants for multipart parsing | ||||
| static constexpr size_t CRLF_LENGTH = 2; | ||||
| static constexpr size_t MIN_BOUNDARY_BUFFER = 4;  // Extra bytes to keep for split boundary detection | ||||
| static constexpr const char *CRLF_STR = "\r\n"; | ||||
|  | ||||
| bool MultipartParser::parse(const uint8_t *data, size_t len) { | ||||
|   // Append new data to buffer | ||||
|   if (data && len > 0) { | ||||
|     buffer_.insert(buffer_.end(), data, data + len); | ||||
|   } | ||||
|  | ||||
|   // Limit iterations to prevent infinite loops | ||||
|   static constexpr size_t MAX_ITERATIONS = 10; | ||||
|   size_t iterations = 0; | ||||
|  | ||||
|   bool made_progress = true; | ||||
|   while (made_progress && state_ != DONE && state_ != ERROR && !buffer_.empty() && iterations < MAX_ITERATIONS) { | ||||
|     made_progress = false; | ||||
|     iterations++; | ||||
|  | ||||
|     switch (state_) { | ||||
|       case BOUNDARY_SEARCH: | ||||
|         if (find_boundary()) { | ||||
|           state_ = HEADERS; | ||||
|           made_progress = true; | ||||
|         } | ||||
|         break; | ||||
|  | ||||
|       case HEADERS: | ||||
|         if (parse_headers()) { | ||||
|           state_ = CONTENT; | ||||
|           made_progress = true; | ||||
|         } | ||||
|         break; | ||||
|  | ||||
|       case CONTENT: | ||||
|         if (extract_content()) { | ||||
|           // Content is ready, return to caller | ||||
|           return true; | ||||
|         } | ||||
|         // If we're waiting for more data in CONTENT state, exit the loop | ||||
|         return false; | ||||
|  | ||||
|       default: | ||||
|         ESP_LOGE(TAG, "Invalid parser state: %d", state_); | ||||
|         state_ = ERROR; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (iterations >= MAX_ITERATIONS) { | ||||
|     ESP_LOGW(TAG, "Parser reached maximum iterations, possible malformed data"); | ||||
|   } | ||||
|  | ||||
|   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(); | ||||
|   part.length = content_length_; | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void MultipartParser::consume_part() { | ||||
|   if (!part_ready_) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Remove consumed data from buffer | ||||
|   if (content_length_ < buffer_.size()) { | ||||
|     buffer_.erase(buffer_.begin(), buffer_.begin() + content_length_); | ||||
|   } else { | ||||
|     buffer_.clear(); | ||||
|   } | ||||
|  | ||||
|   // Reset for next part | ||||
|   part_ready_ = false; | ||||
|   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_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() + MIN_BOUNDARY_BUFFER) { | ||||
|       buffer_.erase(buffer_.begin(), buffer_.end() - boundary_.length() - MIN_BOUNDARY_BUFFER); | ||||
|     } | ||||
|     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() >= CRLF_LENGTH && buffer_[0] == '\r' && buffer_[1] == '\n') { | ||||
|     buffer_.erase(buffer_.begin(), buffer_.begin() + CRLF_LENGTH); | ||||
|   } | ||||
|  | ||||
|   // Check if this is the end boundary | ||||
|   if (buffer_.size() >= CRLF_LENGTH && buffer_[0] == '-' && buffer_[1] == '-') { | ||||
|     state_ = DONE; | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool MultipartParser::parse_headers() { | ||||
|   // Limit header lines to prevent DOS attacks | ||||
|   static constexpr size_t MAX_HEADER_LINES = 50; | ||||
|   size_t header_count = 0; | ||||
|  | ||||
|   while (header_count < MAX_HEADER_LINES) { | ||||
|     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 *>(CRLF_STR), CRLF_LENGTH); | ||||
|       if (crlf_pos == std::string::npos) { | ||||
|         return false;  // Need more data | ||||
|       } | ||||
|       // Empty line means headers are done | ||||
|       buffer_.erase(buffer_.begin(), buffer_.begin() + CRLF_LENGTH); | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     process_header_line(line); | ||||
|     header_count++; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGW(TAG, "Too many headers in multipart data"); | ||||
|   state_ = ERROR; | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void MultipartParser::process_header_line(const std::string &line) { | ||||
|   if (str_startswith_case_insensitive(line, "content-disposition:")) { | ||||
|     // Extract name and filename parameters | ||||
|     current_name_ = extract_header_param(line, "name"); | ||||
|     current_filename_ = extract_header_param(line, "filename"); | ||||
|   } else if (str_startswith_case_insensitive(line, "content-type:")) { | ||||
|     current_content_type_ = extract_header_value(line); | ||||
|   } | ||||
|   // RFC 7578: Ignore any other Content-* headers | ||||
| } | ||||
|  | ||||
| bool MultipartParser::extract_content() { | ||||
|   // Look for next boundary | ||||
|   std::string search_boundary = CRLF_STR + 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; | ||||
|     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() + MIN_BOUNDARY_BUFFER) { | ||||
|     safe_length -= search_boundary.length() + MIN_BOUNDARY_BUFFER; | ||||
|     if (safe_length > 0) { | ||||
|       content_length_ = safe_length; | ||||
|       // We have partial content but not complete yet | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| std::string MultipartParser::read_line() { | ||||
|   // Limit line length to prevent excessive memory usage | ||||
|   static constexpr size_t MAX_LINE_LENGTH = 4096; | ||||
|  | ||||
|   auto crlf_pos = find_pattern(reinterpret_cast<const uint8_t *>(CRLF_STR), CRLF_LENGTH); | ||||
|   if (crlf_pos == std::string::npos) { | ||||
|     // If we have too much data without CRLF, it's likely malformed | ||||
|     if (buffer_.size() > MAX_LINE_LENGTH) { | ||||
|       ESP_LOGW(TAG, "Header line too long, truncating"); | ||||
|       state_ = ERROR; | ||||
|     } | ||||
|     return ""; | ||||
|   } | ||||
|  | ||||
|   if (crlf_pos > MAX_LINE_LENGTH) { | ||||
|     ESP_LOGW(TAG, "Header line exceeds maximum length"); | ||||
|     state_ = ERROR; | ||||
|     return ""; | ||||
|   } | ||||
|  | ||||
|   std::string line(buffer_.begin(), buffer_.begin() + crlf_pos); | ||||
|   buffer_.erase(buffer_.begin(), buffer_.begin() + crlf_pos + CRLF_LENGTH); | ||||
|   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  // USE_WEBSERVER_OTA | ||||
| #endif  // USE_ESP_IDF | ||||
| @@ -1,75 +0,0 @@ | ||||
| #pragma once | ||||
| #ifdef USE_ESP_IDF | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
|  | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include <cstring> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace web_server_idf { | ||||
|  | ||||
| // Multipart form data parser for ESP-IDF | ||||
| // Implements RFC 7578 compliant multipart/form-data parsing | ||||
| class MultipartParser { | ||||
|  public: | ||||
|   static constexpr const char *MULTIPART_BOUNDARY_PREFIX = "--"; | ||||
|  | ||||
|   enum State : uint8_t { 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_(MULTIPART_BOUNDARY_PREFIX + boundary), | ||||
|         state_(BOUNDARY_SEARCH), | ||||
|         content_length_(0), | ||||
|         part_ready_(false) {} | ||||
|  | ||||
|   // 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(); | ||||
|   void process_header_line(const std::string &line); | ||||
|   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_; | ||||
|   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_length_{0}; | ||||
|   bool part_ready_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace web_server_idf | ||||
| }  // namespace esphome | ||||
| #endif  // USE_WEBSERVER_OTA | ||||
| #endif  // USE_ESP_IDF | ||||
							
								
								
									
										193
									
								
								esphome/components/web_server_idf/multipart_reader.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								esphome/components/web_server_idf/multipart_reader.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| #ifdef USE_ESP_IDF | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
| #include "multipart_reader.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <cstring> | ||||
| #include <cctype> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace web_server_idf { | ||||
|  | ||||
| static const char *const TAG = "multipart_reader"; | ||||
|  | ||||
| MultipartReader::MultipartReader(const std::string &boundary) { | ||||
|   // Initialize settings with callbacks | ||||
|   memset(&settings_, 0, sizeof(settings_)); | ||||
|   settings_.on_header_field = on_header_field; | ||||
|   settings_.on_header_value = on_header_value; | ||||
|   settings_.on_part_data_begin = on_part_data_begin; | ||||
|   settings_.on_part_data = on_part_data; | ||||
|   settings_.on_part_data_end = on_part_data_end; | ||||
|   settings_.on_headers_complete = on_headers_complete; | ||||
|  | ||||
|   // Create parser with boundary | ||||
|   parser_ = multipart_parser_init(boundary.c_str(), &settings_); | ||||
|   if (parser_) { | ||||
|     multipart_parser_set_data(parser_, this); | ||||
|   } | ||||
| } | ||||
|  | ||||
| MultipartReader::~MultipartReader() { | ||||
|   if (parser_) { | ||||
|     multipart_parser_free(parser_); | ||||
|   } | ||||
| } | ||||
|  | ||||
| size_t MultipartReader::parse(const char *data, size_t len) { | ||||
|   if (!parser_) { | ||||
|     return 0; | ||||
|   } | ||||
|   return multipart_parser_execute(parser_, data, len); | ||||
| } | ||||
|  | ||||
| int MultipartReader::on_header_field(multipart_parser *parser, const char *at, size_t length) { | ||||
|   MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser)); | ||||
|  | ||||
|   // If we were processing a value, save it | ||||
|   if (!reader->current_header_value_.empty()) { | ||||
|     // Process the previous header | ||||
|     std::string field_lower = reader->current_header_field_; | ||||
|     std::transform(field_lower.begin(), field_lower.end(), field_lower.begin(), ::tolower); | ||||
|  | ||||
|     if (field_lower == "content-disposition") { | ||||
|       // Parse name and filename from Content-Disposition | ||||
|       size_t name_pos = reader->current_header_value_.find("name="); | ||||
|       if (name_pos != std::string::npos) { | ||||
|         name_pos += 5; | ||||
|         size_t end_pos; | ||||
|         if (reader->current_header_value_[name_pos] == '"') { | ||||
|           name_pos++; | ||||
|           end_pos = reader->current_header_value_.find('"', name_pos); | ||||
|         } else { | ||||
|           end_pos = reader->current_header_value_.find_first_of("; \r\n", name_pos); | ||||
|         } | ||||
|         if (end_pos != std::string::npos) { | ||||
|           reader->current_part_.name = reader->current_header_value_.substr(name_pos, end_pos - name_pos); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       size_t filename_pos = reader->current_header_value_.find("filename="); | ||||
|       if (filename_pos != std::string::npos) { | ||||
|         filename_pos += 9; | ||||
|         size_t end_pos; | ||||
|         if (reader->current_header_value_[filename_pos] == '"') { | ||||
|           filename_pos++; | ||||
|           end_pos = reader->current_header_value_.find('"', filename_pos); | ||||
|         } else { | ||||
|           end_pos = reader->current_header_value_.find_first_of("; \r\n", filename_pos); | ||||
|         } | ||||
|         if (end_pos != std::string::npos) { | ||||
|           reader->current_part_.filename = reader->current_header_value_.substr(filename_pos, end_pos - filename_pos); | ||||
|         } | ||||
|       } | ||||
|     } else if (field_lower == "content-type") { | ||||
|       reader->current_part_.content_type = reader->current_header_value_; | ||||
|     } | ||||
|  | ||||
|     reader->current_header_value_.clear(); | ||||
|   } | ||||
|  | ||||
|   // Start new header field | ||||
|   reader->current_header_field_.assign(at, length); | ||||
|   reader->in_headers_ = true; | ||||
|  | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| int MultipartReader::on_header_value(multipart_parser *parser, const char *at, size_t length) { | ||||
|   MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser)); | ||||
|   reader->current_header_value_.append(at, length); | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| int MultipartReader::on_headers_complete(multipart_parser *parser) { | ||||
|   MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser)); | ||||
|  | ||||
|   // Process last header if any | ||||
|   if (!reader->current_header_value_.empty()) { | ||||
|     std::string field_lower = reader->current_header_field_; | ||||
|     std::transform(field_lower.begin(), field_lower.end(), field_lower.begin(), ::tolower); | ||||
|  | ||||
|     if (field_lower == "content-disposition") { | ||||
|       // Parse name and filename from Content-Disposition | ||||
|       size_t name_pos = reader->current_header_value_.find("name="); | ||||
|       if (name_pos != std::string::npos) { | ||||
|         name_pos += 5; | ||||
|         size_t end_pos; | ||||
|         if (reader->current_header_value_[name_pos] == '"') { | ||||
|           name_pos++; | ||||
|           end_pos = reader->current_header_value_.find('"', name_pos); | ||||
|         } else { | ||||
|           end_pos = reader->current_header_value_.find_first_of("; \r\n", name_pos); | ||||
|         } | ||||
|         if (end_pos != std::string::npos) { | ||||
|           reader->current_part_.name = reader->current_header_value_.substr(name_pos, end_pos - name_pos); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       size_t filename_pos = reader->current_header_value_.find("filename="); | ||||
|       if (filename_pos != std::string::npos) { | ||||
|         filename_pos += 9; | ||||
|         size_t end_pos; | ||||
|         if (reader->current_header_value_[filename_pos] == '"') { | ||||
|           filename_pos++; | ||||
|           end_pos = reader->current_header_value_.find('"', filename_pos); | ||||
|         } else { | ||||
|           end_pos = reader->current_header_value_.find_first_of("; \r\n", filename_pos); | ||||
|         } | ||||
|         if (end_pos != std::string::npos) { | ||||
|           reader->current_part_.filename = reader->current_header_value_.substr(filename_pos, end_pos - filename_pos); | ||||
|         } | ||||
|       } | ||||
|     } else if (field_lower == "content-type") { | ||||
|       reader->current_part_.content_type = reader->current_header_value_; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   reader->in_headers_ = false; | ||||
|   reader->current_header_field_.clear(); | ||||
|   reader->current_header_value_.clear(); | ||||
|  | ||||
|   ESP_LOGD(TAG, "Part headers complete: name='%s', filename='%s', content_type='%s'", | ||||
|            reader->current_part_.name.c_str(), reader->current_part_.filename.c_str(), | ||||
|            reader->current_part_.content_type.c_str()); | ||||
|  | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| int MultipartReader::on_part_data_begin(multipart_parser *parser) { | ||||
|   MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser)); | ||||
|   ESP_LOGD(TAG, "Part data begin"); | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| int MultipartReader::on_part_data(multipart_parser *parser, const char *at, size_t length) { | ||||
|   MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser)); | ||||
|  | ||||
|   // Only process file uploads | ||||
|   if (reader->has_file() && reader->data_callback_) { | ||||
|     reader->data_callback_(reinterpret_cast<const uint8_t *>(at), length); | ||||
|   } | ||||
|  | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| int MultipartReader::on_part_data_end(multipart_parser *parser) { | ||||
|   MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser)); | ||||
|  | ||||
|   ESP_LOGD(TAG, "Part data end"); | ||||
|  | ||||
|   if (reader->part_complete_callback_) { | ||||
|     reader->part_complete_callback_(); | ||||
|   } | ||||
|  | ||||
|   // Clear part info for next part | ||||
|   reader->current_part_ = Part{}; | ||||
|  | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| }  // namespace web_server_idf | ||||
| }  // namespace esphome | ||||
| #endif  // USE_WEBSERVER_OTA | ||||
| #endif  // USE_ESP_IDF | ||||
							
								
								
									
										65
									
								
								esphome/components/web_server_idf/multipart_reader.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								esphome/components/web_server_idf/multipart_reader.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| #pragma once | ||||
| #ifdef USE_ESP_IDF | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
|  | ||||
| #include <esp_http_server.h> | ||||
| #include <multipart_parser.h> | ||||
| #include <string> | ||||
| #include <functional> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace web_server_idf { | ||||
|  | ||||
| // Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads | ||||
| class MultipartReader { | ||||
|  public: | ||||
|   struct Part { | ||||
|     std::string name; | ||||
|     std::string filename; | ||||
|     std::string content_type; | ||||
|   }; | ||||
|  | ||||
|   using DataCallback = std::function<void(const uint8_t *data, size_t len)>; | ||||
|   using PartCompleteCallback = std::function<void()>; | ||||
|  | ||||
|   explicit MultipartReader(const std::string &boundary); | ||||
|   ~MultipartReader(); | ||||
|  | ||||
|   // Set callbacks for handling data | ||||
|   void set_data_callback(DataCallback callback) { data_callback_ = callback; } | ||||
|   void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = callback; } | ||||
|  | ||||
|   // Parse incoming data | ||||
|   size_t parse(const char *data, size_t len); | ||||
|  | ||||
|   // Get current part info | ||||
|   const Part &get_current_part() const { return current_part_; } | ||||
|  | ||||
|   // Check if we found a file upload | ||||
|   bool has_file() const { return !current_part_.filename.empty(); } | ||||
|  | ||||
|  private: | ||||
|   static int on_header_field(multipart_parser *parser, const char *at, size_t length); | ||||
|   static int on_header_value(multipart_parser *parser, const char *at, size_t length); | ||||
|   static int on_part_data_begin(multipart_parser *parser); | ||||
|   static int on_part_data(multipart_parser *parser, const char *at, size_t length); | ||||
|   static int on_part_data_end(multipart_parser *parser); | ||||
|   static int on_headers_complete(multipart_parser *parser); | ||||
|  | ||||
|   multipart_parser *parser_{nullptr}; | ||||
|   multipart_parser_settings settings_{}; | ||||
|  | ||||
|   Part current_part_; | ||||
|   std::string current_header_field_; | ||||
|   std::string current_header_value_; | ||||
|  | ||||
|   DataCallback data_callback_; | ||||
|   PartCompleteCallback part_complete_callback_; | ||||
|  | ||||
|   bool in_headers_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace web_server_idf | ||||
| }  // namespace esphome | ||||
| #endif  // USE_WEBSERVER_OTA | ||||
| #endif  // USE_ESP_IDF | ||||
| @@ -9,8 +9,7 @@ | ||||
|  | ||||
| #include "utils.h" | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
| #include "multipart_parser.h" | ||||
| #include "multipart_parser_utils.h" | ||||
| #include "multipart_reader.h" | ||||
| #endif | ||||
|  | ||||
| #include "web_server_idf.h" | ||||
| @@ -79,16 +78,30 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
|   // Check if this is a multipart form data request (for OTA updates) | ||||
|   const char *boundary_start = nullptr; | ||||
|   size_t boundary_len = 0; | ||||
|   bool is_multipart = false; | ||||
|   std::string boundary; | ||||
|  | ||||
|   if (content_type.has_value()) { | ||||
|     const char *ct = content_type.value().c_str(); | ||||
|     is_multipart = parse_multipart_boundary(ct, &boundary_start, &boundary_len); | ||||
|     const std::string &ct = content_type.value(); | ||||
|     size_t boundary_pos = ct.find("boundary="); | ||||
|     if (boundary_pos != std::string::npos) { | ||||
|       boundary_pos += 9;  // Skip "boundary=" | ||||
|       size_t boundary_end = ct.find_first_of(" ;\r\n", boundary_pos); | ||||
|       if (boundary_end == std::string::npos) { | ||||
|         boundary_end = ct.length(); | ||||
|       } | ||||
|       if (ct[boundary_pos] == '"' && boundary_end > boundary_pos + 1 && ct[boundary_end - 1] == '"') { | ||||
|         // Quoted boundary | ||||
|         boundary = ct.substr(boundary_pos + 1, boundary_end - boundary_pos - 2); | ||||
|       } else { | ||||
|         // Unquoted boundary | ||||
|         boundary = ct.substr(boundary_pos, boundary_end - boundary_pos); | ||||
|       } | ||||
|       is_multipart = ct.find("multipart/form-data") != std::string::npos && !boundary.empty(); | ||||
|     } | ||||
|  | ||||
|     if (!is_multipart && !is_form_urlencoded(ct)) { | ||||
|       ESP_LOGW(TAG, "Unsupported content type for POST: %s", ct); | ||||
|     if (!is_multipart && ct.find("application/x-www-form-urlencoded") == std::string::npos) { | ||||
|       ESP_LOGW(TAG, "Unsupported content type for POST: %s", ct.c_str()); | ||||
|       // fallback to get handler to support backward compatibility | ||||
|       return AsyncWebServer::request_handler(r); | ||||
|     } | ||||
| @@ -109,7 +122,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_OTA | ||||
|   // Handle multipart form data | ||||
|   if (is_multipart && boundary_start && boundary_len > 0) { | ||||
|   if (is_multipart && !boundary.empty()) { | ||||
|     // Create request object | ||||
|     AsyncWebServerRequest req(r); | ||||
|     auto *server = static_cast<AsyncWebServer *>(r->user_ctx); | ||||
| @@ -128,18 +141,36 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | ||||
|       return ESP_OK; | ||||
|     } | ||||
|  | ||||
|     // Handle multipart upload - create boundary string only when needed | ||||
|     std::string boundary(boundary_start, boundary_len); | ||||
|     MultipartParser parser(boundary); | ||||
|     // Handle multipart upload using the multipart-parser library | ||||
|     MultipartReader reader(boundary); | ||||
|     static constexpr size_t CHUNK_SIZE = 1024; | ||||
|     uint8_t *chunk_buf = new uint8_t[CHUNK_SIZE]; | ||||
|     char *chunk_buf = new char[CHUNK_SIZE]; | ||||
|     size_t total_len = r->content_len; | ||||
|     size_t remaining = total_len; | ||||
|     bool first_part = true; | ||||
|     std::string current_filename; | ||||
|     bool upload_started = false; | ||||
|  | ||||
|     // Set up callbacks for the multipart reader | ||||
|     reader.set_data_callback([&](const uint8_t *data, size_t len) { | ||||
|       if (!current_filename.empty()) { | ||||
|         found_handler->handleUpload(&req, current_filename, upload_started ? 1 : 0, const_cast<uint8_t *>(data), len, | ||||
|                                     false); | ||||
|         upload_started = true; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     reader.set_part_complete_callback([&]() { | ||||
|       if (!current_filename.empty() && upload_started) { | ||||
|         // Signal end of this part | ||||
|         found_handler->handleUpload(&req, current_filename, 2, nullptr, 0, false); | ||||
|         current_filename.clear(); | ||||
|         upload_started = false; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     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); | ||||
|       int recv_len = httpd_req_recv(r, chunk_buf, to_read); | ||||
|  | ||||
|       if (recv_len <= 0) { | ||||
|         delete[] chunk_buf; | ||||
| @@ -152,23 +183,25 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { | ||||
|       } | ||||
|  | ||||
|       // 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(); | ||||
|         } | ||||
|       size_t parsed = reader.parse(chunk_buf, recv_len); | ||||
|       if (parsed != recv_len) { | ||||
|         ESP_LOGW(TAG, "Multipart parser error at byte %zu", total_len - remaining + parsed); | ||||
|         delete[] chunk_buf; | ||||
|         httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); | ||||
|         return ESP_FAIL; | ||||
|       } | ||||
|  | ||||
|       // Check if we found a new file part | ||||
|       if (reader.has_file() && current_filename.empty()) { | ||||
|         current_filename = reader.get_current_part().filename; | ||||
|       } | ||||
|  | ||||
|       remaining -= recv_len; | ||||
|     } | ||||
|  | ||||
|     // Final call to handler | ||||
|     if (!first_part) { | ||||
|       found_handler->handleUpload(&req, "", 2, nullptr, 0, true); | ||||
|     // Final cleanup - send final signal if upload was in progress | ||||
|     if (!current_filename.empty() && upload_started) { | ||||
|       found_handler->handleUpload(&req, current_filename, 2, nullptr, 0, true); | ||||
|     } | ||||
|  | ||||
|     delete[] chunk_buf; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user