From fe3c2ba5553f4d07328d1c5b1e927ca0cbea25e1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:15:22 -0500 Subject: [PATCH 1/2] [http_request.ota] Percent-encode credentials in URL (#14257) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http_request/ota/ota_http_request.cpp | 22 +++++++++++++++++++ .../http_request/ota/ota_http_request.h | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index d77a7682118..0db3a50b47d 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -1,5 +1,7 @@ #include "ota_http_request.h" +#include + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -209,6 +211,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() { return ota::OTA_RESPONSE_OK; } +// URL-encode characters that are not unreserved per RFC 3986 section 2.3. +// This is needed for embedding userinfo (username/password) in URLs safely. +static std::string url_encode(const std::string &str) { + std::string result; + result.reserve(str.size()); + for (char c : str) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + result += '%'; + result += format_hex_pretty_char((static_cast(c) >> 4) & 0x0F); + result += format_hex_pretty_char(static_cast(c) & 0x0F); + } + } + return result; +} + +void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); } +void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); } + std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { if (this->username_.empty() || this->password_.empty()) { return url; diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 8735189e993..e3f1a4aa90e 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -29,9 +29,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent, public Parentedmd5_expected_ = md5; } - void set_password(const std::string &password) { this->password_ = password; } + void set_password(const std::string &password); void set_url(const std::string &url); - void set_username(const std::string &username) { this->username_ = username; } + void set_username(const std::string &username); std::string md5_computed() { return this->md5_computed_; } std::string md5_expected() { return this->md5_expected_; } From b3983bc088d43d223c4f14aacdc698238b06310e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 13:19:53 -0600 Subject: [PATCH 2/2] [core] Pass std::function by rvalue reference in set_timer_common_ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change set_timer_common_ to take std::function&& instead of by value. This avoids materializing a std::function copy on the stack at each call site — the caller just passes a pointer to the rvalue. On BK7231N (Thumb-1), each forwarder (set_timeout, set_interval) was 118 bytes due to the inlined std::function move constructor + register spilling. With rvalue reference, they shrink to 32 bytes each. All 12 callers already pass rvalues (std::move or lambda temporaries), so this is a purely mechanical change with no semantic difference. BK7231N: forwarders 118 -> 32 bytes each, ~258 bytes saved total ESP32: forwarders 46 -> 30 bytes each, ~20 bytes saved total --- esphome/core/scheduler.cpp | 2 +- esphome/core/scheduler.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e4e0751e100..fab54eb6487 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -135,7 +135,7 @@ bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_t // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, uint32_t hash_or_id, uint32_t delay, - std::function func, bool is_retry, bool skip_cancel) { + std::function &&func, bool is_retry, bool skip_cancel) { if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if we have a name/id if (!skip_cancel) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ed457b87f6d..1ed897a7e4b 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -255,7 +255,7 @@ class Scheduler { // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, - uint32_t hash_or_id, uint32_t delay, std::function func, bool is_retry = false, + uint32_t hash_or_id, uint32_t delay, std::function &&func, bool is_retry = false, bool skip_cancel = false); // Common implementation for retry - Remove before 2026.8.0