ESPHome  2024.11.1
ota_http_request.cpp
Go to the documentation of this file.
1 #include "ota_http_request.h"
2 
4 #include "esphome/core/defines.h"
5 #include "esphome/core/log.h"
6 
14 
15 namespace esphome {
16 namespace http_request {
17 
18 static const char *const TAG = "http_request.ota";
19 
21 #ifdef USE_OTA_STATE_CALLBACK
23 #endif
24 }
25 
26 void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
27 
28 void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
29  if (!this->validate_url_(url)) {
30  this->md5_url_.clear(); // URL was not valid; prevent flashing until it is
31  return;
32  }
33  this->md5_url_ = url;
34  this->md5_expected_.clear(); // to be retrieved later
35 }
36 
37 void OtaHttpRequestComponent::set_url(const std::string &url) {
38  if (!this->validate_url_(url)) {
39  this->url_.clear(); // URL was not valid; prevent flashing until it is
40  return;
41  }
42  this->url_ = url;
43 }
44 
46  if (this->url_.empty()) {
47  ESP_LOGE(TAG, "URL not set; cannot start update");
48  return;
49  }
50 
51  ESP_LOGI(TAG, "Starting update...");
52 #ifdef USE_OTA_STATE_CALLBACK
53  this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
54 #endif
55 
56  auto ota_status = this->do_ota_();
57 
58  switch (ota_status) {
60 #ifdef USE_OTA_STATE_CALLBACK
61  this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status);
62 #endif
63  delay(10);
64  App.safe_reboot();
65  break;
66 
67  default:
68 #ifdef USE_OTA_STATE_CALLBACK
69  this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status);
70 #endif
71  this->md5_computed_.clear(); // will be reset at next attempt
72  this->md5_expected_.clear(); // will be reset at next attempt
73  break;
74  }
75 }
76 
77 void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
78  const std::shared_ptr<HttpContainer> &container) {
79  if (this->update_started_) {
80  ESP_LOGV(TAG, "Aborting OTA backend");
81  backend->abort();
82  }
83  ESP_LOGV(TAG, "Aborting HTTP connection");
84  container->end();
85 };
86 
89  uint32_t last_progress = 0;
90  uint32_t update_start_time = millis();
91  md5::MD5Digest md5_receive;
92  std::unique_ptr<char[]> md5_receive_str(new char[33]);
93 
94  if (this->md5_expected_.empty() && !this->http_get_md5_()) {
95  return OTA_MD5_INVALID;
96  }
97 
98  ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str());
99 
100  auto url_with_auth = this->get_url_with_auth_(this->url_);
101  if (url_with_auth.empty()) {
102  return OTA_BAD_URL;
103  }
104  ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
105  ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
106 
107  auto container = this->parent_->get(url_with_auth);
108 
109  if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
110  return OTA_CONNECTION_ERROR;
111  }
112 
113  // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it
114  md5_receive.init();
115  ESP_LOGV(TAG, "MD5Digest initialized");
116 
117  ESP_LOGV(TAG, "OTA backend begin");
118  auto backend = ota::make_ota_backend();
119  auto error_code = backend->begin(container->content_length);
120  if (error_code != ota::OTA_RESPONSE_OK) {
121  ESP_LOGW(TAG, "backend->begin error: %d", error_code);
122  this->cleanup_(std::move(backend), container);
123  return error_code;
124  }
125 
126  while (container->get_bytes_read() < container->content_length) {
127  // read a maximum of chunk_size bytes into buf. (real read size returned)
128  int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
129  ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
130  container->content_length, bufsize);
131 
132  // feed watchdog and give other tasks a chance to run
133  App.feed_wdt();
134  yield();
135 
136  if (bufsize < 0) {
137  ESP_LOGE(TAG, "Stream closed");
138  this->cleanup_(std::move(backend), container);
139  return OTA_CONNECTION_ERROR;
140  } else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
141  // add read bytes to MD5
142  md5_receive.add(buf, bufsize);
143 
144  // write bytes to OTA backend
145  this->update_started_ = true;
146  error_code = backend->write(buf, bufsize);
147  if (error_code != ota::OTA_RESPONSE_OK) {
148  // error code explanation available at
149  // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
150  ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
151  container->get_bytes_read() - bufsize, container->content_length);
152  this->cleanup_(std::move(backend), container);
153  return error_code;
154  }
155  }
156 
157  uint32_t now = millis();
158  if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
159  last_progress = now;
160  float percentage = container->get_bytes_read() * 100.0f / container->content_length;
161  ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
162 #ifdef USE_OTA_STATE_CALLBACK
163  this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
164 #endif
165  }
166  } // while
167 
168  ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000);
169 
170  // verify MD5 is as expected and act accordingly
171  md5_receive.calculate();
172  md5_receive.get_hex(md5_receive_str.get());
173  this->md5_computed_ = md5_receive_str.get();
174  if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
175  ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
176  this->cleanup_(std::move(backend), container);
178  } else {
179  backend->set_update_md5(md5_receive_str.get());
180  }
181 
182  container->end();
183 
184  // feed watchdog and give other tasks a chance to run
185  App.feed_wdt();
186  yield();
187  delay(100); // NOLINT
188 
189  error_code = backend->end();
190  if (error_code != ota::OTA_RESPONSE_OK) {
191  ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
192  this->cleanup_(std::move(backend), container);
193  return error_code;
194  }
195 
196  ESP_LOGI(TAG, "Update complete");
197  return ota::OTA_RESPONSE_OK;
198 }
199 
200 std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
201  if (this->username_.empty() || this->password_.empty()) {
202  return url;
203  }
204 
205  auto start_char = url.find("://");
206  if ((start_char == std::string::npos) || (start_char < 4)) {
207  ESP_LOGE(TAG, "Incorrect URL prefix");
208  return {};
209  }
210 
211  ESP_LOGD(TAG, "Using basic HTTP authentication");
212 
213  start_char += 3; // skip '://' characters
214  auto url_with_auth =
215  url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char);
216  return url_with_auth;
217 }
218 
220  if (this->md5_url_.empty()) {
221  return false;
222  }
223 
224  auto url_with_auth = this->get_url_with_auth_(this->md5_url_);
225  if (url_with_auth.empty()) {
226  return false;
227  }
228 
229  ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
230  ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
231  auto container = this->parent_->get(url_with_auth);
232  if (container == nullptr) {
233  ESP_LOGE(TAG, "Failed to connect to MD5 URL");
234  return false;
235  }
236  size_t length = container->content_length;
237  if (length == 0) {
238  container->end();
239  return false;
240  }
241  if (length < MD5_SIZE) {
242  ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
243  container->end();
244  return false;
245  }
246 
247  this->md5_expected_.resize(MD5_SIZE);
248  int read_len = 0;
249  while (container->get_bytes_read() < MD5_SIZE) {
250  read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
251  App.feed_wdt();
252  yield();
253  }
254  container->end();
255 
256  ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
257  return read_len == MD5_SIZE;
258 }
259 
260 bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
261  if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) {
262  ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
263  return false;
264  }
265  return true;
266 }
267 
268 } // namespace http_request
269 } // namespace esphome
void init()
Initialize a new MD5 digest computation.
Definition: md5.cpp:11
void cleanup_(std::unique_ptr< ota::OTABackend > backend, const std::shared_ptr< HttpContainer > &container)
std::string get_url_with_auth_(const std::string &url)
void set_md5_url(const std::string &md5_url)
void register_ota_platform(OTAComponent *ota_caller)
Definition: ota_backend.cpp:16
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
void add(const uint8_t *data, size_t len)
Add bytes of data for the digest.
Definition: md5.cpp:16
CallbackManager< void(ota::OTAState, float, uint8_t)> state_callback_
Definition: ota_backend.h:70
HttpRequestComponent * parent_
Definition: helpers.h:533
Application App
Global storage of Application pointer - only one Application can exist.
std::unique_ptr< ota::OTABackend > make_ota_backend()
void get_hex(char *output)
Retrieve the MD5 digest as hex characters.
Definition: md5.cpp:45
void IRAM_ATTR HOT yield()
Definition: core.cpp:24
uint16_t length
Definition: tt21100.cpp:12
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
void calculate()
Compute the digest, based on the provided data.
Definition: md5.cpp:18
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26