Multi-platform http server (#646)
- Add server support to `http-module` - Ensure `DevelopmentService` works on all platforms (including posix) - Ensure the built-in web server with dashboard works on all platforms (inluding posix). It still has some issues with certain featuers, but the basics work.
This commit is contained in:
committed by
GitHub
parent
a0b2ee7ebc
commit
1b16184a72
@@ -1,137 +1,10 @@
|
||||
#include <Tactility/LogMessages.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/network/HttpdReq.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
constexpr auto* TAG = "HttpdReq";
|
||||
|
||||
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) {
|
||||
LOG_E(TAG, LOG_MESSAGE_ALLOC_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;
|
||||
|
||||
// We have to use malloc() because make_unique() throws an exception
|
||||
// and we don't have exceptions enabled in the compiler settings
|
||||
auto* buffer = static_cast<char*>(malloc(length));
|
||||
if (buffer == nullptr) {
|
||||
LOG_E(TAG, "Out of memory (failed to allocated %u bytes)", (unsigned)length);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
constexpr int MAX_TIMEOUT_RETRIES = 5;
|
||||
int timeout_retries = 0;
|
||||
while (bytesRead < length) {
|
||||
size_t read_size = length - bytesRead;
|
||||
int bytes_received = httpd_req_recv(request, buffer + bytesRead, read_size);
|
||||
if (bytes_received == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
// Timeout - retry with backoff
|
||||
timeout_retries++;
|
||||
if (timeout_retries >= MAX_TIMEOUT_RETRIES) {
|
||||
LOG_W(TAG, "Recv timeout after %d retries, read %u/%u bytes", timeout_retries, (unsigned)bytesRead, (unsigned)length);
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
LOG_W(TAG, "Recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES);
|
||||
vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Exponential backoff
|
||||
continue;
|
||||
}
|
||||
if (bytes_received <= 0) {
|
||||
LOG_W(TAG, "Received error %d after reading %u/%u bytes", bytes_received, (unsigned)bytesRead, (unsigned)length);
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Successful read - reset timeout counter
|
||||
timeout_retries = 0;
|
||||
bytesRead += bytes_received;
|
||||
}
|
||||
|
||||
return std::unique_ptr<char[]>(buffer);
|
||||
}
|
||||
|
||||
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: ";
|
||||
@@ -164,69 +37,4 @@ std::map<std::string, std::string> parseContentDisposition(const std::vector<std
|
||||
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 (buffer == nullptr || 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;
|
||||
}
|
||||
|
||||
size_t receiveFile(httpd_req_t* request, size_t length, const std::string& filePath) {
|
||||
constexpr auto BUFFER_SIZE = 512;
|
||||
char buffer[BUFFER_SIZE];
|
||||
size_t bytes_received = 0;
|
||||
|
||||
auto* file = fopen(filePath.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
LOG_E(TAG, "Failed to open file for writing: %s", filePath.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr int MAX_TIMEOUT_RETRIES = 5;
|
||||
int timeout_retries = 0;
|
||||
while (bytes_received < length) {
|
||||
auto expected_chunk_size = std::min<size_t>(BUFFER_SIZE, length - bytes_received);
|
||||
int received = httpd_req_recv(request, buffer, expected_chunk_size);
|
||||
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
// Timeout - retry with backoff, same as receiveByteArray(). A large file takes many
|
||||
// more chunks (and much longer overall) than the small reads elsewhere in this file,
|
||||
// so it's far more likely to hit at least one transient stall somewhere along the way.
|
||||
timeout_retries++;
|
||||
if (timeout_retries >= MAX_TIMEOUT_RETRIES) {
|
||||
LOG_E(TAG, "Recv timeout after %d retries, wrote %zu/%zu bytes", timeout_retries, bytes_received, length);
|
||||
break;
|
||||
}
|
||||
LOG_W(TAG, "Recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES);
|
||||
vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Exponential backoff
|
||||
continue;
|
||||
}
|
||||
if (received <= 0) {
|
||||
LOG_E(TAG, "Receive failed, got 0 bytes but expected %zu more", length - bytes_received);
|
||||
break;
|
||||
}
|
||||
timeout_retries = 0;
|
||||
size_t receive_chunk_size = (size_t)received;
|
||||
|
||||
if (fwrite(buffer, 1, receive_chunk_size, file) != receive_chunk_size) {
|
||||
LOG_E(TAG, "Failed to write all bytes");
|
||||
break;
|
||||
}
|
||||
bytes_received += receive_chunk_size;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
Reference in New Issue
Block a user