client.cpp 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #include "kbf/http/client.h"
  2. #include <freertos/task.h>
  3. #include <esp_log.h>
  4. #include <esp_tls.h>
  5. #include <memory>
  6. #include "kbf/exception.h"
  7. #include "kbf/http/exception.h"
  8. #include "kbf/macros.h"
  9. using namespace kbf;
  10. using std::make_shared;
  11. using std::string;
  12. using std::shared_ptr;
  13. using std::optional;
  14. using std::nullopt;
  15. using std::map;
  16. http::Client::Client(int timeoutMs, bool async) : timeoutMs(timeoutMs), async(async), buffer() {
  17. ESP_LOGD(TAG, "%s()", __func__);
  18. init();
  19. }
  20. void http::Client::init() {
  21. ESP_LOGD(TAG, "%s()", __func__);
  22. esp_http_client_config_t config{};
  23. config.event_handler = handleHttpEvent;
  24. config.user_data = this;
  25. config.host = "localhost";
  26. config.path = "/";
  27. config.disable_auto_redirect = false; // do not set to true, IDF bug :(
  28. config.max_redirection_count = 0;
  29. config.timeout_ms = timeoutMs;
  30. config.is_async = async;
  31. config.use_global_ca_store = true;
  32. handle = esp_http_client_init(&config);
  33. }
  34. http::Client::~Client() {
  35. ESP_LOGD(TAG, "%s()", __func__);
  36. CHECK_ABORT(esp_http_client_cleanup(handle));
  37. }
  38. std::shared_ptr<kbf::http::Response>
  39. http::Client::performRequest(const kbf::http::Method method, const std::string &url, const nlohmann::json *postData,
  40. const optional<const map<string, string>> &headers) {
  41. ESP_LOGD(TAG, "%s(); method: %d, url: %s", __func__, method, url.c_str());
  42. if (running) {
  43. ESP_LOGE(TAG, "request already in progress");
  44. return nullptr;
  45. }
  46. running = true;
  47. response = std::make_shared<Response>();
  48. buffer.clear();
  49. string body;
  50. if (method == GET) {
  51. esp_http_client_set_method(handle, HTTP_METHOD_GET);
  52. } else if (method == POST) {
  53. body = postData ? postData->dump() : "";
  54. ESP_LOGV(TAG, "request body:");
  55. ESP_LOG_BUFFER_HEXDUMP(TAG, body.c_str(), body.size(), ESP_LOG_VERBOSE);
  56. esp_http_client_set_method(handle, HTTP_METHOD_POST);
  57. esp_http_client_set_header(handle, "Content-Type", "application/json");
  58. esp_http_client_set_post_field(handle, body.c_str(), body.length());
  59. } else {
  60. ESP_LOGE(TAG, "unhandled method: %d", method);
  61. throw kbf::exception::KBFError("unknown HTTP method " + std::to_string(method));
  62. }
  63. if (headers) {
  64. ESP_LOGD(TAG, "adding headers");
  65. for (auto const &[key, value] : *headers) {
  66. ESP_LOGD(TAG, " %s: %s", key.c_str(), value.c_str());
  67. esp_http_client_set_header(handle, key.c_str(), value.c_str());
  68. }
  69. }
  70. esp_http_client_set_url(handle, url.c_str());
  71. auto err = esp_http_client_perform(handle);
  72. if (err == ESP_ERR_HTTP_MAX_REDIRECT) {
  73. // TODO this seems get triggered on HTTP 4XX
  74. ESP_LOGW(TAG, "reached redirect limit");
  75. updateResponse(*this);
  76. err = ESP_OK;
  77. }
  78. if (err == ESP_ERR_HTTP_EAGAIN) {
  79. ESP_LOGD(TAG, "async request in progress, returning nullptr");
  80. return nullptr;
  81. } else if (err == ESP_OK) {
  82. ESP_LOGD(TAG, "success");
  83. return response;
  84. } else if ((err == ESP_ERR_HTTP_FETCH_HEADER || err == ESP_FAIL)) {
  85. // TODO happens on timeout and conn reset as well; can we distinguish between errors here properly?
  86. if (retry) {
  87. ESP_LOGE(TAG, "retry failed (%s)", esp_err_to_name(err));
  88. throw exception::RequestError();
  89. }
  90. if (timeoutMs) {
  91. ESP_LOGE(TAG, "request failed (%s), assuming timeout", esp_err_to_name(err));
  92. throw exception::Timeout();
  93. }
  94. ESP_LOGW(TAG, "request failed (%s), maybe the connection was dropped? retrying...", esp_err_to_name(err));
  95. retry = true;
  96. esp_http_client_cleanup(handle);
  97. init();
  98. if (method == http::GET) {
  99. return get(url, headers);
  100. } else if (method == http::POST) {
  101. return post(url, *postData, headers);
  102. } else {
  103. throw kbf::exception::KBFError("unknown HTTP method " + std::to_string(method));
  104. }
  105. } else if (err == ESP_ERR_HTTP_CONNECT) {
  106. ESP_LOGW(TAG, "HTTP request failed: %s", esp_err_to_name(err));
  107. running = false;
  108. throw exception::ConnectionError();
  109. } else {
  110. ESP_LOGE(TAG, "unhandled HTTP error: %s", esp_err_to_name(err));
  111. running = false;
  112. throw kbf::exception::KBFError("unhandled HTTP error: %s" + string(esp_err_to_name(err)));
  113. }
  114. }
  115. esp_err_t http::Client::handleHttpEvent(esp_http_client_event_t *event) {
  116. auto client = (Client *) event->user_data;
  117. switch (event->event_id) {
  118. case HTTP_EVENT_ERROR:
  119. ESP_LOGE(TAG, "fixme: unhandled error event: HTTP_EVENT_ERROR");
  120. client->running = false;
  121. client->retry = false;
  122. throw kbf::exception::KBFError("HTTP_EVENT_ERROR");
  123. case HTTP_EVENT_ON_CONNECTED:
  124. ESP_LOGD(TAG, "connected");
  125. break;
  126. case HTTP_EVENT_HEADERS_SENT:
  127. ESP_LOGD(TAG, "request sent");
  128. break;
  129. case HTTP_EVENT_ON_HEADER:
  130. ESP_LOGD(TAG, "header received: %s: %s", event->header_key, event->header_value);
  131. client->response->headers[event->header_key] = event->header_value;
  132. break;
  133. case HTTP_EVENT_ON_DATA:
  134. ESP_LOGD(TAG, "data received");
  135. if (esp_http_client_is_chunked_response(client->handle)) {
  136. ESP_LOGE(TAG, "receiving chunked data not implemented");
  137. throw kbf::exception::KBFError("HTTP client chunked data not implemented");
  138. }
  139. client->buffer.append((char *) event->data, event->data_len);
  140. break;
  141. case HTTP_EVENT_ON_FINISH:
  142. ESP_LOGD(TAG, "finished");
  143. updateResponse(*client);
  144. break;
  145. case HTTP_EVENT_DISCONNECTED:
  146. ESP_LOGI(TAG, "disconnected");
  147. client->running = false;
  148. break;
  149. }
  150. return ESP_OK;
  151. }
  152. void http::Client::updateResponse(http::Client &client) {
  153. client.response->status = esp_http_client_get_status_code(client.handle);
  154. client.response->body = client.buffer;
  155. ESP_LOGD(TAG, "HTTP status: %d, Content-Length: %s", client.response->status,
  156. client.response->headers["Content-Length"].c_str());
  157. ESP_LOG_BUFFER_HEXDUMP(TAG, client.response->body.data(), client.response->body.length(), ESP_LOG_VERBOSE);
  158. if (client.onSuccess) { client.onSuccess(client, *client.response); }
  159. client.running = false;
  160. client.retry = false;
  161. }
  162. shared_ptr<http::Response>
  163. http::Client::get(const string &url, const optional<const map<string, string>> &headers) {
  164. ESP_LOGD(TAG, "%s(%s)", __func__, url.c_str());
  165. return performRequest(GET, url, nullptr, headers);
  166. }
  167. shared_ptr<http::Response>
  168. http::Client::post(const string &url, const nlohmann::json &data, const optional<const map<string, string>> &headers) {
  169. ESP_LOGD(TAG, "%s(%s)", __func__, url.c_str());
  170. return performRequest(POST, url, &data, headers);
  171. }
  172. void http::Client::addCert(const unsigned char *const start, const unsigned char *const end) {
  173. ESP_LOGI(TAG, "%s()", __func__);
  174. ESP_LOGD(TAG, "certificate:\n%s", start);
  175. CHECK(esp_tls_set_global_ca_store(start, end - start));
  176. }
  177. void http::Client::clearCerts() {
  178. ESP_LOGI(TAG, "%s()", __func__);
  179. esp_tls_free_global_ca_store();
  180. }
  181. void http::Client::disconnect() {
  182. ESP_LOGD(TAG, "%s()", __func__);
  183. CHECK(esp_http_client_close(handle));
  184. }