ESPHome 2026.3.0
Loading...
Searching...
No Matches
ota_http_request.cpp
Go to the documentation of this file.
1#include "ota_http_request.h"
2
3#include <cctype>
4
7#include "esphome/core/log.h"
8
11
12namespace esphome {
13namespace http_request {
14
15static const char *const TAG = "http_request.ota";
16
17void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
18
19void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
20 if (!this->validate_url_(url)) {
21 this->md5_url_.clear(); // URL was not valid; prevent flashing until it is
22 return;
23 }
24 this->md5_url_ = url;
25 this->md5_expected_.clear(); // to be retrieved later
26}
27
28void OtaHttpRequestComponent::set_url(const std::string &url) {
29 if (!this->validate_url_(url)) {
30 this->url_.clear(); // URL was not valid; prevent flashing until it is
31 return;
32 }
33 this->url_ = url;
34}
35
37 if (this->url_.empty()) {
38 ESP_LOGE(TAG, "URL not set; cannot start update");
39 return;
40 }
41
42 ESP_LOGI(TAG, "Starting update");
43#ifdef USE_OTA_STATE_LISTENER
44 this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
45#endif
46
47 auto ota_status = this->do_ota_();
48
49 switch (ota_status) {
51#ifdef USE_OTA_STATE_LISTENER
52 this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status);
53#endif
54 delay(10);
56 break;
57
58 default:
59#ifdef USE_OTA_STATE_LISTENER
60 this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
61#endif
62 this->md5_computed_.clear(); // will be reset at next attempt
63 this->md5_expected_.clear(); // will be reset at next attempt
64 break;
65 }
66}
67
68void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
69 if (this->update_started_) {
70 ESP_LOGV(TAG, "Aborting OTA backend");
71 backend->abort();
72 }
73 ESP_LOGV(TAG, "Aborting HTTP connection");
74 container->end();
75};
76
79 uint32_t last_progress = 0;
80 uint32_t update_start_time = millis();
81 md5::MD5Digest md5_receive;
82 char md5_receive_str[33];
83
84 if (this->md5_expected_.empty() && !this->http_get_md5_()) {
85 return OTA_MD5_INVALID;
86 }
87
88 ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str());
89
90 auto url_with_auth = this->get_url_with_auth_(this->url_);
91 if (url_with_auth.empty()) {
92 return OTA_BAD_URL;
93 }
94 ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
95 ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
96
97 auto container = this->parent_->get(url_with_auth);
98
99 if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
101 }
102
103 // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it
104 md5_receive.init();
105 ESP_LOGV(TAG, "MD5Digest initialized, OTA backend begin");
106 auto backend = ota::make_ota_backend();
107 auto error_code = backend->begin(container->content_length);
108 if (error_code != ota::OTA_RESPONSE_OK) {
109 ESP_LOGW(TAG, "backend->begin error: %d", error_code);
110 this->cleanup_(std::move(backend), container);
111 return error_code;
112 }
113
114 // NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
115 // Use http_read_loop_result() helper instead of checking return values directly
116 uint32_t last_data_time = millis();
117 const uint32_t read_timeout = this->parent_->get_timeout();
118
119 while (container->get_bytes_read() < container->content_length) {
120 // read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
121 int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
122 ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
123 container->content_length, bufsize_or_error);
124
125 // feed watchdog and give other tasks a chance to run
126 App.feed_wdt();
127 yield();
128
129 auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
130 if (result == HttpReadLoopResult::RETRY)
131 continue;
132 // For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length).
133 // For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives,
134 // which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder
135 // finishes mid-read, so this is needed for correctness.
136 if (result == HttpReadLoopResult::COMPLETE)
137 break;
138 if (result != HttpReadLoopResult::DATA) {
139 if (result == HttpReadLoopResult::TIMEOUT) {
140 ESP_LOGE(TAG, "Timeout reading data");
141 } else {
142 ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
143 }
144 this->cleanup_(std::move(backend), container);
146 }
147
148 // At this point bufsize_or_error > 0, so it's a valid size
149 if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
150 // add read bytes to MD5
151 md5_receive.add(buf, bufsize_or_error);
152
153 // write bytes to OTA backend
154 this->update_started_ = true;
155 error_code = backend->write(buf, bufsize_or_error);
156 if (error_code != ota::OTA_RESPONSE_OK) {
157 // error code explanation available at
158 // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
159 ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
160 container->get_bytes_read() - bufsize_or_error, container->content_length);
161 this->cleanup_(std::move(backend), container);
162 return error_code;
163 }
164 }
165
166 uint32_t now = millis();
167 if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
168 last_progress = now;
169 float percentage = container->get_bytes_read() * 100.0f / container->content_length;
170 ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
171#ifdef USE_OTA_STATE_LISTENER
172 this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
173#endif
174 }
175 } // while
176
177 ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000);
178
179 // verify MD5 is as expected and act accordingly
180 md5_receive.calculate();
181 md5_receive.get_hex(md5_receive_str);
182 this->md5_computed_ = md5_receive_str;
183 if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
184 ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
185 this->cleanup_(std::move(backend), container);
187 } else {
188 backend->set_update_md5(md5_receive_str);
189 }
190
191 container->end();
192
193 // feed watchdog and give other tasks a chance to run
194 App.feed_wdt();
195 yield();
196 delay(100); // NOLINT
197
198 error_code = backend->end();
199 if (error_code != ota::OTA_RESPONSE_OK) {
200 ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
201 this->cleanup_(std::move(backend), container);
202 return error_code;
203 }
204
205 ESP_LOGI(TAG, "Update complete");
207}
208
209// URL-encode characters that are not unreserved per RFC 3986 section 2.3.
210// This is needed for embedding userinfo (username/password) in URLs safely.
211static std::string url_encode(const std::string &str) {
212 std::string result;
213 result.reserve(str.size());
214 for (char c : str) {
215 if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' || c == '.' || c == '~') {
216 result += c;
217 } else {
218 result += '%';
219 result += format_hex_pretty_char((static_cast<uint8_t>(c) >> 4) & 0x0F);
220 result += format_hex_pretty_char(static_cast<uint8_t>(c) & 0x0F);
221 }
222 }
223 return result;
224}
225
226void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
227void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
228
229std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
230 if (this->username_.empty() || this->password_.empty()) {
231 return url;
232 }
233
234 auto start_char = url.find("://");
235 if ((start_char == std::string::npos) || (start_char < 4)) {
236 ESP_LOGE(TAG, "Incorrect URL prefix");
237 return {};
238 }
239
240 ESP_LOGD(TAG, "Using basic HTTP authentication");
241
242 start_char += 3; // skip '://' characters
243 auto url_with_auth =
244 url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char);
245 return url_with_auth;
246}
247
249 if (this->md5_url_.empty()) {
250 return false;
251 }
252
253 auto url_with_auth = this->get_url_with_auth_(this->md5_url_);
254 if (url_with_auth.empty()) {
255 return false;
256 }
257
258 ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
259 ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
260 auto container = this->parent_->get(url_with_auth);
261 if (container == nullptr) {
262 ESP_LOGE(TAG, "Failed to connect to MD5 URL");
263 return false;
264 }
265 size_t length = container->content_length;
266 if (length == 0) {
267 container->end();
268 return false;
269 }
270 if (length < MD5_SIZE) {
271 ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
272 container->end();
273 return false;
274 }
275
276 this->md5_expected_.resize(MD5_SIZE);
277 auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
278 this->parent_->get_timeout());
279 container->end();
280
281 if (result.status != HttpReadStatus::OK) {
282 if (result.status == HttpReadStatus::TIMEOUT) {
283 ESP_LOGE(TAG, "Timeout reading MD5");
284 } else {
285 ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
286 }
287 return false;
288 }
289 return true;
290}
291
292bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
293 if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) {
294 ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
295 return false;
296 }
297 return true;
298}
299
300} // namespace http_request
301} // namespace esphome
void feed_wdt(uint32_t time=0)
void get_hex(char *output)
Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes.
Definition hash_base.h:29
void set_password(const std::string &password)
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr< HttpContainer > &container)
void set_username(const std::string &username)
std::string get_url_with_auth_(const std::string &url)
void set_md5_url(const std::string &md5_url)
void calculate() override
Compute the digest, based on the provided data.
Definition md5.cpp:17
void add(const uint8_t *data, size_t len) override
Add bytes of data for the digest.
Definition md5.cpp:15
void init() override
Initialize a new MD5 digest computation.
Definition md5.cpp:10
void notify_state_(OTAState state, float progress, uint8_t error)
@ TIMEOUT
Timeout waiting for data, caller should exit loop.
@ COMPLETE
All content has been read, caller should exit loop.
@ RETRY
No data yet, already delayed, caller should continue loop.
@ DATA
Data was read, process it.
HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, bool is_read_complete)
Process a read result with timeout tracking and delay handling.
HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms)
Read data from HTTP container into buffer with timeout handling Handles feed_wdt, yield,...
@ TIMEOUT
Timeout waiting for data.
@ OK
Read completed successfully.
decltype(make_ota_backend()) OTABackendPtr
@ OTA_RESPONSE_ERROR_MD5_MISMATCH
Definition ota_backend.h:39
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
char format_hex_pretty_char(uint8_t v)
Convert a nibble (0-15) to uppercase hex char (used for pretty printing)
Definition helpers.h:1119
void HOT yield()
Definition core.cpp:25
void HOT delay(uint32_t ms)
Definition core.cpp:28
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0