Merge Develop into Main (#298)
Various improvements and new internal APIs including a new Development service+app which allows `tactility.py` to upload and run applications remotely.
This commit is contained in:
committed by
GitHub
parent
d06197a6aa
commit
ab4cf79a47
@@ -0,0 +1,160 @@
|
||||
#include "Tactility/network/HttpdReq.h"
|
||||
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <Tactility/Log.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#define TAG "network"
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) {
|
||||
size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
|
||||
if (header_size == 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "header missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto header_buffer = std::make_unique<char[]>(header_size + 1);
|
||||
if (header_buffer == nullptr) {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (httpd_req_get_hdr_value_str(request, name.c_str(), header_buffer.get(), header_size + 1) != ESP_OK) {
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
value = header_buffer.get();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary) {
|
||||
std::string content_type_header;
|
||||
if (!getHeaderOrSendError(request, "Content-Type", content_type_header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto boundary_index = content_type_header.find("boundary=");
|
||||
if (boundary_index == std::string::npos) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "boundary not found in Content-Type");
|
||||
return false;
|
||||
}
|
||||
|
||||
boundary = content_type_header.substr(boundary_index + 9);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getQueryOrSendError(httpd_req_t* request, std::string& query) {
|
||||
size_t buffer_length = httpd_req_get_url_query_len(request);
|
||||
if (buffer_length == 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto buffer = std::make_unique<char[]>(buffer_length + 1);
|
||||
if (buffer.get() == nullptr || httpd_req_get_url_query_str(request, buffer.get(), buffer_length + 1) != ESP_OK) {
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
query = buffer.get();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead) {
|
||||
assert(length > 0);
|
||||
bytesRead = 0;
|
||||
|
||||
auto result = std::make_unique<char[]>(length);
|
||||
|
||||
while (bytesRead < length) {
|
||||
size_t read_size = length - bytesRead;
|
||||
size_t bytes_received = httpd_req_recv(request, result.get() + bytesRead, read_size);
|
||||
if (bytes_received <= 0) {
|
||||
TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bytesRead += bytes_received;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) {
|
||||
size_t read_index = 0;
|
||||
std::stringstream result;
|
||||
while (!result.str().ends_with(terminator)) {
|
||||
char buffer;
|
||||
size_t bytes_read = httpd_req_recv(request, &buffer, 1);
|
||||
if (bytes_read <= 0) {
|
||||
return "";
|
||||
} else {
|
||||
read_index += bytes_read;
|
||||
}
|
||||
|
||||
result << buffer;
|
||||
}
|
||||
|
||||
return result.str();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> parseContentDisposition(const std::vector<std::string>& input) {
|
||||
std::map<std::string, std::string> result;
|
||||
static std::string prefix = "Content-Disposition: ";
|
||||
|
||||
// Find header
|
||||
auto content_disposition_header = std::ranges::find_if(input, [](const std::string& header) {
|
||||
return header.starts_with(prefix);
|
||||
});
|
||||
|
||||
// Header not found
|
||||
if (content_disposition_header == input.end()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto parseable = content_disposition_header->substr(prefix.size());
|
||||
auto parts = string::split(parseable, "; ");
|
||||
for (auto part : parts) {
|
||||
auto key_value = string::split(part, "=");
|
||||
if (key_value.size() == 2) {
|
||||
// Trim trailing newlines
|
||||
auto value = string::trim(key_value[1], "\r\n");
|
||||
if (value.size() > 2) {
|
||||
result[key_value[0]] = value.substr(1, value.size() - 2);
|
||||
} else {
|
||||
result[key_value[0]] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead) {
|
||||
size_t bytes_read;
|
||||
auto buffer = receiveByteArray(request, toRead.length(), bytes_read);
|
||||
if (bytes_read != toRead.length()) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "failed to read discardable data");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memcmp(buffer.get(), toRead.c_str(), bytes_read) != 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "discardable data mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "Tactility/network/Url.h"
|
||||
|
||||
#include <Tactility/Log.h>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
std::map<std::string, std::string> parseUrlQuery(std::string query) {
|
||||
std::map<std::string, std::string> result;
|
||||
|
||||
if (query.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t current_index = query[0] == '?' ? 1U : 0U;
|
||||
auto equals_index = query.find_first_of('=', current_index);
|
||||
while (equals_index != std::string::npos) {
|
||||
auto index_boundary = query.find_first_of('&', equals_index + 1);
|
||||
if (index_boundary == std::string::npos) {
|
||||
index_boundary = query.size();
|
||||
}
|
||||
auto key = query.substr(current_index, (equals_index - current_index));
|
||||
auto decodedKey = urlDecode(key);
|
||||
auto value = query.substr(equals_index + 1, (index_boundary - equals_index - 1));
|
||||
auto decodedValue = urlDecode(value);
|
||||
|
||||
result[decodedKey] = decodedValue;
|
||||
|
||||
// Find next token
|
||||
current_index = index_boundary + 1;
|
||||
equals_index = query.find_first_of('=', current_index);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Adapted from https://stackoverflow.com/a/29962178/3848666
|
||||
std::string urlEncode(const std::string& input) {
|
||||
std::string result = "";
|
||||
const char* characters = input.c_str();
|
||||
char hex_buffer[10];
|
||||
size_t input_length = input.length();
|
||||
|
||||
for (size_t i = 0;i < input_length;i++) {
|
||||
unsigned char c = characters[i];
|
||||
// uncomment this if you want to encode spaces with +
|
||||
if (c==' ') {
|
||||
result += '+';
|
||||
} else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
result += c;
|
||||
} else {
|
||||
sprintf(hex_buffer, "%%%02X", c); //%% means '%' literal, %02X means at least two digits, paddable with a leading zero
|
||||
result += hex_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Adapted from https://stackoverflow.com/a/29962178/3848666
|
||||
std::string urlDecode(const std::string& input) {
|
||||
std::string result;
|
||||
size_t conversion_buffer, input_length = input.length();
|
||||
|
||||
for (size_t i = 0; i < input_length; i++) {
|
||||
if (input[i] != '%') {
|
||||
if (input[i] == '+') {
|
||||
result += ' ';
|
||||
} else {
|
||||
result += input[i];
|
||||
}
|
||||
} else {
|
||||
sscanf(input.substr(i + 1, 2).c_str(), "%x", &conversion_buffer);
|
||||
char c = static_cast<char>(conversion_buffer);
|
||||
result += c;
|
||||
i = i + 2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user