mirror of
https://github.com/esphome/esphome.git
synced 2025-08-07 02:47:47 +00:00
merg3
This commit is contained in:
parent
f61a40efb8
commit
6596f864be
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user