From d065f4ae6270641b6375bc4ef8d47a4430ce298b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 20:15:18 -0500 Subject: [PATCH] cleanup --- .../web_server_idf/multipart_parser_utils.cpp | 40 +-------------- .../web_server_idf/multipart_parser_utils.h | 12 ----- .../web_server_idf/parser_utils.cpp | 51 +++++++++++++++++++ .../components/web_server_idf/parser_utils.h | 24 +++++++++ .../web_server_idf/web_server_idf.cpp | 45 +++++++++------- 5 files changed, 103 insertions(+), 69 deletions(-) create mode 100644 esphome/components/web_server_idf/parser_utils.cpp create mode 100644 esphome/components/web_server_idf/parser_utils.h diff --git a/esphome/components/web_server_idf/multipart_parser_utils.cpp b/esphome/components/web_server_idf/multipart_parser_utils.cpp index de1906a0a6..a0869648f8 100644 --- a/esphome/components/web_server_idf/multipart_parser_utils.cpp +++ b/esphome/components/web_server_idf/multipart_parser_utils.cpp @@ -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"); diff --git a/esphome/components/web_server_idf/multipart_parser_utils.h b/esphome/components/web_server_idf/multipart_parser_utils.h index 1829a17b35..26f7d05b96 100644 --- a/esphome/components/web_server_idf/multipart_parser_utils.h +++ b/esphome/components/web_server_idf/multipart_parser_utils.h @@ -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 ¶m); -// 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); diff --git a/esphome/components/web_server_idf/parser_utils.cpp b/esphome/components/web_server_idf/parser_utils.cpp new file mode 100644 index 0000000000..6a9af37e24 --- /dev/null +++ b/esphome/components/web_server_idf/parser_utils.cpp @@ -0,0 +1,51 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP_IDF +#include "parser_utils.h" +#include +#include + +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 diff --git a/esphome/components/web_server_idf/parser_utils.h b/esphome/components/web_server_idf/parser_utils.h new file mode 100644 index 0000000000..52c32849c6 --- /dev/null +++ b/esphome/components/web_server_idf/parser_utils.h @@ -0,0 +1,24 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ESP_IDF + +#include + +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 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index e4e0861292..b7eac8369f 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #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; +#endif if (content_type.has_value()) { - const std::string &ct = content_type.value(); - const char *boundary_start = nullptr; - size_t boundary_len = 0; + const char *content_type_char = content_type.value().c_str(); - if (parse_multipart_boundary(ct.c_str(), &boundary_start, &boundary_len)) { - boundary.assign(boundary_start, boundary_len); + // 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; - 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()); +#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); } } -#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 (!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(r->user_ctx);