#ifdef USE_ESP_IDF #include #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esp_tls_crypto.h" #include "web_server_idf.h" namespace esphome { namespace web_server_idf { #ifndef HTTPD_409 #define HTTPD_409 "409 Conflict" #endif #define CRLF_STR "\r\n" #define CRLF_LEN (sizeof(CRLF_STR) - 1) static const char *const TAG = "web_server_idf"; void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); this->server_ = nullptr; } } void AsyncWebServer::begin() { if (this->server_) { this->end(); } httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", .method = HTTP_GET, .handler = AsyncWebServer::request_handler, .user_ctx = this, }; httpd_register_uri_handler(this->server_, &handler_get); const httpd_uri_t handler_post = { .uri = "", .method = HTTP_POST, .handler = AsyncWebServer::request_handler, .user_ctx = this, }; httpd_register_uri_handler(this->server_, &handler_post); } } esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) { ESP_LOGV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri); AsyncWebServerRequest req(r); auto *server = static_cast(r->user_ctx); for (auto *handler : server->handlers_) { if (handler->canHandle(&req)) { // At now process only basic requests. // OTA requires multipart request support and handleUpload for it handler->handleRequest(&req); return ESP_OK; } } if (server->on_not_found_) { server->on_not_found_(&req); return ESP_OK; } return ESP_ERR_NOT_FOUND; } AsyncWebServerRequest::~AsyncWebServerRequest() { delete this->rsp_; for (const auto &pair : this->params_) { delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) } } optional AsyncWebServerRequest::get_header(const char *name) const { size_t buf_len = httpd_req_get_hdr_value_len(*this, name); if (buf_len == 0) { return {}; } auto buf = std::unique_ptr(new char[++buf_len]); if (!buf) { ESP_LOGE(TAG, "No enough memory for get header %s", name); return {}; } if (httpd_req_get_hdr_value_str(*this, name, buf.get(), buf_len) != ESP_OK) { return {}; } return {buf.get()}; } std::string AsyncWebServerRequest::url() const { auto *str = strchr(this->req_->uri, '?'); if (str == nullptr) { return this->req_->uri; } return std::string(this->req_->uri, str - this->req_->uri); } std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); } void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { this->init_response_(nullptr, code, content_type); if (content) { httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); } else { httpd_resp_send(*this, nullptr, 0); } } void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); httpd_resp_send(*this, nullptr, 0); } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { httpd_resp_set_status(*this, code == 200 ? HTTPD_200 : code == 404 ? HTTPD_404 : code == 409 ? HTTPD_409 : to_string(code).c_str()); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); } httpd_resp_set_hdr(*this, "Accept-Ranges", "none"); for (const auto &pair : DefaultHeaders::Instance().headers_) { httpd_resp_set_hdr(*this, pair.first.c_str(), pair.second.c_str()); } delete this->rsp_; this->rsp_ = rsp; } bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { if (username == nullptr || password == nullptr || *username == 0) { return true; } auto auth = this->get_header("Authorization"); if (!auth.has_value()) { return false; } auto *auth_str = auth.value().c_str(); const auto auth_prefix_len = sizeof("Basic ") - 1; if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) { ESP_LOGW(TAG, "Only Basic authorization supported yet"); return false; } std::string user_info; user_info += username; user_info += ':'; user_info += password; size_t n = 0, out; esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast(user_info.c_str()), user_info.size()); auto digest = std::unique_ptr(new char[n + 1]); esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, reinterpret_cast(user_info.c_str()), user_info.size()); return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0; } void AsyncWebServerRequest::requestAuthentication(const char *realm) const { httpd_resp_set_hdr(*this, "Connection", "keep-alive"); auto auth_val = str_sprintf("Basic realm=\"%s\"", realm ? realm : "Login Required"); httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str()); httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); } static std::string url_decode(const std::string &in) { std::string out; out.reserve(in.size()); for (std::size_t i = 0; i < in.size(); ++i) { if (in[i] == '%') { ++i; if (i + 1 < in.size()) { auto c = parse_hex(&in[i], 2); if (c.has_value()) { out += static_cast(*c); ++i; } else { out += '%'; out += in[i++]; out += in[i]; } } else { out += '%'; out += in[i]; } } else if (in[i] == '+') { out += ' '; } else { out += in[i]; } } return out; } AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { auto find = this->params_.find(name); if (find != this->params_.end()) { return find->second; } auto query_len = httpd_req_get_url_query_len(this->req_); if (query_len == 0) { return nullptr; } auto query_str = std::unique_ptr(new char[++query_len]); if (!query_str) { ESP_LOGE(TAG, "No enough memory for get query param"); return nullptr; } auto res = httpd_req_get_url_query_str(*this, query_str.get(), query_len); if (res != ESP_OK) { ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); return nullptr; } auto query_val = std::unique_ptr(new char[query_len]); if (!query_val) { ESP_LOGE(TAG, "No enough memory for get query param value"); return nullptr; } res = httpd_query_key_value(query_str.get(), name.c_str(), query_val.get(), query_len); if (res != ESP_OK) { this->params_.insert({name, nullptr}); return nullptr; } query_str.release(); auto decoded = url_decode(query_val.get()); query_val.release(); auto *param = new AsyncWebParameter(decoded); // NOLINT(cppcoreguidelines-owning-memory) this->params_.insert(std::make_pair(name, param)); return param; } void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } void AsyncResponseStream::print(float value) { this->print(to_string(value)); } void AsyncResponseStream::printf(const char *fmt, ...) { std::string str; va_list args; va_start(args, fmt); size_t length = vsnprintf(nullptr, 0, fmt, args); va_end(args); str.resize(length); va_start(args, fmt); vsnprintf(&str[0], length + 1, fmt, args); va_end(args); this->print(str); } AsyncEventSource::~AsyncEventSource() { for (auto *ses : this->sessions_) { delete ses; // NOLINT(cppcoreguidelines-owning-memory) } } void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { auto *rsp = new AsyncEventSourceResponse(request, this); // NOLINT(cppcoreguidelines-owning-memory) if (this->on_connect_) { this->on_connect_(rsp); } this->sessions_.insert(rsp); } void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { for (auto *ses : this->sessions_) { ses->send(message, event, id, reconnect); } } AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server) : server_(server) { httpd_req_t *req = *request; httpd_resp_set_status(req, HTTPD_200); httpd_resp_set_type(req, "text/event-stream"); httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); httpd_resp_set_hdr(req, "Connection", "keep-alive"); httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN); req->sess_ctx = this; req->free_ctx = AsyncEventSourceResponse::destroy; this->hd_ = req->handle; this->fd_ = httpd_req_to_sockfd(req); } void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); rsp->server_->sessions_.erase(rsp); delete rsp; // NOLINT(cppcoreguidelines-owning-memory) } void AsyncEventSourceResponse::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { if (this->fd_ == 0) { return; } std::string ev; if (reconnect) { ev.append("retry: ", sizeof("retry: ") - 1); ev.append(to_string(reconnect)); ev.append(CRLF_STR, CRLF_LEN); } if (id) { ev.append("id: ", sizeof("id: ") - 1); ev.append(to_string(id)); ev.append(CRLF_STR, CRLF_LEN); } if (event && *event) { ev.append("event: ", sizeof("event: ") - 1); ev.append(event); ev.append(CRLF_STR, CRLF_LEN); } if (message && *message) { ev.append("data: ", sizeof("data: ") - 1); ev.append(message); ev.append(CRLF_STR, CRLF_LEN); } if (ev.empty()) { return; } ev.append(CRLF_STR, CRLF_LEN); // Sending chunked content prelude auto cs = str_snprintf("%x" CRLF_STR, 4 * sizeof(ev.size()) + CRLF_LEN, ev.size()); httpd_socket_send(this->hd_, this->fd_, cs.c_str(), cs.size(), 0); // Sendiing content chunk httpd_socket_send(this->hd_, this->fd_, ev.c_str(), ev.size(), 0); // Indicate end of chunk httpd_socket_send(this->hd_, this->fd_, CRLF_STR, CRLF_LEN, 0); } } // namespace web_server_idf } // namespace esphome #endif // !defined(USE_ESP_IDF)