This commit is contained in:
J. Nick Koston 2025-06-29 20:15:18 -05:00
parent ed2c3e626b
commit d065f4ae62
No known key found for this signature in database
5 changed files with 103 additions and 69 deletions

View File

@ -1,21 +1,12 @@
#include "esphome/core/defines.h"
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
#include "multipart_parser_utils.h"
#include "parser_utils.h"
#include "esphome/core/log.h"
namespace esphome {
namespace web_server_idf {
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
for (size_t i = 0; i < n; i++) {
if (!char_equals_ci(s1[i], s2[i])) {
return false;
}
}
return true;
}
// Case-insensitive string prefix check
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) {
if (str.length() < prefix.length()) {
@ -108,26 +99,6 @@ std::string extract_header_param(const std::string &header, const std::string &p
return "";
}
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle) {
if (!haystack || !needle) {
return nullptr;
}
size_t needle_len = strlen(needle);
if (needle_len == 0) {
return haystack;
}
for (const char *p = haystack; *p; p++) {
if (str_ncmp_ci(p, needle, needle_len)) {
return p;
}
}
return nullptr;
}
// Parse boundary from Content-Type header
// Returns true if boundary found, false otherwise
// boundary_start and boundary_len will point to the boundary value
@ -188,15 +159,6 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
return true;
}
// Check if content type is form-urlencoded (case-insensitive)
bool is_form_urlencoded(const char *content_type) {
if (!content_type) {
return false;
}
return stristr(content_type, "application/x-www-form-urlencoded") != nullptr;
}
// Trim whitespace from both ends of a string
std::string str_trim(const std::string &str) {
size_t start = str.find_first_not_of(" \t\r\n");

View File

@ -9,12 +9,6 @@
namespace esphome {
namespace web_server_idf {
// Helper function for case-insensitive character comparison
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
// Case-insensitive string prefix check
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix);
@ -25,17 +19,11 @@ size_t str_find_case_insensitive(const std::string &haystack, const std::string
// Handles both quoted and unquoted values
std::string extract_header_param(const std::string &header, const std::string &param);
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle);
// Parse boundary from Content-Type header
// Returns true if boundary found, false otherwise
// boundary_start and boundary_len will point to the boundary value
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len);
// Check if content type is form-urlencoded (case-insensitive)
bool is_form_urlencoded(const char *content_type);
// Trim whitespace from both ends of a string
std::string str_trim(const std::string &str);

View File

@ -0,0 +1,51 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP_IDF
#include "parser_utils.h"
#include <cstring>
#include <cctype>
namespace esphome {
namespace web_server_idf {
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
for (size_t i = 0; i < n; i++) {
if (!char_equals_ci(s1[i], s2[i])) {
return false;
}
}
return true;
}
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle) {
if (!haystack || !needle) {
return nullptr;
}
size_t needle_len = strlen(needle);
if (needle_len == 0) {
return haystack;
}
for (const char *p = haystack; *p; p++) {
if (str_ncmp_ci(p, needle, needle_len)) {
return p;
}
}
return nullptr;
}
// Check if content type is form-urlencoded (case-insensitive)
bool is_form_urlencoded(const char *content_type) {
if (!content_type) {
return false;
}
return stristr(content_type, "application/x-www-form-urlencoded") != nullptr;
}
} // namespace web_server_idf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP_IDF
#include <string>
namespace esphome {
namespace web_server_idf {
// Helper function for case-insensitive character comparison
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle);
// Check if content type is form-urlencoded (case-insensitive)
bool is_form_urlencoded(const char *content_type);
} // namespace web_server_idf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@ -2,6 +2,8 @@
#include <cstdarg>
#include <memory>
#include <cstring>
#include <cctype>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@ -12,6 +14,7 @@
#include "utils.h"
#include "web_server_idf.h"
#include "parser_utils.h"
#ifdef USE_WEBSERVER_OTA
#include "multipart_reader.h"
@ -88,40 +91,46 @@ 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)
bool is_multipart = false;
std::string boundary;
if (content_type.has_value()) {
const std::string &ct = content_type.value();
const char *boundary_start = nullptr;
size_t boundary_len = 0;
if (parse_multipart_boundary(ct.c_str(), &boundary_start, &boundary_len)) {
boundary.assign(boundary_start, boundary_len);
is_multipart = true;
ESP_LOGV(TAG, "Multipart upload detected, boundary: '%s' (len: %zu)", boundary.c_str(), boundary_len);
} else if (!is_form_urlencoded(ct.c_str())) {
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);
}
}
#else
if (content_type.has_value() && content_type.value() != "application/x-www-form-urlencoded") {
ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request");
// fallback to get handler to support backward compatibility
return AsyncWebServer::request_handler(r);
}
#endif
if (content_type.has_value()) {
const char *content_type_char = content_type.value().c_str();
// Check most common case first
if (is_form_urlencoded(content_type_char)) {
// Normal form data - proceed with regular handling
#ifdef USE_WEBSERVER_OTA
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
is_multipart = true;
#endif
} else {
ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char);
// fallback to get handler to support backward compatibility
return AsyncWebServer::request_handler(r);
}
}
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 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 && !boundary.empty()) {
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<AsyncWebServer *>(r->user_ctx);