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
@@ -120,9 +120,7 @@ namespace service {
|
||||
// Primary
|
||||
namespace audio { extern const ServiceManifest manifest; }
|
||||
namespace wifi { extern const ServiceManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace development { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||
namespace espnow { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
@@ -137,9 +135,7 @@ namespace service {
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
namespace screenshot { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace webserver { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -189,10 +185,10 @@ namespace app {
|
||||
namespace wificonnect { extern const ::AppManifest manifest; }
|
||||
namespace wifimanage { extern const ::AppManifest manifest; }
|
||||
|
||||
namespace webserversettings { extern const ::AppManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace apwebserver { extern const ::AppManifest manifest; }
|
||||
namespace crashdiagnostics { extern const ::AppManifest manifest; }
|
||||
namespace webserversettings { extern const ::AppManifest manifest; }
|
||||
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
||||
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
||||
#endif
|
||||
@@ -251,11 +247,11 @@ static void registerInternalApps() {
|
||||
app_manager_add(&app::wificonnect::manifest);
|
||||
app_manager_add(&app::wifimanage::manifest);
|
||||
|
||||
app_manager_add(&app::development::manifest);
|
||||
app_manager_add(&app::webserversettings::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
app_manager_add(&app::apwebserver::manifest);
|
||||
app_manager_add(&app::webserversettings::manifest);
|
||||
app_manager_add(&app::crashdiagnostics::manifest);
|
||||
app_manager_add(&app::development::manifest);
|
||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||
app_manager_add(&app::keyboardsettings::manifest);
|
||||
#endif
|
||||
@@ -320,16 +316,12 @@ static void registerAndStartServices() {
|
||||
addService(service::audio::manifest);
|
||||
}
|
||||
addService(service::wifi::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::development::manifest);
|
||||
#endif
|
||||
addService(service::webserver::manifest);
|
||||
|
||||
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||
addService(service::espnow::manifest);
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::webserver::manifest);
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM)
|
||||
if (device_exists_of_type(&RTC_TYPE)) {
|
||||
addService(service::rtctime::manifest);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
@@ -23,6 +21,7 @@
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace tt::app::development {
|
||||
|
||||
@@ -235,5 +234,3 @@ extern const ::AppManifest manifest = {
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@@ -218,7 +218,7 @@ void View::runFile(const std::string& file_path) {
|
||||
|
||||
if (!isExecutablePath(file_path)) {
|
||||
LOG_W(TAG, "Not executable: %s", file_path.c_str());
|
||||
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
|
||||
alertdialog::start(appInstanceId, "Run failed", "\"" + file::getLastPathSegment(file_path) + "\" is not an executable.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/settings/WebServerSettings.h>
|
||||
#include <Tactility/service/webserver/WebServerService.h>
|
||||
@@ -19,9 +17,6 @@
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
namespace tt::app::webserversettings {
|
||||
|
||||
constexpr auto* TAG = "WebServerSettingsApp";
|
||||
@@ -169,16 +164,9 @@ void updateUrlDisplay(Context* ctx) {
|
||||
url += "192.168.4.1";
|
||||
} else {
|
||||
// Station mode - try to get actual IP
|
||||
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif != nullptr) {
|
||||
esp_netif_ip_info_t ip_info;
|
||||
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
|
||||
char ip_str[16];
|
||||
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||
url += ip_str;
|
||||
} else {
|
||||
url = "Connecting...";
|
||||
}
|
||||
std::string ip = service::wifi::getIp();
|
||||
if (!ip.empty()) {
|
||||
url += ip;
|
||||
} else {
|
||||
url = "Not connected";
|
||||
}
|
||||
@@ -440,5 +428,3 @@ extern const ::AppManifest manifest = {
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/network/HttpServer.h>
|
||||
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
constexpr auto* TAG = "HttpServer";
|
||||
|
||||
static constexpr size_t INTERNAL_URI_HANDLER_COUNT = 2;
|
||||
|
||||
bool HttpServer::startInternal() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = stackSize;
|
||||
config.server_port = port;
|
||||
config.uri_match_fn = matchUri;
|
||||
config.max_uri_handlers = handlers.size() + INTERNAL_URI_HANDLER_COUNT;
|
||||
|
||||
if (httpd_start(&server, &config) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to start http server on port %u", (unsigned)port);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allRegistered = true;
|
||||
for (std::vector<httpd_uri_t>::reference handler : handlers) {
|
||||
if (httpd_register_uri_handler(server, &handler) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to register URI handler: %s", handler.uri);
|
||||
allRegistered = false;
|
||||
}
|
||||
}
|
||||
if (!allRegistered) {
|
||||
httpd_stop(server);
|
||||
server = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Started on port %u", (unsigned)config.server_port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpServer::stopInternal() {
|
||||
LOG_I(TAG, "Stopping server");
|
||||
if (server != nullptr) {
|
||||
if (httpd_stop(server) == ESP_OK) {
|
||||
server = nullptr;
|
||||
} else {
|
||||
LOG_W(TAG, "Error while stopping");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpServer::start() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (isStarted()) {
|
||||
LOG_W(TAG, "Already started");
|
||||
return true;
|
||||
}
|
||||
|
||||
return startInternal();
|
||||
}
|
||||
|
||||
void HttpServer::stop() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (!isStarted()) {
|
||||
LOG_W(TAG, "Not started");
|
||||
return;
|
||||
}
|
||||
|
||||
stopInternal();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,132 @@
|
||||
#include <Tactility/network/HttpServerReq.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
constexpr auto* TAG = "HttpServerReq";
|
||||
|
||||
bool getHeaderOrSendError(struct HttpServerRequest* request, const std::string& name, std::string& value) {
|
||||
size_t length = http_server_request_get_header(request, name.c_str(), nullptr, 0);
|
||||
if (length == 0) {
|
||||
http_server_request_send_error(request, 400, "header missing");
|
||||
return false;
|
||||
}
|
||||
value.resize(length);
|
||||
http_server_request_get_header(request, name.c_str(), value.data(), length + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(struct HttpServerRequest* request, std::string& boundary) {
|
||||
std::string content_type;
|
||||
if (!getHeaderOrSendError(request, "Content-Type", content_type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto boundary_index = content_type.find("boundary=");
|
||||
if (boundary_index == std::string::npos) {
|
||||
http_server_request_send_error(request, 400, "boundary not found in Content-Type");
|
||||
return false;
|
||||
}
|
||||
|
||||
boundary = content_type.substr(boundary_index + 9);
|
||||
boundary = boundary.substr(0, boundary.find(';'));
|
||||
// Trim any whitespace left by the ';' cut above, then unquote (RFC 2231 allows a quoted value).
|
||||
while (!boundary.empty() && boundary.back() == ' ') {
|
||||
boundary.pop_back();
|
||||
}
|
||||
if (boundary.size() >= 2 && boundary.front() == '"' && boundary.back() == '"') {
|
||||
boundary = boundary.substr(1, boundary.size() - 2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getQueryOrSendError(struct HttpServerRequest* request, std::string& query) {
|
||||
size_t length = http_server_request_get_query(request, nullptr, 0);
|
||||
if (length == 0) {
|
||||
http_server_request_send_error(request, 400, "id not specified");
|
||||
return false;
|
||||
}
|
||||
query.resize(length);
|
||||
http_server_request_get_query(request, query.data(), length + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reads exactly `length` bytes, or fails - unlike http_server_request_receive() itself, which may
|
||||
// return fewer bytes than requested per call.
|
||||
static bool receiveExact(struct HttpServerRequest* request, void* buffer, size_t length) {
|
||||
size_t total_read = 0;
|
||||
while (total_read < length) {
|
||||
int read = http_server_request_receive(request, static_cast<char*>(buffer) + total_read, length - total_read);
|
||||
if (read <= 0) {
|
||||
return false;
|
||||
}
|
||||
total_read += static_cast<size_t>(read);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bounds a client's multipart preamble (boundary + part headers): without this, a client that
|
||||
// keeps sending bytes without the terminator would make this buffer, and re-scan, unboundedly.
|
||||
constexpr size_t MAX_PREAMBLE_LENGTH = 8192;
|
||||
|
||||
std::string receiveTextUntil(struct HttpServerRequest* request, const std::string& terminator) {
|
||||
std::string result;
|
||||
while (!result.ends_with(terminator)) {
|
||||
if (result.length() >= MAX_PREAMBLE_LENGTH) {
|
||||
return "";
|
||||
}
|
||||
char byte;
|
||||
if (!receiveExact(request, &byte, 1)) {
|
||||
return "";
|
||||
}
|
||||
result += byte;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool readAndDiscardOrSendError(struct HttpServerRequest* request, const std::string& toRead) {
|
||||
std::string buffer(toRead.length(), '\0');
|
||||
if (!receiveExact(request, buffer.data(), toRead.length())) {
|
||||
http_server_request_send_error(request, 400, "failed to read discardable data");
|
||||
return false;
|
||||
}
|
||||
if (buffer != toRead) {
|
||||
http_server_request_send_error(request, 400, "discardable data mismatch");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t receiveFile(struct HttpServerRequest* request, size_t length, const std::string& filePath) {
|
||||
constexpr size_t 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;
|
||||
}
|
||||
|
||||
while (bytes_received < length) {
|
||||
size_t expected_chunk_size = std::min<size_t>(BUFFER_SIZE, length - bytes_received);
|
||||
int received = http_server_request_receive(request, buffer, expected_chunk_size);
|
||||
if (received <= 0) {
|
||||
LOG_E(TAG, "Receive failed, got 0 bytes but expected %zu more", length - bytes_received);
|
||||
break;
|
||||
}
|
||||
if (fwrite(buffer, 1, static_cast<size_t>(received), file) != static_cast<size_t>(received)) {
|
||||
LOG_E(TAG, "Failed to write all bytes");
|
||||
break;
|
||||
}
|
||||
bytes_received += static_cast<size_t>(received);
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -57,7 +57,8 @@ std::string urlEncode(const std::string& input) {
|
||||
// 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();
|
||||
unsigned int conversion_buffer;
|
||||
size_t input_length = input.length();
|
||||
|
||||
for (size_t i = 0; i < input_length; i++) {
|
||||
if (input[i] != '%') {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <app/install.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/start.h>
|
||||
@@ -9,13 +7,16 @@
|
||||
#include <Tactility/DeprecatedPaths.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/network/HttpServerReq.h>
|
||||
#include <Tactility/network/HttpdReq.h>
|
||||
#include <Tactility/network/Url.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/service/development/DevelopmentService.h>
|
||||
#include <Tactility/service/development/DevelopmentSettings.h>
|
||||
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
|
||||
namespace tt::service::development {
|
||||
@@ -24,10 +25,38 @@ extern const ServiceManifest manifest;
|
||||
|
||||
constexpr auto* TAG = "DevService";
|
||||
|
||||
DevelopmentService::DevelopmentService() {
|
||||
HttpServerRequestHandler handlers[] = {
|
||||
{ .uri = "/info", .method = HTTP_METHOD_GET, .callback = handleGetInfo, .user_ctx = this },
|
||||
{ .uri = "/app/run", .method = HTTP_METHOD_POST, .callback = handleAppRun, .user_ctx = this },
|
||||
{ .uri = "/app/install", .method = HTTP_METHOD_PUT, .callback = handleAppInstall, .user_ctx = this },
|
||||
{ .uri = "/app/uninstall", .method = HTTP_METHOD_PUT, .callback = handleAppUninstall, .user_ctx = this },
|
||||
};
|
||||
HttpServerConfig config {
|
||||
.port = 6666,
|
||||
.address = "0.0.0.0",
|
||||
.stack_size = 5120,
|
||||
.handlers = handlers,
|
||||
.handler_count = std::size(handlers),
|
||||
};
|
||||
httpServer = http_server_alloc(&config);
|
||||
if (httpServer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate http server");
|
||||
}
|
||||
}
|
||||
|
||||
DevelopmentService::~DevelopmentService() {
|
||||
http_server_free(httpServer);
|
||||
}
|
||||
|
||||
bool DevelopmentService::onStart(ServiceContext& service) {
|
||||
std::stringstream stream;
|
||||
stream << "{";
|
||||
#ifdef ESP_PLATFORM
|
||||
stream << "\"cpuFamily\":\"" << CONFIG_IDF_TARGET << "\", ";
|
||||
#else
|
||||
stream << "\"cpuFamily\":\"" << CONFIG_TT_DEVICE_ID << "\", ";
|
||||
#endif
|
||||
stream << "\"osVersion\":\"" << TT_VERSION << "\", ";
|
||||
stream << "\"protocolVersion\":\"1.0.0\"";
|
||||
stream << "}";
|
||||
@@ -45,61 +74,64 @@ void DevelopmentService::onStop(ServiceContext& service) {
|
||||
// region Enable/disable
|
||||
|
||||
void DevelopmentService::setEnabled(bool enabled) {
|
||||
if (httpServer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (enabled) {
|
||||
if (!httpServer.isStarted()) {
|
||||
httpServer.start();
|
||||
if (!http_server_is_started(httpServer)) {
|
||||
http_server_start(httpServer);
|
||||
}
|
||||
} else {
|
||||
if (httpServer.isStarted()) {
|
||||
httpServer.stop();
|
||||
if (http_server_is_started(httpServer)) {
|
||||
http_server_stop(httpServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DevelopmentService::isEnabled() const {
|
||||
if (httpServer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
return httpServer.isStarted();
|
||||
return http_server_is_started(httpServer);
|
||||
}
|
||||
|
||||
// region endpoints
|
||||
|
||||
esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleGetInfo(HttpServerRequest* request, void* user_ctx) {
|
||||
LOG_I(TAG, "GET /device");
|
||||
|
||||
if (httpd_resp_set_type(request, "application/json") != ESP_OK) {
|
||||
LOG_W(TAG, "Failed to send header");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto* service = static_cast<DevelopmentService*>(request->user_ctx);
|
||||
|
||||
if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) {
|
||||
auto* service = static_cast<DevelopmentService*>(user_ctx);
|
||||
http_server_request_set_content_type(request, "application/json");
|
||||
if (http_server_request_send_string(request, service->deviceResponse.c_str()) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Failed to send response body");
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "[200] /device");
|
||||
return ESP_OK;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleAppRun(HttpServerRequest* request, void*) {
|
||||
LOG_I(TAG, "POST /app/run");
|
||||
|
||||
std::string query;
|
||||
if (!network::getQueryOrSendError(request, query)) {
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
auto parameters = network::parseUrlQuery(query);
|
||||
auto id_key_pos = parameters.find("id");
|
||||
if (id_key_pos == parameters.end()) {
|
||||
LOG_W(TAG, "[400] /app/run id not specified");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "id not specified");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
char app_id[32];
|
||||
@@ -117,34 +149,40 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
app_start(id_key_pos->second.c_str(), 0, nullptr, &instance_id);
|
||||
|
||||
LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
|
||||
return ESP_OK;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleAppInstall(HttpServerRequest* request, void*) {
|
||||
LOG_I(TAG, "PUT /app/install");
|
||||
|
||||
std::string boundary;
|
||||
if (!network::getMultiPartBoundaryOrSendError(request, boundary)) {
|
||||
return false;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
size_t content_left = request->content_len;
|
||||
size_t content_left = http_server_request_get_content_length(request);
|
||||
|
||||
// Skip newline after reading boundary
|
||||
auto content_headers_data = network::receiveTextUntil(request, "\r\n\r\n");
|
||||
if (content_headers_data.empty()) {
|
||||
http_server_request_send_error(request, 400, "Multipart form error: preamble too long or unterminated");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
content_left -= content_headers_data.length();
|
||||
auto content_headers = string::split(content_headers_data, "\r\n")
|
||||
| std::views::filter([](const std::string& line) {
|
||||
return line.length() > 0;
|
||||
})
|
||||
| std::ranges::to<std::vector>();
|
||||
auto content_header_lines = string::split(content_headers_data, "\r\n");
|
||||
std::vector<std::string> content_headers;
|
||||
for (auto& line : content_header_lines) {
|
||||
if (!line.empty()) {
|
||||
content_headers.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
auto content_disposition_map = network::parseContentDisposition(content_headers);
|
||||
if (content_disposition_map.empty()) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: invalid content disposition");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "Multipart form error: invalid content disposition");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
auto name_entry = content_disposition_map.find("name");
|
||||
@@ -154,8 +192,8 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
filename_entry == content_disposition_map.end() ||
|
||||
name_entry->second != "elf"
|
||||
) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: name or filename parameter missing or mismatching");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "Multipart form error: name or filename parameter missing or mismatching");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
// Receive boundary
|
||||
@@ -165,28 +203,28 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
// Create tmp directory
|
||||
const std::string tmp_path = getTempPath();
|
||||
if (!file::findOrCreateDirectory(tmp_path, 0777)) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create temp path");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to create temp path");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
std::string safe_name = file::getLastPathSegment(filename_entry->second);
|
||||
if (safe_name.empty() || safe_name.find("..") != std::string::npos ||
|
||||
safe_name.find('/') != std::string::npos || safe_name.find('\\') != std::string::npos) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid filename");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "invalid filename");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
auto file_path = std::format("{}/{}", tmp_path, safe_name);
|
||||
if (network::receiveFile(request, file_size, file_path) != file_size) {
|
||||
file::deleteFile(file_path);
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to receive file");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
content_left -= file_size;
|
||||
|
||||
// Read and verify part
|
||||
if (!network::readAndDiscardOrSendError(request, boundary_and_newlines_after_file)) {
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
content_left -= boundary_and_newlines_after_file.length();
|
||||
|
||||
@@ -195,8 +233,8 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
}
|
||||
|
||||
if (app_install(file_path.c_str()) != ERROR_NONE) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to install");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to install");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
if (!file::deleteFile(file_path)) {
|
||||
@@ -205,42 +243,42 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
|
||||
LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str());
|
||||
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
|
||||
return ESP_OK;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppUninstall(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleAppUninstall(HttpServerRequest* request, void*) {
|
||||
LOG_I(TAG, "PUT /app/uninstall");
|
||||
|
||||
std::string query;
|
||||
if (!network::getQueryOrSendError(request, query)) {
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
auto parameters = network::parseUrlQuery(query);
|
||||
auto id_key_pos = parameters.find("id");
|
||||
if (id_key_pos == parameters.end()) {
|
||||
LOG_W(TAG, "[400] /app/uninstall id not specified");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "id not specified");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
AppManifest manifest;
|
||||
if (app_manager_find_manifest(id_key_pos->second.c_str(), &manifest) != ERROR_NONE) {
|
||||
LOG_I(TAG, "[200] /app/uninstall %s (app wasn't installed)", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (app_uninstall(id_key_pos->second.c_str()) == ERROR_NONE) {
|
||||
LOG_I(TAG, "[200] /app/uninstall %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
return ERROR_NONE;
|
||||
} else {
|
||||
LOG_W(TAG, "[500] /app/uninstall %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to uninstall");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to uninstall");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,5 +296,3 @@ extern const ServiceManifest manifest = {
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/service/development/DevelopmentSettings.h>
|
||||
@@ -72,5 +71,3 @@ bool shouldEnableOnBoot() {
|
||||
return settings.enableOnBoot;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user