web_server_idf: support x-www-form-urlencoded POST requests (#6037)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
dentra 2024-03-11 03:09:36 +03:00 committed by GitHub
parent 725b0c81e8
commit c899a33d1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 181 additions and 85 deletions

View file

@ -0,0 +1,93 @@
#ifdef USE_ESP_IDF
#include <memory>
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "http_parser.h"
#include "utils.h"
namespace esphome {
namespace web_server_idf {
static const char *const TAG = "web_server_idf_utils";
void url_decode(char *str) {
char *ptr = str, buf;
for (; *str; str++, ptr++) {
if (*str == '%') {
str++;
if (parse_hex(str, 2, reinterpret_cast<uint8_t *>(&buf), 1) == 2) {
*ptr = buf;
str++;
} else {
str--;
*ptr = *str;
}
} else if (*str == '+') {
*ptr = ' ';
} else {
*ptr = *str;
}
}
*ptr = *str;
}
bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); }
optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
size_t len = httpd_req_get_hdr_value_len(req, name);
if (len == 0) {
return {};
}
std::string str;
str.resize(len);
auto res = httpd_req_get_hdr_value_str(req, name, &str[0], len + 1);
if (res != ESP_OK) {
return {};
}
return {str};
}
optional<std::string> request_get_url_query(httpd_req_t *req) {
auto len = httpd_req_get_url_query_len(req);
if (len == 0) {
return {};
}
std::string str;
str.resize(len);
auto res = httpd_req_get_url_query_str(req, &str[0], len + 1);
if (res != ESP_OK) {
ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res));
return {};
}
return {str};
}
optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
if (query_url.empty()) {
return {};
}
auto val = std::unique_ptr<char[]>(new char[query_url.size()]);
if (!val) {
ESP_LOGE(TAG, "Not enough memory to the query key value");
return {};
}
if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) {
return {};
}
url_decode(val.get());
return {val.get()};
}
} // namespace web_server_idf
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -0,0 +1,17 @@
#pragma once
#ifdef USE_ESP_IDF
#include <esp_http_server.h>
#include "esphome/core/helpers.h"
namespace esphome {
namespace web_server_idf {
bool request_has_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_url_query(httpd_req_t *req);
optional<std::string> query_key_value(const std::string &query_url, const std::string &key);
} // namespace web_server_idf
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -7,6 +7,7 @@
#include "esp_tls_crypto.h" #include "esp_tls_crypto.h"
#include "utils.h"
#include "web_server_idf.h" #include "web_server_idf.h"
namespace esphome { namespace esphome {
@ -47,7 +48,7 @@ void AsyncWebServer::begin() {
const httpd_uri_t handler_post = { const httpd_uri_t handler_post = {
.uri = "", .uri = "",
.method = HTTP_POST, .method = HTTP_POST,
.handler = AsyncWebServer::request_handler, .handler = AsyncWebServer::request_post_handler,
.user_ctx = this, .user_ctx = this,
}; };
httpd_register_uri_handler(this->server_, &handler_post); httpd_register_uri_handler(this->server_, &handler_post);
@ -62,20 +63,62 @@ void AsyncWebServer::begin() {
} }
} }
esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
auto content_type = request_get_header(r, "Content-Type");
if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") {
ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request");
// fallback to get handler to support backward compatibility
return AsyncWebServer::request_handler(r);
}
if (!request_has_header(r, "Content-Length")) {
ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri);
httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
return ESP_OK;
}
if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) {
ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL;
}
std::string post_query;
if (r->content_len > 0) {
post_query.resize(r->content_len);
const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1);
if (ret <= 0) { // 0 return value indicates connection closed
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr);
return ESP_ERR_TIMEOUT;
}
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL;
}
}
AsyncWebServerRequest req(r, std::move(post_query));
return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
}
esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) { 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); ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri);
AsyncWebServerRequest req(r); AsyncWebServerRequest req(r);
auto *server = static_cast<AsyncWebServer *>(r->user_ctx); return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
for (auto *handler : server->handlers_) { }
if (handler->canHandle(&req)) {
esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const {
for (auto *handler : this->handlers_) {
if (handler->canHandle(request)) {
// At now process only basic requests. // At now process only basic requests.
// OTA requires multipart request support and handleUpload for it // OTA requires multipart request support and handleUpload for it
handler->handleRequest(&req); handler->handleRequest(request);
return ESP_OK; return ESP_OK;
} }
} }
if (server->on_not_found_) { if (this->on_not_found_) {
server->on_not_found_(&req); this->on_not_found_(request);
return ESP_OK; return ESP_OK;
} }
return ESP_ERR_NOT_FOUND; return ESP_ERR_NOT_FOUND;
@ -88,22 +131,10 @@ AsyncWebServerRequest::~AsyncWebServerRequest() {
} }
} }
bool AsyncWebServerRequest::hasHeader(const char *name) const { return httpd_req_get_hdr_value_len(*this, name); } bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); }
optional<std::string> AsyncWebServerRequest::get_header(const char *name) const { optional<std::string> AsyncWebServerRequest::get_header(const char *name) const {
size_t buf_len = httpd_req_get_hdr_value_len(*this, name); return request_get_header(*this, name);
if (buf_len == 0) {
return {};
}
auto buf = std::unique_ptr<char[]>(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 { std::string AsyncWebServerRequest::url() const {
@ -193,74 +224,25 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); 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<uint8_t>(&in[i], 2);
if (c.has_value()) {
out += static_cast<char>(*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) { AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
auto find = this->params_.find(name); auto find = this->params_.find(name);
if (find != this->params_.end()) { if (find != this->params_.end()) {
return find->second; return find->second;
} }
auto query_len = httpd_req_get_url_query_len(this->req_); optional<std::string> val = query_key_value(this->post_query_, name);
if (query_len == 0) { if (!val.has_value()) {
return nullptr; auto url_query = request_get_url_query(*this);
if (url_query.has_value()) {
val = query_key_value(url_query.value(), name);
}
} }
auto query_str = std::unique_ptr<char[]>(new char[++query_len]); AsyncWebParameter *param = nullptr;
if (!query_str) { if (val.has_value()) {
ESP_LOGE(TAG, "No enough memory for get query param"); param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory)
return nullptr;
} }
this->params_.insert({name, param});
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<char[]>(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; return param;
} }
@ -271,14 +253,15 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
void AsyncResponseStream::print(float value) { this->print(to_string(value)); } void AsyncResponseStream::print(float value) { this->print(to_string(value)); }
void AsyncResponseStream::printf(const char *fmt, ...) { void AsyncResponseStream::printf(const char *fmt, ...) {
std::string str;
va_list args; va_list args;
va_start(args, fmt); va_start(args, fmt);
size_t length = vsnprintf(nullptr, 0, fmt, args); const int length = vsnprintf(nullptr, 0, fmt, args);
va_end(args); va_end(args);
std::string str;
str.resize(length); str.resize(length);
va_start(args, fmt); va_start(args, fmt);
vsnprintf(&str[0], length + 1, fmt, args); vsnprintf(&str[0], length + 1, fmt, args);
va_end(args); va_end(args);

View file

@ -90,11 +90,10 @@ class AsyncWebServerResponseProgmem : public AsyncWebServerResponse {
protected: protected:
const uint8_t *data_; const uint8_t *data_;
const size_t size_; size_t size_;
}; };
class AsyncWebServerRequest { class AsyncWebServerRequest {
// FIXME friend class AsyncWebServerResponse;
friend class AsyncWebServer; friend class AsyncWebServer;
public: public:
@ -164,7 +163,9 @@ class AsyncWebServerRequest {
httpd_req_t *req_; httpd_req_t *req_;
AsyncWebServerResponse *rsp_{}; AsyncWebServerResponse *rsp_{};
std::map<std::string, AsyncWebParameter *> params_; std::map<std::string, AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {}
void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type); void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type);
}; };
@ -191,6 +192,8 @@ class AsyncWebServer {
uint16_t port_{}; uint16_t port_{};
httpd_handle_t server_{}; httpd_handle_t server_{};
static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_handler(httpd_req_t *r);
static esp_err_t request_post_handler(httpd_req_t *r);
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
std::vector<AsyncWebHandler *> handlers_; std::vector<AsyncWebHandler *> handlers_;
std::function<void(AsyncWebServerRequest *request)> on_not_found_{}; std::function<void(AsyncWebServerRequest *request)> on_not_found_{};
}; };