diff --git a/Modules/http-module/include/http/server.h b/Modules/http-module/include/http/server.h new file mode 100644 index 000000000..99ec26c18 --- /dev/null +++ b/Modules/http-module/include/http/server.h @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include +#include +#include + +#include "types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Opaque per-request handle; a handler only ever holds a pointer to one of these. */ +struct HttpServerRequest; + +/** @return ERROR_NONE if the request was handled; any other value is only logged, the response + * (or its absence) is entirely up to the handler having already sent one via the + * http_server_request_send*() functions below. */ +typedef error_t (*HttpServerHandlerFn)(struct HttpServerRequest* request, void* user_ctx); + +/** + * One route: @a uri is matched against the request path (not the query string) together with + * @a method. A trailing wildcard character matches by prefix (e.g. "/fs/" + wildcard matches + * "/fs/list" and "/fs/x/y"); anything else must match exactly, same wildcard convention as + * ESP-IDF's httpd_uri_match_wildcard(). @a uri is caller-owned and must outlive the server, same + * contract as ESP-IDF's httpd_uri_t: a string literal is the usual case. + */ +struct HttpServerRequestHandler { + const char* uri; + enum HttpMethod method; + HttpServerHandlerFn callback; + void* user_ctx; +}; + +/** @a handlers is copied into the server at http_server_alloc() time; each handler's own `uri` + * pointer is not, so it must still outlive the server. */ +struct HttpServerConfig { + uint16_t port; + /** Bind address, e.g. "0.0.0.0". Caller-owned; only read during http_server_alloc(). */ + const char* address; + /** Stack size in bytes for the server's own task, where the platform backend needs one. */ + uint32_t stack_size; + const struct HttpServerRequestHandler* handlers; + size_t handler_count; +}; + +struct HttpServer; + +/** + * Allocates a server for @a config; does not start listening yet, see http_server_start(). + * @return NULL on allocation failure + */ +struct HttpServer* http_server_alloc(const struct HttpServerConfig* config); + +/** Stops the server if still running (see http_server_stop()) and frees it. */ +void http_server_free(struct HttpServer* server); + +/** + * Starts listening and serving requests. + * A request whose method+uri matches no registered handler gets a 404 response automatically. + * @retval ERROR_NONE on success, including if the server was already started + * @retval ERROR_RESOURCE the listening socket could not be created/bound + */ +error_t http_server_start(struct HttpServer* server); + +/** Stops listening and blocks until any in-flight request has finished. Safe to call when not started. */ +void http_server_stop(struct HttpServer* server); + +bool http_server_is_started(struct HttpServer* server); + +/** @return the bound port, e.g. to read back the OS-assigned port after starting with port 0. 0 if not started. */ +uint16_t http_server_get_port(struct HttpServer* server); + +// region Request + +enum HttpMethod http_server_request_get_method(struct HttpServerRequest* request); + +/** + * Copies the request's path (not including the query string, e.g. "/fs/list") into @a buffer. + * Useful from a handler registered against a wildcard route to see which concrete path matched. + * @return the path's actual length, same truncation convention as http_server_request_get_query(). + */ +size_t http_server_request_get_uri(struct HttpServerRequest* request, char* buffer, size_t buffer_size); + +/** + * Copies the request's raw query string (the part after '?', still URL-encoded, empty if none) into @a buffer. + * @return the query string's actual length, regardless of @a buffer_size. Same truncation + * convention as snprintf(): a return value >= @a buffer_size means the copy was truncated. + */ +size_t http_server_request_get_query(struct HttpServerRequest* request, char* buffer, size_t buffer_size); + +/** + * Copies the named header's value into @a buffer, case-insensitively. + * @return the header value's actual length, same truncation convention as http_server_request_get_query(); 0 (with @a buffer left untouched) if the header is absent. + */ +size_t http_server_request_get_header(struct HttpServerRequest* request, const char* name, char* buffer, size_t buffer_size); + +/** The request body's declared length (the "Content-Length" header), or 0 if absent. */ +uint64_t http_server_request_get_content_length(struct HttpServerRequest* request); + +/** + * Reads up to @a buffer_size currently-available body bytes. Blocking: waits for at least one byte, up to an internal per-call timeout. + * @return bytes read; 0 at end of body; negative on error or timeout + */ +int http_server_request_receive(struct HttpServerRequest* request, void* buffer, size_t buffer_size); + +/** Must be called before the first http_server_request_send*() call on this request, if at all. + * Defaults to 200. Has no effect once a response has started sending. */ +void http_server_request_set_status(struct HttpServerRequest* request, status_code_t status_code); + +/** Same timing as http_server_request_set_status(); defaults to "text/plain". */ +void http_server_request_set_content_type(struct HttpServerRequest* request, const char* content_type); + +/** Same timing as http_server_request_set_status(): adds one arbitrary response header. + * e.g. "Location", "Content-Disposition". + * Both @a name and @a value are copied. + */ +void http_server_request_set_header(struct HttpServerRequest* request, const char* name, const char* value); + +/** + * Sends the full response: status line, headers, then @a data as the entire body in one shot. + * Only the first call to any http_server_request_send*()/send_chunk_start() for a given request has any effect. + * @param[in] data may be NULL if @a length is 0 + */ +error_t http_server_request_send(struct HttpServerRequest* request, const void* data, size_t length); + +/** Same as http_server_request_send() with @a text's length and content type "text/plain". */ +error_t http_server_request_send_string(struct HttpServerRequest* request, const char* text); + +/** Sets @a status_code, then sends @a message as a plain-text body. */ +error_t http_server_request_send_error(struct HttpServerRequest* request, int status_code, const char* message); + +/** + * Starts a chunked response: sends the status line and headers (no Content-Length; chunked + * transfer instead) without a body yet. Follow with zero or more http_server_request_send_chunk() calls, + * then exactly one http_server_request_send_chunk_end(). Useful for streaming a file whose size you don't + * want to (or can't cheaply) compute up front. Only the first call to any + * http_server_request_send*()/send_chunk_start() for a given request has any effect. + */ +error_t http_server_request_send_chunk_start(struct HttpServerRequest* request); + +/** Sends one chunk of a response started with http_server_request_send_chunk_start(). */ +error_t http_server_request_send_chunk(struct HttpServerRequest* request, const void* data, size_t length); + +/** Terminates a chunked response started with http_server_request_send_chunk_start(). */ +error_t http_server_request_send_chunk_end(struct HttpServerRequest* request); + +// endregion + +#ifdef __cplusplus +} +#endif diff --git a/Modules/http-module/include/http/types.h b/Modules/http-module/include/http/types.h new file mode 100644 index 000000000..4cecc4a4c --- /dev/null +++ b/Modules/http-module/include/http/types.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// esp_http_client.h defines the same unscoped HTTP_METHOD_GET/POST/PUT/DELETE names. +// module.cpp avoids the clash by forward-declaring this enum instead of including this header. +// The fixed underlying type (C++ only) is what makes that forward declaration legal. +#ifdef __cplusplus +enum HttpMethod : int { +#else +enum HttpMethod { +#endif + HTTP_METHOD_CONNECT, + HTTP_METHOD_DELETE, + HTTP_METHOD_GET, + HTTP_METHOD_HEAD, + HTTP_METHOD_OPTIONS, + HTTP_METHOD_POST, + HTTP_METHOD_PATCH, + HTTP_METHOD_PUT, + HTTP_METHOD_TRACE, +}; + +/** An HTTP response status code, e.g. 200 or 404. */ +typedef uint16_t status_code_t; + +/** @return @a method's wire form, e.g. "GET" for HTTP_METHOD_GET. */ +const char* http_method_to_string(enum HttpMethod method); + +/** + * Parses @a text (e.g. the method token off a request line) into @a out_method. + * @retval ERROR_NONE on success + * @retval ERROR_NOT_FOUND @a text does not match any HttpMethod + */ +error_t http_method_from_string(const char* text, enum HttpMethod* out_method); + +/** + * @warning Not all status codes are implemented, so check the return value + * @param[out] text @a code's standard reason phrase, e.g. "OK" for 200; only set on success + * @retval ERROR_NONE on success + * @retval ERROR_NOT_FOUND @a code has no known reason phrase + */ +error_t status_code_to_string(status_code_t code, const char** text); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/http-module/source/module.cpp b/Modules/http-module/source/module.cpp index b04d929db..fbb9f70e4 100644 --- a/Modules/http-module/source/module.cpp +++ b/Modules/http-module/source/module.cpp @@ -2,6 +2,11 @@ #include #include +#include + +#include +#include + #ifdef ESP_PLATFORM #include #include @@ -14,16 +19,74 @@ #endif #endif - #include extern "C" { +enum HttpMethod : int; +typedef uint16_t status_code_t; +struct HttpServerRequest; +struct HttpServer; +struct HttpServerConfig; + +// Deliberately not #include or : their HttpMethod enum shares +// enumerator names with esp_http_client.h's own, so this file forward-declares exactly the +// functions it needs instead of including either header. +struct HttpServer* http_server_alloc(const struct HttpServerConfig* config); +void http_server_free(struct HttpServer* server); +error_t http_server_start(struct HttpServer* server); +void http_server_stop(struct HttpServer* server); +bool http_server_is_started(struct HttpServer* server); +uint16_t http_server_get_port(struct HttpServer* server); +enum HttpMethod http_server_request_get_method(struct HttpServerRequest* request); +size_t http_server_request_get_uri(struct HttpServerRequest* request, char* buffer, size_t buffer_size); +size_t http_server_request_get_query(struct HttpServerRequest* request, char* buffer, size_t buffer_size); +size_t http_server_request_get_header(struct HttpServerRequest* request, const char* name, char* buffer, size_t buffer_size); +uint64_t http_server_request_get_content_length(struct HttpServerRequest* request); +int http_server_request_receive(struct HttpServerRequest* request, void* buffer, size_t buffer_size); +void http_server_request_set_status(struct HttpServerRequest* request, status_code_t status_code); +void http_server_request_set_content_type(struct HttpServerRequest* request, const char* content_type); +void http_server_request_set_header(struct HttpServerRequest* request, const char* name, const char* value); +error_t http_server_request_send(struct HttpServerRequest* request, const void* data, size_t length); +error_t http_server_request_send_string(struct HttpServerRequest* request, const char* text); +error_t http_server_request_send_error(struct HttpServerRequest* request, int status_code, const char* message); +error_t http_server_request_send_chunk_start(struct HttpServerRequest* request); +error_t http_server_request_send_chunk(struct HttpServerRequest* request, const void* data, size_t length); +error_t http_server_request_send_chunk_end(struct HttpServerRequest* request); +const char* http_method_to_string(enum HttpMethod method); +error_t http_method_from_string(const char* text, enum HttpMethod* out_method); +error_t status_code_to_string(status_code_t code, const char** text); + static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(http_download_subscribe), DEFINE_MODULE_SYMBOL(http_download_unsubscribe), DEFINE_MODULE_SYMBOL(http_download_poll), DEFINE_MODULE_SYMBOL(http_download_start), DEFINE_MODULE_SYMBOL(http_download_cancel), + DEFINE_MODULE_SYMBOL(http_server_alloc), + DEFINE_MODULE_SYMBOL(http_server_free), + DEFINE_MODULE_SYMBOL(http_server_start), + DEFINE_MODULE_SYMBOL(http_server_stop), + DEFINE_MODULE_SYMBOL(http_server_is_started), + DEFINE_MODULE_SYMBOL(http_server_get_port), + DEFINE_MODULE_SYMBOL(http_server_request_get_method), + DEFINE_MODULE_SYMBOL(http_server_request_get_query), + DEFINE_MODULE_SYMBOL(http_server_request_get_header), + DEFINE_MODULE_SYMBOL(http_server_request_get_content_length), + DEFINE_MODULE_SYMBOL(http_server_request_receive), + DEFINE_MODULE_SYMBOL(http_server_request_set_status), + DEFINE_MODULE_SYMBOL(http_server_request_set_content_type), + DEFINE_MODULE_SYMBOL(http_server_request_send), + DEFINE_MODULE_SYMBOL(http_server_request_send_string), + DEFINE_MODULE_SYMBOL(http_server_request_send_error), + DEFINE_MODULE_SYMBOL(http_server_request_get_uri), + DEFINE_MODULE_SYMBOL(http_server_request_set_header), + DEFINE_MODULE_SYMBOL(http_server_request_send_chunk_start), + DEFINE_MODULE_SYMBOL(http_server_request_send_chunk), + DEFINE_MODULE_SYMBOL(http_server_request_send_chunk_end), + // types + DEFINE_MODULE_SYMBOL(http_method_to_string), + DEFINE_MODULE_SYMBOL(http_method_from_string), + DEFINE_MODULE_SYMBOL(status_code_to_string), // posix DEFINE_MODULE_SYMBOL(select), #ifdef ESP_PLATFORM @@ -94,7 +157,7 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(esp_http_client_get_url), DEFINE_MODULE_SYMBOL(esp_http_client_get_chunk_length), #endif - MODULE_SYMBOL_TERMINATOR + MODULE_SYMBOL_TERMINATOR, }; Module http_module = { diff --git a/Modules/http-module/source/server.cpp b/Modules/http-module/source/server.cpp new file mode 100644 index 000000000..baa64575a --- /dev/null +++ b/Modules/http-module/source/server.cpp @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr auto* TAG = "http-server"; + +constexpr uint32_t DEFAULT_STACK_SIZE = 5120; +constexpr int LISTEN_BACKLOG = 8; +// How often the accept loop wakes to re-check stop_requested; not a per-request timeout. +constexpr int ACCEPT_POLL_TIMEOUT_MS = 200; +// Applied to every accepted connection's socket, for both header/body reads. +constexpr int CONNECTION_RECEIVE_TIMEOUT_MS = 5000; +// Total wall-clock budget for one connection's request line + headers, independent of the +// per-recv timeout above: a client trickling one byte at a time never trips that timeout but +// would otherwise stall the single-threaded accept loop (and http_server_stop()) indefinitely. +constexpr int CONNECTION_TOTAL_TIMEOUT_MS = 10000; +constexpr size_t MAX_LINE_LENGTH = 8192; +constexpr size_t MAX_HEADER_COUNT = 32; + +// The FreeRTOS POSIX port's tick signal can interrupt a blocking syscall on the thread it targets +// (observed on the socket calls below), so every blocking recv()/send() here retries on EINTR +// rather than treating it as a real error or an orderly close. +ssize_t receive_retry(int socket_fd, void* buffer, size_t size) { + ssize_t result; + do { + result = recv(socket_fd, buffer, size, 0); + } while (result < 0 && errno == EINTR); + return result; +} + +ssize_t send_retry(int socket_fd, const void* buffer, size_t size) { + size_t total_sent = 0; + while (total_sent < size) { + ssize_t sent = send(socket_fd, static_cast(buffer) + total_sent, size - total_sent, 0); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return sent; + } + total_sent += static_cast(sent); + } + return static_cast(total_sent); +} + +// Bounded, byte-at-a-time (same technique HttpdReq.cpp already uses for the ESP32 backend's own +// multipart parsing) since request lines/headers are short and this isn't a hot path. +bool read_line(int socket_fd, std::string& out_line, TickType_t deadline) { + out_line.clear(); + char byte; + while (out_line.size() < MAX_LINE_LENGTH) { + if (xTaskGetTickCount() >= deadline) { + return false; // Connection exceeded its total budget. + } + ssize_t received = receive_retry(socket_fd, &byte, 1); + if (received <= 0) { + return false; + } + if (byte == '\n') { + if (!out_line.empty() && out_line.back() == '\r') { + out_line.pop_back(); + } + return true; + } + out_line.push_back(byte); + } + return false; // Line exceeded MAX_LINE_LENGTH without a terminator. +} + +} // namespace + +struct HttpServerRequest { + int socket_fd; + HttpMethod method; + std::string path; // e.g. "/fs/list"; not including the query string + std::string query; // still URL-encoded, without the leading '?' + std::vector> headers; + uint64_t content_length = 0; + uint64_t body_remaining = 0; + + status_code_t status_code = 200; + std::string content_type = "text/plain"; + std::vector> extra_headers; + bool response_sent = false; + bool chunked = false; +}; + +struct HttpServer { + std::string address; + uint16_t configured_port = 0; + uint32_t stack_size = DEFAULT_STACK_SIZE; + std::vector handlers; + + Mutex mutex {}; + int listen_fd = -1; + uint16_t bound_port = 0; + volatile bool stop_requested = false; + volatile bool running = false; + SemaphoreHandle_t stopped_semaphore = nullptr; + + HttpServer() { mutex_construct(&mutex); } + ~HttpServer() { mutex_destruct(&mutex); } +}; + +namespace { + +// A caller-supplied header name/value/content-type reaching here unfiltered (e.g. a decoded +// upload filename) could otherwise inject extra header lines or split the response. +bool contains_crlf(const char* text) { + return strpbrk(text, "\r\n") != nullptr; +} + +const std::pair* find_header(const HttpServerRequest* request, const char* name) { + for (const auto& header : request->headers) { + if (strcasecmp(header.first.c_str(), name) == 0) { + return &header; + } + } + return nullptr; +} + +// Status line, Content-Type, extra headers, then either Content-Length or (if @a chunked) +// Transfer-Encoding: chunked, terminated by the blank line that starts the body. +std::string build_response_prologue(HttpServerRequest* request, bool chunked, size_t content_length) { + const char* status_code_text; + if (status_code_to_string(request->status_code, &status_code_text) != ERROR_NONE) status_code_text = "Unknown"; + std::string result = "HTTP/1.1 " + std::to_string(request->status_code) + " " + status_code_text + "\r\n"; + result += "Content-Type: " + request->content_type + "\r\n"; + if (chunked) { + result += "Transfer-Encoding: chunked\r\n"; + } else { + result += "Content-Length: " + std::to_string(content_length) + "\r\n"; + } + for (const auto& header : request->extra_headers) { + result += header.first + ": " + header.second + "\r\n"; + } + result += "Connection: close\r\n\r\n"; + return result; +} + +// Reads the request line + headers off `client_fd`, dispatches to the matching handler (or a +// built-in 404/400), and guarantees a response is sent even if the handler didn't send one. +void handle_connection(HttpServer* server, int client_fd) { + timeval receive_timeout { + .tv_sec = CONNECTION_RECEIVE_TIMEOUT_MS / 1000, + .tv_usec = (CONNECTION_RECEIVE_TIMEOUT_MS % 1000) * 1000, + }; + setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, sizeof(receive_timeout)); + + TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(CONNECTION_TOTAL_TIMEOUT_MS); + + std::string request_line; + if (!read_line(client_fd, request_line, deadline)) { + return; // Malformed/empty request, or the connection's total budget ran out. + } + + size_t first_space = request_line.find(' '); + size_t second_space = request_line.find(' ', first_space == std::string::npos ? std::string::npos : first_space + 1); + if (first_space == std::string::npos || second_space == std::string::npos) { + return; + } + + HttpServerRequest request {}; + request.socket_fd = client_fd; + auto method_text = request_line.substr(0, first_space); + if (http_method_from_string(method_text.c_str(), &request.method) != ERROR_NONE) { + return; + } + + std::string target = request_line.substr(first_space + 1, second_space - first_space - 1); + size_t query_start = target.find('?'); + request.path = query_start == std::string::npos ? target : target.substr(0, query_start); + if (query_start != std::string::npos) { + request.query = target.substr(query_start + 1); + } + const std::string& path = request.path; + + std::string header_line; + bool headers_complete = false; + while (read_line(client_fd, header_line, deadline)) { + if (header_line.empty()) { + headers_complete = true; + break; + } + if (request.headers.size() >= MAX_HEADER_COUNT) { + continue; // Cap reached: known callers only need the first handful (Content-Type etc). + } + size_t colon = header_line.find(':'); + if (colon == std::string::npos) { + continue; + } + std::string name = header_line.substr(0, colon); + size_t value_start = header_line.find_first_not_of(' ', colon + 1); + std::string value = value_start == std::string::npos ? "" : header_line.substr(value_start); + request.headers.emplace_back(std::move(name), std::move(value)); + } + if (!headers_complete) { + return; // Connection closed/timed out before the terminating blank line: never dispatch. + } + + if (const auto* content_length_header = find_header(&request, "Content-Length")) { + const std::string& raw = content_length_header->second; + errno = 0; + char* end = nullptr; + unsigned long long parsed = strtoull(raw.c_str(), &end, 10); + bool all_digits = !raw.empty() && raw.find_first_not_of("0123456789") == std::string::npos; + if (!all_digits || errno == ERANGE || end != raw.c_str() + raw.size()) { + http_server_request_send_error(&request, 400, "Invalid Content-Length"); + return; + } + request.content_length = parsed; + request.body_remaining = parsed; + } + + const HttpServerRequestHandler* matched = nullptr; + for (const auto& handler : server->handlers) { + if (handler.method != request.method) { + continue; + } + size_t handler_uri_length = strlen(handler.uri); + bool is_wildcard = handler_uri_length > 0 && handler.uri[handler_uri_length - 1] == '*'; + bool matches = is_wildcard + ? path.compare(0, handler_uri_length - 1, handler.uri, handler_uri_length - 1) == 0 + : path == handler.uri; + if (matches) { + matched = &handler; + break; + } + } + + if (matched == nullptr) { + http_server_request_send_error(&request, 404, "Not Found"); + return; + } + + matched->callback(&request, matched->user_ctx); + + if (!request.response_sent) { + LOG_W(TAG, "Handler for %s did not send a response", matched->uri); + http_server_request_send_error(&request, 500, "Handler did not send a response"); + } +} + +void server_task_main(void* raw_server) { + auto* server = static_cast(raw_server); + LOG_I(TAG, "Listening on port %u", static_cast(server->bound_port)); + + while (!server->stop_requested) { + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(server->listen_fd, &read_fds); + timeval timeout { + .tv_sec = 0, + .tv_usec = ACCEPT_POLL_TIMEOUT_MS * 1000, + }; + + int ready = select(server->listen_fd + 1, &read_fds, nullptr, nullptr, &timeout); + if (ready <= 0) { + continue; // Timeout or interrupted: loop back around to re-check stop_requested. + } + + int client_fd = accept(server->listen_fd, nullptr, nullptr); + if (client_fd < 0) { + continue; + } + handle_connection(server, client_fd); + close(client_fd); + } + + xSemaphoreGive(server->stopped_semaphore); + vTaskDelete(nullptr); +} + +} // namespace + +extern "C" { + +HttpServer* http_server_alloc(const HttpServerConfig* config) { + auto* server = new (std::nothrow) HttpServer(); + if (server == nullptr) { + return nullptr; + } + + server->address = config->address != nullptr ? config->address : "0.0.0.0"; + server->configured_port = config->port; + server->stack_size = config->stack_size != 0 ? config->stack_size : DEFAULT_STACK_SIZE; + server->handlers.assign(config->handlers, config->handlers + config->handler_count); + return server; +} + +void http_server_free(HttpServer* server) { + if (server == nullptr) { + return; + } + http_server_stop(server); + delete server; +} + +error_t http_server_start(HttpServer* server) { + mutex_lock(&server->mutex); + if (server->running) { + mutex_unlock(&server->mutex); + return ERROR_NONE; + } + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + mutex_unlock(&server->mutex); + return ERROR_RESOURCE; + } + + int reuse = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + + sockaddr_in address {}; + address.sin_family = AF_INET; + address.sin_port = htons(server->configured_port); + if (inet_pton(AF_INET, server->address.c_str(), &address.sin_addr) != 1) { + address.sin_addr.s_addr = INADDR_ANY; + } + + if (bind(fd, reinterpret_cast(&address), sizeof(address)) != 0 || + listen(fd, LISTEN_BACKLOG) != 0) { + LOG_E(TAG, "Failed to bind/listen on port %u", static_cast(server->configured_port)); + close(fd); + mutex_unlock(&server->mutex); + return ERROR_RESOURCE; + } + + socklen_t address_length = sizeof(address); + getsockname(fd, reinterpret_cast(&address), &address_length); + + server->listen_fd = fd; + server->bound_port = ntohs(address.sin_port); + server->stop_requested = false; + server->stopped_semaphore = xSemaphoreCreateBinary(); + if (server->stopped_semaphore == nullptr) { + close(fd); + server->listen_fd = -1; + mutex_unlock(&server->mutex); + return ERROR_RESOURCE; + } + + TaskHandle_t task_handle = nullptr; + if (xTaskCreate(server_task_main, "http-server", server->stack_size / sizeof(StackType_t), server, tskIDLE_PRIORITY + 1, &task_handle) != pdPASS) { + close(fd); + server->listen_fd = -1; + vSemaphoreDelete(server->stopped_semaphore); + server->stopped_semaphore = nullptr; + mutex_unlock(&server->mutex); + return ERROR_RESOURCE; + } + + server->running = true; + mutex_unlock(&server->mutex); + return ERROR_NONE; +} + +void http_server_stop(HttpServer* server) { + mutex_lock(&server->mutex); + if (!server->running) { + mutex_unlock(&server->mutex); + return; + } + server->stop_requested = true; + SemaphoreHandle_t semaphore = server->stopped_semaphore; + mutex_unlock(&server->mutex); + + // Not held while waiting: the task itself never touches `mutex`, so this is only about not + // blocking a concurrent http_server_is_started()/get_port() call for the whole shutdown. + xSemaphoreTake(semaphore, portMAX_DELAY); + + mutex_lock(&server->mutex); + close(server->listen_fd); + server->listen_fd = -1; + vSemaphoreDelete(server->stopped_semaphore); + server->stopped_semaphore = nullptr; + server->running = false; + mutex_unlock(&server->mutex); +} + +bool http_server_is_started(HttpServer* server) { + mutex_lock(&server->mutex); + bool started = server->running; + mutex_unlock(&server->mutex); + return started; +} + +uint16_t http_server_get_port(HttpServer* server) { + mutex_lock(&server->mutex); + uint16_t port = server->running ? server->bound_port : 0; + mutex_unlock(&server->mutex); + return port; +} + +HttpMethod http_server_request_get_method(HttpServerRequest* request) { + return request->method; +} + +size_t http_server_request_get_uri(HttpServerRequest* request, char* buffer, size_t buffer_size) { + if (buffer_size > 0) { + snprintf(buffer, buffer_size, "%s", request->path.c_str()); + } + return request->path.size(); +} + +size_t http_server_request_get_query(HttpServerRequest* request, char* buffer, size_t buffer_size) { + if (buffer_size > 0) { + snprintf(buffer, buffer_size, "%s", request->query.c_str()); + } + return request->query.size(); +} + +size_t http_server_request_get_header(HttpServerRequest* request, const char* name, char* buffer, size_t buffer_size) { + const auto* header = find_header(request, name); + if (header == nullptr) { + return 0; + } + if (buffer_size > 0) { + snprintf(buffer, buffer_size, "%s", header->second.c_str()); + } + return header->second.size(); +} + +uint64_t http_server_request_get_content_length(HttpServerRequest* request) { + return request->content_length; +} + +int http_server_request_receive(HttpServerRequest* request, void* buffer, size_t buffer_size) { + if (request->body_remaining == 0) { + return 0; // End of the declared body. + } + if (buffer_size > request->body_remaining) { + buffer_size = static_cast(request->body_remaining); + } + ssize_t received = receive_retry(request->socket_fd, buffer, buffer_size); + if (received > 0) { + request->body_remaining -= static_cast(received); + } + return static_cast(received); +} + +void http_server_request_set_status(HttpServerRequest* request, status_code_t status_code) { + if (!request->response_sent) { + request->status_code = status_code; + } +} + +void http_server_request_set_content_type(HttpServerRequest* request, const char* content_type) { + if (request->response_sent) { + return; + } + if (contains_crlf(content_type)) { + LOG_W(TAG, "Rejected content type containing CR/LF"); + return; + } + request->content_type = content_type; +} + +void http_server_request_set_header(HttpServerRequest* request, const char* name, const char* value) { + if (request->response_sent) { + return; + } + if (contains_crlf(name) || contains_crlf(value)) { + LOG_W(TAG, "Rejected header containing CR/LF: %s", name); + return; + } + request->extra_headers.emplace_back(name, value); +} + +error_t http_server_request_send(HttpServerRequest* request, const void* data, size_t length) { + if (request->response_sent) { + return ERROR_INVALID_STATE; + } + request->response_sent = true; + + std::string prologue = build_response_prologue(request, false, length); + if (send_retry(request->socket_fd, prologue.data(), prologue.size()) < 0) { + return ERROR_RESOURCE; + } + if (length > 0) { + if (send_retry(request->socket_fd, data, length) < 0) { + return ERROR_RESOURCE; + } + } + return ERROR_NONE; +} + +error_t http_server_request_send_string(HttpServerRequest* request, const char* text) { + return http_server_request_send(request, text, strlen(text)); +} + +error_t http_server_request_send_error(HttpServerRequest* request, int status_code, const char* message) { + http_server_request_set_status(request, status_code); + return http_server_request_send_string(request, message); +} + +error_t http_server_request_send_chunk_start(HttpServerRequest* request) { + if (request->response_sent) { + return ERROR_INVALID_STATE; + } + request->response_sent = true; + request->chunked = true; + + std::string prologue = build_response_prologue(request, true, 0); + if (send_retry(request->socket_fd, prologue.data(), prologue.size()) < 0) { + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +error_t http_server_request_send_chunk(HttpServerRequest* request, const void* data, size_t length) { + if (!request->chunked) { + return ERROR_INVALID_STATE; + } + if (length == 0) { + return ERROR_NONE; // A zero-length chunk is indistinguishable from the terminator. + } + + char size_line[32]; + int size_line_length = snprintf(size_line, sizeof(size_line), "%zx\r\n", length); + if (send_retry(request->socket_fd, size_line, static_cast(size_line_length)) < 0) { + return ERROR_RESOURCE; + } + if (send_retry(request->socket_fd, data, length) < 0) { + return ERROR_RESOURCE; + } + if (send_retry(request->socket_fd, "\r\n", 2) < 0) { + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +error_t http_server_request_send_chunk_end(HttpServerRequest* request) { + if (!request->chunked) { + return ERROR_INVALID_STATE; + } + request->chunked = false; // A second call now no-ops instead of re-sending the terminator. + if (send_retry(request->socket_fd, "0\r\n\r\n", 5) < 0) { + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +} // extern "C" diff --git a/Modules/http-module/source/types.cpp b/Modules/http-module/source/types.cpp new file mode 100644 index 000000000..7371a21bc --- /dev/null +++ b/Modules/http-module/source/types.cpp @@ -0,0 +1,59 @@ +#include +#include +#include + +extern "C" { + +const char* http_method_to_string(HttpMethod method) { + switch (method) { + case HTTP_METHOD_CONNECT: + return "CONNECT"; + case HTTP_METHOD_DELETE: + return "DELETE"; + case HTTP_METHOD_GET: + return "GET"; + case HTTP_METHOD_HEAD: + return "HEAD"; + case HTTP_METHOD_OPTIONS: + return "OPTIONS"; + case HTTP_METHOD_POST: + return "POST"; + case HTTP_METHOD_PATCH: + return "PATCH"; + case HTTP_METHOD_PUT: + return "PUT"; + case HTTP_METHOD_TRACE: + return "TRACE"; + } + return "UNKNOWN"; +} + +error_t http_method_from_string(const char* text, HttpMethod* out_method) { + if (strcmp("CONNECT", text) == 0) { *out_method = HTTP_METHOD_CONNECT; return ERROR_NONE; } + if (strcmp("DELETE", text) == 0) { *out_method = HTTP_METHOD_DELETE; return ERROR_NONE; } + if (strcmp("GET", text) == 0) { *out_method = HTTP_METHOD_GET; return ERROR_NONE; } + if (strcmp("HEAD", text) == 0) { *out_method = HTTP_METHOD_HEAD; return ERROR_NONE; } + if (strcmp("OPTIONS", text) == 0) { *out_method = HTTP_METHOD_OPTIONS; return ERROR_NONE; } + if (strcmp("POST", text) == 0) { *out_method = HTTP_METHOD_POST; return ERROR_NONE; } + if (strcmp("PATCH", text) == 0) { *out_method = HTTP_METHOD_PATCH; return ERROR_NONE; } + if (strcmp("PUT", text) == 0) { *out_method = HTTP_METHOD_PUT; return ERROR_NONE; } + if (strcmp("TRACE", text) == 0) { *out_method = HTTP_METHOD_TRACE; return ERROR_NONE; } + return ERROR_NOT_FOUND; +} + +error_t status_code_to_string(status_code_t code, const char** text) { + switch (code) { + case 200: *text = "OK"; return ERROR_NONE; + case 302: *text = "Found"; return ERROR_NONE; + case 400: *text = "Bad Request"; return ERROR_NONE; + case 401: *text = "Unauthorized"; return ERROR_NONE; + case 403: *text = "Forbidden"; return ERROR_NONE; + case 404: *text = "Not Found"; return ERROR_NONE; + case 405: *text = "Method Not Allowed"; return ERROR_NONE; + case 500: *text = "Internal Server Error"; return ERROR_NONE; + case 501: *text = "Method Not Implemented"; return ERROR_NONE; + default: return ERROR_NOT_FOUND; + } +} + +} diff --git a/Modules/http-module/tests/source/server_test.cpp b/Modules/http-module/tests/source/server_test.cpp new file mode 100644 index 000000000..faa94ad48 --- /dev/null +++ b/Modules/http-module/tests/source/server_test.cpp @@ -0,0 +1,252 @@ +#include "doctest.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +// Connects to 127.0.0.1:port, sends `request` verbatim, and returns whatever the server sent +// back before closing the connection. +std::string send_request(uint16_t port, const std::string& request) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + REQUIRE(fd >= 0); + + struct sockaddr_in address {}; + address.sin_family = AF_INET; + address.sin_port = htons(port); + inet_pton(AF_INET, "127.0.0.1", &address.sin_addr); + + // The FreeRTOS POSIX port's tick signal can interrupt a blocking syscall on this thread, so + // every blocking call below retries on EINTR (matches server_posix.cpp's own recv_retry()). + int connect_result; + do { + connect_result = connect(fd, reinterpret_cast(&address), sizeof(address)); + } while (connect_result != 0 && errno == EINTR); + REQUIRE(connect_result == 0); + + // Without this, a server bug that leaves the connection open makes this block in recv() + // until CTest's/CI's external job timeout, instead of failing this test. + struct timeval receive_timeout { .tv_sec = 5, .tv_usec = 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, sizeof(receive_timeout)); + + size_t total_sent = 0; + while (total_sent < request.size()) { + ssize_t sent = send(fd, request.data() + total_sent, request.size() - total_sent, 0); + if (sent < 0) { + REQUIRE(errno == EINTR); + continue; + } + total_sent += static_cast(sent); + } + + std::string response; + char chunk[256]; + ssize_t received; + while (true) { + received = recv(fd, chunk, sizeof(chunk), 0); + if (received < 0 && errno == EINTR) { + continue; + } + if (received <= 0) { + break; + } + response.append(chunk, static_cast(received)); + } + close(fd); + return response; +} + +error_t handle_ping(struct HttpServerRequest* request, void*) { + return http_server_request_send_string(request, "pong"); +} + +error_t handle_wildcard(struct HttpServerRequest* request, void*) { + char uri[64] {}; + http_server_request_get_uri(request, uri, sizeof(uri)); + return http_server_request_send_string(request, uri); +} + +error_t handle_redirect(struct HttpServerRequest* request, void*) { + http_server_request_set_status(request, 302); + http_server_request_set_header(request, "Location", "/elsewhere"); + return http_server_request_send(request, nullptr, 0); +} + +error_t handle_chunked(struct HttpServerRequest* request, void*) { + if (http_server_request_send_chunk_start(request) != ERROR_NONE) { + return ERROR_UNDEFINED; + } + http_server_request_send_chunk(request, "one-", 4); + http_server_request_send_chunk(request, "two", 3); + http_server_request_send_chunk_end(request); + return ERROR_NONE; +} + +error_t handle_echo(struct HttpServerRequest* request, void*) { + char query[64] {}; + http_server_request_get_query(request, query, sizeof(query)); + + char body[64] {}; + size_t total_read = 0; + uint64_t content_length = http_server_request_get_content_length(request); + while (total_read < content_length && total_read < sizeof(body) - 1) { + int read = http_server_request_receive(request, body + total_read, sizeof(body) - 1 - total_read); + if (read <= 0) { + break; + } + total_read += static_cast(read); + } + + std::string response = std::string("query=") + query + " body=" + body; + return http_server_request_send_string(request, response.c_str()); +} + +} // namespace + +TEST_CASE("http_server_start binds an OS-assigned port and serves a registered handler") { + HttpServerRequestHandler handlers[] = { + { .uri = "/ping", .method = HTTP_METHOD_GET, .callback = handle_ping, .user_ctx = nullptr }, + }; + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = handlers, .handler_count = 1 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + CHECK_EQ(http_server_start(server), ERROR_NONE); + CHECK(http_server_is_started(server)); + + uint16_t port = http_server_get_port(server); + CHECK_NE(port, 0); + + std::string response = send_request(port, "GET /ping HTTP/1.1\r\nHost: x\r\n\r\n"); + CHECK_NE(response.find("200"), std::string::npos); + CHECK_NE(response.find("pong"), std::string::npos); + + http_server_free(server); +} + +TEST_CASE("http_server_start serves query string and request body to the handler") { + HttpServerRequestHandler handlers[] = { + { .uri = "/echo", .method = HTTP_METHOD_PUT, .callback = handle_echo, .user_ctx = nullptr }, + }; + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = handlers, .handler_count = 1 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + REQUIRE_EQ(http_server_start(server), ERROR_NONE); + uint16_t port = http_server_get_port(server); + + std::string body = "hello"; + std::string request = "PUT /echo?name=world HTTP/1.1\r\nHost: x\r\nContent-Length: " + std::to_string(body.size()) + "\r\n\r\n" + body; + std::string response = send_request(port, request); + CHECK_NE(response.find("query=name=world"), std::string::npos); + CHECK_NE(response.find("body=hello"), std::string::npos); + + http_server_free(server); +} + +TEST_CASE("an unmatched uri gets a 404") { + HttpServerRequestHandler handlers[] = { + { .uri = "/ping", .method = HTTP_METHOD_GET, .callback = handle_ping, .user_ctx = nullptr }, + }; + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = handlers, .handler_count = 1 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + REQUIRE_EQ(http_server_start(server), ERROR_NONE); + uint16_t port = http_server_get_port(server); + + std::string response = send_request(port, "GET /missing HTTP/1.1\r\nHost: x\r\n\r\n"); + CHECK_NE(response.find("404"), std::string::npos); + + http_server_free(server); +} + +TEST_CASE("a trailing '*' route matches by prefix and get_uri returns the matched path") { + HttpServerRequestHandler handlers[] = { + { .uri = "/fs/*", .method = HTTP_METHOD_GET, .callback = handle_wildcard, .user_ctx = nullptr }, + }; + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = handlers, .handler_count = 1 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + REQUIRE_EQ(http_server_start(server), ERROR_NONE); + uint16_t port = http_server_get_port(server); + + std::string response = send_request(port, "GET /fs/list?path=/data HTTP/1.1\r\nHost: x\r\n\r\n"); + CHECK_NE(response.find("200"), std::string::npos); + CHECK_NE(response.find("/fs/list"), std::string::npos); + // get_uri() must not include the query string. + CHECK_EQ(response.find("path=/data"), std::string::npos); + + http_server_free(server); +} + +TEST_CASE("set_status and set_header apply to the response") { + HttpServerRequestHandler handlers[] = { + { .uri = "/redirect", .method = HTTP_METHOD_GET, .callback = handle_redirect, .user_ctx = nullptr }, + }; + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = handlers, .handler_count = 1 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + REQUIRE_EQ(http_server_start(server), ERROR_NONE); + uint16_t port = http_server_get_port(server); + + std::string response = send_request(port, "GET /redirect HTTP/1.1\r\nHost: x\r\n\r\n"); + CHECK_NE(response.find("302"), std::string::npos); + CHECK_NE(response.find("Location: /elsewhere"), std::string::npos); + + http_server_free(server); +} + +TEST_CASE("a chunked response delivers all chunks concatenated") { + HttpServerRequestHandler handlers[] = { + { .uri = "/chunked", .method = HTTP_METHOD_GET, .callback = handle_chunked, .user_ctx = nullptr }, + }; + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = handlers, .handler_count = 1 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + REQUIRE_EQ(http_server_start(server), ERROR_NONE); + uint16_t port = http_server_get_port(server); + + std::string response = send_request(port, "GET /chunked HTTP/1.1\r\nHost: x\r\n\r\n"); + CHECK_NE(response.find("Transfer-Encoding: chunked"), std::string::npos); + // Wire format is "\r\n\r\n" per chunk, terminated by "0\r\n\r\n". The two + // chunks are not contiguous in the raw response, so check each piece and the framing. + CHECK_NE(response.find("4\r\none-\r\n"), std::string::npos); + CHECK_NE(response.find("3\r\ntwo\r\n"), std::string::npos); + CHECK(response.ends_with("0\r\n\r\n")); + + http_server_free(server); +} + +TEST_CASE("http_server_stop closes the listening port") { + HttpServerConfig config { .port = 0, .address = "0.0.0.0", .stack_size = 0, .handlers = nullptr, .handler_count = 0 }; + + HttpServer* server = http_server_alloc(&config); + REQUIRE(server != nullptr); + REQUIRE_EQ(http_server_start(server), ERROR_NONE); + uint16_t port = http_server_get_port(server); + + http_server_stop(server); + CHECK_FALSE(http_server_is_started(server)); + + int fd = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in address {}; + address.sin_family = AF_INET; + address.sin_port = htons(port); + inet_pton(AF_INET, "127.0.0.1", &address.sin_addr); + CHECK_NE(connect(fd, reinterpret_cast(&address), sizeof(address)), 0); + close(fd); + + http_server_free(server); +} diff --git a/Tactility/Include/Tactility/network/HttpServer.h b/Tactility/Include/Tactility/network/HttpServer.h deleted file mode 100644 index 1ae6e7ac7..000000000 --- a/Tactility/Include/Tactility/network/HttpServer.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once -#ifdef ESP_PLATFORM - -#include -#include - -namespace tt::network { - -class HttpServer { - -public: - - /** - * @brief Function for URI matching used by server. - * - * @param[in] referenceUri URI/template with respect to which the other URI is matched - * @param[in] uriToCheck URI/template being matched to the reference URI/template - * @param[in] matchUpTo For specifying the actual length of `uri_to_match` up to - * which the matching algorithm is to be applied (The maximum - * value is `strlen(uri_to_match)`, independent of the length - * of `reference_uri`) - * @return true on match - */ - typedef bool (*UriMatchFunction)(const char* referenceUri, const char* uriToCheck, size_t matchUpTo); - -private: - - const uint32_t port; - const std::string address; - const uint32_t stackSize; - const UriMatchFunction matchUri; - - std::vector handlers; - - RecursiveMutex mutex; - httpd_handle_t server = nullptr; - - bool startInternal(); - void stopInternal(); - -public: - - HttpServer( - uint32_t port, - const std::string& address, - std::vector handlers, - uint32_t stackSize = 5120, - UriMatchFunction matchUri = httpd_uri_match_wildcard - ) : - port(port), - address(address), - stackSize(stackSize), - matchUri(matchUri), - handlers(std::move(handlers)) - {} - - bool start(); - - void stop(); - - bool isStarted() const { - auto lock = mutex.asScopedLock(); - lock.lock(); - return server != nullptr; - } -}; - -} - -#endif \ No newline at end of file diff --git a/Tactility/Include/Tactility/network/HttpServerReq.h b/Tactility/Include/Tactility/network/HttpServerReq.h new file mode 100644 index 000000000..1825e0234 --- /dev/null +++ b/Tactility/Include/Tactility/network/HttpServerReq.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +// Helper functions for HttpServerRequest from http-module +namespace tt::network { + +bool getHeaderOrSendError(struct HttpServerRequest* request, const std::string& name, std::string& value); + +bool getMultiPartBoundaryOrSendError(struct HttpServerRequest* request, std::string& boundary); + +bool getQueryOrSendError(struct HttpServerRequest* request, std::string& query); + +/** @return the received text up to and including @a terminator, or "" if the connection failed + * or the preamble exceeded its bounded maximum length without finding @a terminator. */ +std::string receiveTextUntil(struct HttpServerRequest* request, const std::string& terminator); + +bool readAndDiscardOrSendError(struct HttpServerRequest* request, const std::string& toRead); + +size_t receiveFile(struct HttpServerRequest* request, size_t length, const std::string& filePath); + +} diff --git a/Tactility/Include/Tactility/network/HttpdReq.h b/Tactility/Include/Tactility/network/HttpdReq.h index 9802f4f80..690e56d3d 100644 --- a/Tactility/Include/Tactility/network/HttpdReq.h +++ b/Tactility/Include/Tactility/network/HttpdReq.h @@ -1,31 +1,12 @@ #pragma once -#ifdef ESP_PLATFORM - -#include #include -#include #include #include namespace tt::network { -bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value); - -bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary); - -bool getQueryOrSendError(httpd_req_t* request, std::string& query); - -std::unique_ptr receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead); - -std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator); - +/** Pure string parsing, no request I/O */ std::map parseContentDisposition(const std::vector& input); -bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead); - -size_t receiveFile(httpd_req_t* request, size_t length, const std::string& filePath); - } - -#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Include/Tactility/settings/WebServerSettings.h b/Tactility/Include/Tactility/settings/WebServerSettings.h index 59dabf252..c9ef9676c 100644 --- a/Tactility/Include/Tactility/settings/WebServerSettings.h +++ b/Tactility/Include/Tactility/settings/WebServerSettings.h @@ -5,6 +5,12 @@ namespace tt::settings::webserver { +#ifdef ESP_PLATFORM +constexpr uint16_t DEFAULT_PORT = 80; +#else +constexpr uint16_t DEFAULT_PORT = 8080; +#endif + enum class WiFiMode : uint8_t { Station = 0, // Connect to existing WiFi network AccessPoint = 1 // Create own WiFi network @@ -23,7 +29,7 @@ struct WebServerSettings { // Web Server Settings bool webServerEnabled = false; - uint16_t webServerPort = 80; // Default: 80 + uint16_t webServerPort = DEFAULT_PORT; // Optional HTTP Basic Auth bool webServerAuthEnabled = false; diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h index 0d74dbbd7..35220a96f 100644 --- a/Tactility/Private/Tactility/service/development/DevelopmentService.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -1,13 +1,10 @@ #pragma once -#ifdef ESP_PLATFORM #include #include -#include -#include -#include +#include namespace tt::service::development { @@ -15,47 +12,21 @@ class DevelopmentService final : public Service { RecursiveMutex mutex; std::string deviceResponse; - network::HttpServer httpServer = network::HttpServer( - 6666, - "0.0.0.0", - std::vector{ - { - .uri = "/info", - .method = HTTP_GET, - .handler = handleGetInfo, - .user_ctx = this - }, - { - .uri = "/app/run", - .method = HTTP_POST, - .handler = handleAppRun, - .user_ctx = this - }, - { - .uri = "/app/install", - .method = HTTP_PUT, - .handler = handleAppInstall, - .user_ctx = this - }, - { - .uri = "/app/uninstall", - .method = HTTP_PUT, - .handler = handleAppUninstall, - .user_ctx = this - } - } - ); + struct HttpServer* httpServer = nullptr; void startServer(); void stopServer(); - static esp_err_t handleGetInfo(httpd_req_t* request); - static esp_err_t handleAppRun(httpd_req_t* request); - static esp_err_t handleAppInstall(httpd_req_t* request); - static esp_err_t handleAppUninstall(httpd_req_t* request); + static error_t handleGetInfo(struct HttpServerRequest* request, void* user_ctx); + static error_t handleAppRun(struct HttpServerRequest* request, void* user_ctx); + static error_t handleAppInstall(struct HttpServerRequest* request, void* user_ctx); + static error_t handleAppUninstall(struct HttpServerRequest* request, void* user_ctx); public: + DevelopmentService(); + ~DevelopmentService() override; + // region Overrides bool onStart(ServiceContext& service) override; @@ -82,5 +53,3 @@ class DevelopmentService final : public Service { std::shared_ptr findService(); } - -#endif // ESP_PLATFORM diff --git a/Tactility/Private/Tactility/service/development/DevelopmentSettings.h b/Tactility/Private/Tactility/service/development/DevelopmentSettings.h index 9a934002d..d6e2f821a 100644 --- a/Tactility/Private/Tactility/service/development/DevelopmentSettings.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentSettings.h @@ -1,5 +1,4 @@ #pragma once -#ifdef ESP_PLATFORM namespace tt::service::development { @@ -8,5 +7,3 @@ void setEnableOnBoot(bool enable); bool shouldEnableOnBoot(); } - -#endif // ESP_PLATFORM diff --git a/Tactility/Private/Tactility/service/webserver/WebServerService.h b/Tactility/Private/Tactility/service/webserver/WebServerService.h index 058c16001..dcc772b27 100644 --- a/Tactility/Private/Tactility/service/webserver/WebServerService.h +++ b/Tactility/Private/Tactility/service/webserver/WebServerService.h @@ -1,13 +1,15 @@ #pragma once -#ifdef ESP_PLATFORM #include #include -#include #include -#include +#include + +#ifdef ESP_PLATFORM #include +#endif + #include namespace tt::service::webserver { @@ -33,58 +35,63 @@ enum class WebServerEvent { class WebServerService final : public Service { private: mutable RecursiveMutex mutex; - std::unique_ptr httpServer; + struct HttpServer* httpServer = nullptr; PubSub::SubscriptionHandle settingsEventSubscription = nullptr; std::shared_ptr> pubsub = std::make_shared>(); int8_t statusbarIconId = -1; // Statusbar icon for WebServer state - // AP mode WiFi management - esp_netif_t* apNetif = nullptr; + // AP mode WiFi management - real hardware only, see startApMode()/stopApMode()'s own + // non-ESP_PLATFORM definitions. bool apWifiInitialized = false; +#ifdef ESP_PLATFORM + esp_netif_t* apNetif = nullptr; +#endif bool startApMode(); void stopApMode(); // Core HTML endpoints (hardcoded in firmware) - static esp_err_t handleRoot(httpd_req_t* request); - static esp_err_t handleSync(httpd_req_t* request); - static esp_err_t handleReboot(httpd_req_t* request); + static error_t handleRoot(struct HttpServerRequest* request, void* user_ctx); + static error_t handleSync(struct HttpServerRequest* request, void* user_ctx); + static error_t handleReboot(struct HttpServerRequest* request, void* user_ctx); // File browser endpoints - static esp_err_t handleFileBrowser(httpd_req_t* request); - static esp_err_t handleFsList(httpd_req_t* request); - static esp_err_t handleFsTree(httpd_req_t* request); - static esp_err_t handleFsDownload(httpd_req_t* request); - static esp_err_t handleFsMkdir(httpd_req_t* request); - static esp_err_t handleFsDelete(httpd_req_t* request); - static esp_err_t handleFsRename(httpd_req_t* request); - static esp_err_t handleFsUpload(httpd_req_t* request); + static error_t handleFileBrowser(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsList(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsTree(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsDownload(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsMkdir(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsDelete(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsRename(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsUpload(struct HttpServerRequest* request, void* user_ctx); // Consolidated dispatch handlers to reduce URI handler table usage - static esp_err_t handleFsGenericGet(httpd_req_t* request); - static esp_err_t handleFsGenericPost(httpd_req_t* request); + static error_t handleFsGenericGet(struct HttpServerRequest* request, void* user_ctx); + static error_t handleFsGenericPost(struct HttpServerRequest* request, void* user_ctx); // Admin dispatcher to consolidate small POST endpoints (sync/reboot) - static esp_err_t handleAdminPost(httpd_req_t* request); + static error_t handleAdminPost(struct HttpServerRequest* request, void* user_ctx); // API endpoints - static esp_err_t handleApiGet(httpd_req_t* request); - static esp_err_t handleApiPost(httpd_req_t* request); - static esp_err_t handleApiPut(httpd_req_t* request); - static esp_err_t handleApiSysinfo(httpd_req_t* request); - static esp_err_t handleApiApps(httpd_req_t* request); - static esp_err_t handleApiAppsRun(httpd_req_t* request); - static esp_err_t handleApiAppsUninstall(httpd_req_t* request); - static esp_err_t handleApiAppsInstall(httpd_req_t* request); - static esp_err_t handleApiWifi(httpd_req_t* request); - static esp_err_t handleApiScreenshot(httpd_req_t* request); + static error_t handleApiGet(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiPost(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiPut(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiSysinfo(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiApps(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiAppsRun(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiAppsUninstall(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiAppsInstall(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiWifi(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiScreenshot(struct HttpServerRequest* request, void* user_ctx); // Dynamic asset serving - static esp_err_t handleAssets(httpd_req_t* request); + static error_t handleAssets(struct HttpServerRequest* request, void* user_ctx); bool startServer(); void stopServer(); public: + ~WebServerService() override; + bool onStart(ServiceContext& service) override; void onStop(ServiceContext& service) override; @@ -104,5 +111,3 @@ bool isWebServerEnabled(); std::shared_ptr> getPubsub(); } // namespace - -#endif diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index cd9f7dec1..95403594f 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -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); diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index f27c24871..058b5e24c 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -1,5 +1,3 @@ -#ifdef ESP_PLATFORM - #include #include #include @@ -23,6 +21,7 @@ #include #include +#include namespace tt::app::development { @@ -235,5 +234,3 @@ extern const ::AppManifest manifest = { }; } // namespace - -#endif // ESP_PLATFORM diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index c211a28d7..dd4c30c7c 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -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; } diff --git a/Tactility/Source/app/webserversettings/WebServerSettings.cpp b/Tactility/Source/app/webserversettings/WebServerSettings.cpp index e7753a828..ccefd0353 100644 --- a/Tactility/Source/app/webserversettings/WebServerSettings.cpp +++ b/Tactility/Source/app/webserversettings/WebServerSettings.cpp @@ -1,5 +1,3 @@ -#ifdef ESP_PLATFORM - #include #include #include @@ -19,9 +17,6 @@ #include #include -#include -#include - 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 diff --git a/Tactility/Source/network/HttpServer.cpp b/Tactility/Source/network/HttpServer.cpp deleted file mode 100644 index 05ebc4ef7..000000000 --- a/Tactility/Source/network/HttpServer.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#ifdef ESP_PLATFORM - -#include - -#include - -#include - -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::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 \ No newline at end of file diff --git a/Tactility/Source/network/HttpServerReq.cpp b/Tactility/Source/network/HttpServerReq.cpp new file mode 100644 index 000000000..658d17de3 --- /dev/null +++ b/Tactility/Source/network/HttpServerReq.cpp @@ -0,0 +1,132 @@ +#include + +#include + +#include + +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(buffer) + total_read, length - total_read); + if (read <= 0) { + return false; + } + total_read += static_cast(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(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(received), file) != static_cast(received)) { + LOG_E(TAG, "Failed to write all bytes"); + break; + } + bytes_received += static_cast(received); + } + + fclose(file); + return bytes_received; +} + +} diff --git a/Tactility/Source/network/HttpdReq.cpp b/Tactility/Source/network/HttpdReq.cpp index dfcf47342..9d2fc782c 100644 --- a/Tactility/Source/network/HttpdReq.cpp +++ b/Tactility/Source/network/HttpdReq.cpp @@ -1,137 +1,10 @@ -#include #include #include -#include - -#include #include -#include - -#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(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(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 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(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(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 parseContentDisposition(const std::vector& input) { std::map result; static std::string prefix = "Content-Disposition: "; @@ -164,69 +37,4 @@ std::map parseContentDisposition(const std::vector(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 \ No newline at end of file diff --git a/Tactility/Source/network/Url.cpp b/Tactility/Source/network/Url.cpp index cc3525d69..2c33f97e5 100644 --- a/Tactility/Source/network/Url.cpp +++ b/Tactility/Source/network/Url.cpp @@ -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] != '%') { diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp index 232c0687e..c88900c43 100644 --- a/Tactility/Source/service/development/DevelopmentService.cpp +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -1,5 +1,3 @@ -#ifdef ESP_PLATFORM - #include #include #include @@ -9,13 +7,16 @@ #include #include #include +#include #include #include #include #include #include -#include +#include +#include +#include #include 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(request->user_ctx); - - if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) { + auto* service = static_cast(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(); + auto content_header_lines = string::split(content_headers_data, "\r\n"); + std::vector 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 diff --git a/Tactility/Source/service/development/DevelopmentSettings.cpp b/Tactility/Source/service/development/DevelopmentSettings.cpp index 13da6082b..e9657e305 100644 --- a/Tactility/Source/service/development/DevelopmentSettings.cpp +++ b/Tactility/Source/service/development/DevelopmentSettings.cpp @@ -1,4 +1,3 @@ -#ifdef ESP_PLATFORM #include #include #include @@ -72,5 +71,3 @@ bool shouldEnableOnBoot() { return settings.enableOnBoot; } } - -#endif // ESP_PLATFORM diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index 529d0e404..4aad7b0df 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -1,5 +1,3 @@ -#ifdef ESP_PLATFORM - #include #include @@ -14,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -32,11 +31,7 @@ #include "app/install.h" #include "app/manager.h" - -#include -#include -#include -#include +#ifdef ESP_PLATFORM #include #include #include @@ -45,12 +40,17 @@ #include #include #include -#include -#include -#include #include +#endif + +#include +#include +#include +#include +#include +#include +#include #include -#include #include #include @@ -58,6 +58,7 @@ namespace tt::service::webserver { constexpr auto* TAG = "WebServerService"; +#ifdef ESP_PLATFORM // Helper to convert chip model enum to human-readable string static const char* getChipModelName(esp_chip_model_t model) { switch (model) { @@ -74,6 +75,7 @@ static const char* getChipModelName(esp_chip_model_t model) { default: return "Unknown"; } } +#endif // Cached settings to avoid SD card reads on every HTTP request static Mutex g_settingsMutex; @@ -111,16 +113,16 @@ static bool secureCompare(const std::string& a, const std::string& b) { } // Helper to send 401 Unauthorized response with WWW-Authenticate header -static esp_err_t sendUnauthorized(httpd_req_t* request, const char* message) { - httpd_resp_set_hdr(request, "WWW-Authenticate", "Basic realm=\"Tactility\""); - httpd_resp_send_err(request, HTTPD_401_UNAUTHORIZED, message); - return ESP_OK; // Response was sent successfully +static error_t sendUnauthorized(HttpServerRequest* request, const char* message) { + http_server_request_set_header(request, "WWW-Authenticate", "Basic realm=\"Tactility\""); + http_server_request_send_error(request, 401, message); + return ERROR_NONE; // Response was sent successfully } // Helper to validate HTTP Basic Auth on sensitive endpoints -// Returns ESP_OK with authPassed=true if auth succeeded or is disabled -// Returns ESP_OK with authPassed=false if auth failed (401 response already sent) -static esp_err_t validateRequestAuth(httpd_req_t* request, bool& authPassed) { +// Returns ERROR_NONE with authPassed=true if auth succeeded or is disabled +// Returns ERROR_NONE with authPassed=false if auth failed (401 response already sent) +static error_t validateRequestAuth(HttpServerRequest* request, bool& authPassed) { authPassed = false; // Copy settings under lock to avoid race with settings update callback @@ -133,21 +135,16 @@ static esp_err_t validateRequestAuth(httpd_req_t* request, bool& authPassed) { if (!settings.webServerAuthEnabled) { authPassed = true; - return ESP_OK; // Auth disabled, allow request + return ERROR_NONE; // Auth disabled, allow request } // Get Authorization header - size_t auth_len = httpd_req_get_hdr_value_len(request, "Authorization"); + size_t auth_len = http_server_request_get_header(request, "Authorization", nullptr, 0); if (auth_len == 0) { return sendUnauthorized(request, "Authorization required"); } - - std::string auth_header(auth_len + 1, '\0'); - if (httpd_req_get_hdr_value_str(request, "Authorization", auth_header.data(), auth_len + 1) != ESP_OK) { - LOG_W(TAG, "Failed to read Authorization header"); - return sendUnauthorized(request, "Authorization required"); - } - auth_header.resize(auth_len); // Remove null terminator from string length + std::string auth_header(auth_len, '\0'); + http_server_request_get_header(request, "Authorization", auth_header.data(), auth_len + 1); // Check for "Basic " prefix if (auth_header.rfind("Basic ", 0) != 0) { @@ -196,7 +193,7 @@ static esp_err_t validateRequestAuth(httpd_req_t* request, bool& authPassed) { } authPassed = true; - return ESP_OK; // Auth successful + return ERROR_NONE; // Auth successful } bool WebServerService::onStart(ServiceContext& service) { @@ -262,11 +259,11 @@ void WebServerService::setEnabled(bool enabled) { lock.lock(); if (enabled) { - if (!httpServer || !httpServer->isStarted()) { + if (httpServer == nullptr) { startServer(); } } else { - if (httpServer && httpServer->isStarted()) { + if (httpServer != nullptr) { stopServer(); } } @@ -275,11 +272,13 @@ void WebServerService::setEnabled(bool enabled) { bool WebServerService::isEnabled() const { auto lock = mutex.asScopedLock(); lock.lock(); - return httpServer && httpServer->isStarted(); + return httpServer != nullptr; } // region AP Mode WiFi Management +#ifdef ESP_PLATFORM + bool WebServerService::startApMode() { // Copy settings locally settings::webserver::WebServerSettings settings; @@ -416,6 +415,17 @@ void WebServerService::stopApMode() { } } +#else + +bool WebServerService::startApMode() { + LOG_W(TAG, "AP mode WiFi is not supported on this platform"); + return false; +} + +void WebServerService::stopApMode() {} + +#endif // ESP_PLATFORM + // endregion bool WebServerService::startServer() { @@ -437,78 +447,81 @@ bool WebServerService::startServer() { // NOTE: If you see 'no slots left for registering handler', increase CONFIG_HTTPD_MAX_URI_HANDLERS in sdkconfig (default is 8, 16+ recommended for many endpoints) void* ctx = this; // Avoid IDE warnings about 'this' in designated initializers - std::vector handlers = { + HttpServerRequestHandler handlers[] = { { .uri = "/", - .method = HTTP_GET, - .handler = handleRoot, + .method = HTTP_METHOD_GET, + .callback = handleRoot, .user_ctx = ctx }, // Note: /upload removed in favor of POST /fs/upload handled by /fs/* dispatcher { .uri = "/filebrowser", - .method = HTTP_GET, - .handler = handleFileBrowser, + .method = HTTP_METHOD_GET, + .callback = handleFileBrowser, .user_ctx = ctx }, // Consolidated /fs/* handlers (dispatch internally) to save uri handler slots { .uri = "/fs/*", - .method = HTTP_GET, - .handler = handleFsGenericGet, + .method = HTTP_METHOD_GET, + .callback = handleFsGenericGet, .user_ctx = ctx }, { .uri = "/fs/*", - .method = HTTP_POST, - .handler = handleFsGenericPost, + .method = HTTP_METHOD_POST, + .callback = handleFsGenericPost, .user_ctx = ctx }, // Consolidated admin POST endpoints to save handler slots { .uri = "/admin/*", - .method = HTTP_POST, - .handler = handleAdminPost, + .method = HTTP_METHOD_POST, + .callback = handleAdminPost, .user_ctx = ctx }, // API endpoints for system info, apps, wifi, etc { .uri = "/api/*", - .method = HTTP_GET, - .handler = handleApiGet, + .method = HTTP_METHOD_GET, + .callback = handleApiGet, .user_ctx = ctx }, { .uri = "/api/*", - .method = HTTP_POST, - .handler = handleApiPost, + .method = HTTP_METHOD_POST, + .callback = handleApiPost, .user_ctx = ctx }, { .uri = "/api/*", - .method = HTTP_PUT, - .handler = handleApiPut, + .method = HTTP_METHOD_PUT, + .callback = handleApiPut, .user_ctx = ctx }, { .uri = "/*", // Catch-all for dynamic assets - .method = HTTP_GET, - .handler = handleAssets, + .method = HTTP_METHOD_GET, + .callback = handleAssets, .user_ctx = ctx } }; - - httpServer = std::make_unique( - settings.webServerPort, - "0.0.0.0", - handlers, - 8192 // Stack size - ); - - httpServer->start(); - if (!httpServer->isStarted()) { + + HttpServerConfig config { + .port = settings.webServerPort, + .address = "0.0.0.0", + .stack_size = 8192, + .handlers = handlers, + .handler_count = std::size(handlers), + }; + + httpServer = http_server_alloc(&config); + if (httpServer == nullptr || http_server_start(httpServer) != ERROR_NONE) { LOG_E(TAG, "Failed to start HTTP server on port %u", (unsigned)settings.webServerPort); - httpServer.reset(); + http_server_free(httpServer); + httpServer = nullptr; + stopApMode(); return false; } @@ -527,15 +540,19 @@ bool WebServerService::startServer() { } void WebServerService::stopServer() { - if (!httpServer) { + if (httpServer == nullptr) { return; } - httpServer->stop(); - httpServer.reset(); + http_server_free(httpServer); + httpServer = nullptr; // Stop AP mode WiFi if we started it - if (apWifiInitialized || apNetif != nullptr) { + if (apWifiInitialized +#ifdef ESP_PLATFORM + || apNetif != nullptr +#endif + ) { stopApMode(); } @@ -547,15 +564,17 @@ void WebServerService::stopServer() { } } -// region Endpoints - +WebServerService::~WebServerService() { + http_server_free(httpServer); +} +// region Endpoints -esp_err_t WebServerService::handleRoot(httpd_req_t* request) { +error_t WebServerService::handleRoot(HttpServerRequest* request, void*) { LOG_I(TAG, "GET / -> redirecting to /dashboard.html"); - httpd_resp_set_status(request, "302 Found"); - httpd_resp_set_hdr(request, "Location", "/dashboard.html"); - return httpd_resp_send(request, nullptr, 0); + http_server_request_set_status(request, 302); + http_server_request_set_header(request, "Location", "/dashboard.html"); + return http_server_request_send(request, nullptr, 0); } // region File Browser helpers & handlers @@ -704,16 +723,29 @@ static std::string escapeJson(const std::string& s) { return o.str(); } -static bool getQueryParam(httpd_req_t* req, const char* key, std::string& out) { - size_t len = httpd_req_get_url_query_len(req) + 1; - if (len <= 1) return false; - std::unique_ptr buf(new char[len]); - if (httpd_req_get_url_query_str(req, buf.get(), len) != ESP_OK) return false; - // Allocate buffer large enough for the entire query string (worst case) - std::unique_ptr value(new char[len]); - if (httpd_query_key_value(buf.get(), key, value.get(), len) == ESP_OK) { - out = value.get(); - return true; +// Raw (not URL-decoded) extraction, matching ESP-IDF's httpd_query_key_value() semantics - +// callers that need decoding (e.g. normalizePath()) already do it themselves on the raw value. +static bool getQueryParam(HttpServerRequest* request, const char* key, std::string& out) { + size_t length = http_server_request_get_query(request, nullptr, 0); + if (length == 0) { + return false; + } + std::string query(length, '\0'); + http_server_request_get_query(request, query.data(), length + 1); + + size_t pos = 0; + while (pos < query.size()) { + size_t amp = query.find('&', pos); + size_t pair_end = amp == std::string::npos ? query.size() : amp; + size_t eq = query.find('=', pos); + if (eq != std::string::npos && eq < pair_end && query.compare(pos, eq - pos, key) == 0) { + out = query.substr(eq + 1, pair_end - eq - 1); + return true; + } + if (amp == std::string::npos) { + break; + } + pos = amp + 1; } return false; } @@ -723,22 +755,19 @@ static bool uriMatches(const char* uri, const char* route) { return strncmp(uri, route, n) == 0 && (uri[n] == '\0' || uri[n] == '?' || uri[n] == '/'); } -esp_err_t WebServerService::handleFileBrowser(httpd_req_t* request) { +error_t WebServerService::handleFileBrowser(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /filebrowser -> redirecting to /dashboard.html#files"); - httpd_resp_set_status(request, "302 Found"); - httpd_resp_set_hdr(request, "Location", "/dashboard.html#files"); - return httpd_resp_send(request, nullptr, 0); + http_server_request_set_status(request, 302); + http_server_request_set_header(request, "Location", "/dashboard.html#files"); + return http_server_request_send(request, nullptr, 0); } -esp_err_t WebServerService::handleFsList(httpd_req_t* request) { +error_t WebServerService::handleFsList(HttpServerRequest* request, void*) { std::string path; // Log raw query string for diagnostics - size_t qlen = httpd_req_get_url_query_len(request) + 1; - if (qlen > 1) { - std::unique_ptr qbuf(new char[qlen]); - if (httpd_req_get_url_query_str(request, qbuf.get(), qlen) == ESP_OK) { - LOG_I(TAG, "GET /fs/list raw query: %s", qbuf.get()); - } + char qbuf[256]; + if (http_server_request_get_query(request, qbuf, sizeof(qbuf)) > 0) { + LOG_I(TAG, "GET /fs/list raw query: %s", qbuf); } if (!getQueryParam(request, "path", path) || path.empty()) path = "/"; @@ -748,9 +777,9 @@ esp_err_t WebServerService::handleFsList(httpd_req_t* request) { // Allow root path for listing mount points if (!isAllowedBasePath(norm, true)) { LOG_W(TAG, "GET /fs/list - invalid path requested: '%s' normalized: '%s'", path.c_str(), norm.c_str()); - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, "{\"error\":\"invalid path\"}"); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, "{\"error\":\"invalid path\"}"); + return ERROR_NONE; } std::ostringstream json; @@ -778,9 +807,9 @@ esp_err_t WebServerService::handleFsList(httpd_req_t* request) { std::vector entries; int res = file::scandir(norm, entries, file::direntFilterDotEntries, nullptr); if (res < 0) { - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, "{\"error\":\"scan failed\"}"); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, "{\"error\":\"scan failed\"}"); + return ERROR_NONE; } bool first = true; for (auto& e : entries) { @@ -800,24 +829,24 @@ esp_err_t WebServerService::handleFsList(httpd_req_t* request) { json << "]}"; } - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, json.str().c_str()); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, json.str().c_str()); + return ERROR_NONE; } -esp_err_t WebServerService::handleFsDownload(httpd_req_t* request) { +error_t WebServerService::handleFsDownload(HttpServerRequest* request, void*) { std::string path; if (!getQueryParam(request, "path", path) || path.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "path required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "path required"); + return ERROR_UNDEFINED; } std::string norm = normalizePath(path); if (!isAllowedBasePath(norm) || !file::isFile(norm)) { LOG_W(TAG, "GET /fs/download - not found or invalid path: '%s' normalized: '%s'", path.c_str(), norm.c_str()); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } - httpd_resp_set_type(request, getContentType(norm)); + http_server_request_set_content_type(request, getContentType(norm)); // Suggest download - build header into a local string so it remains valid std::string fname = file::getLastPathSegment(norm); std::string disposition = std::string("attachment; filename=\"") + fname + "\""; @@ -841,231 +870,218 @@ esp_err_t WebServerService::handleFsDownload(httpd_req_t* request) { disposition += std::string("; filename*=UTF-8''") + pct; } // Set single Content-Disposition header (avoid adding duplicate headers) - httpd_resp_set_hdr(request, "Content-Disposition", disposition.c_str()); + http_server_request_set_header(request, "Content-Disposition", disposition.c_str()); FILE* fp = fopen(norm.c_str(), "rb"); - if (!fp) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "open failed"); return ESP_FAIL; } + if (!fp) { http_server_request_send_error(request, 500, "open failed"); return ERROR_UNDEFINED; } + if (http_server_request_send_chunk_start(request) != ERROR_NONE) { fclose(fp); return ERROR_UNDEFINED; } char buf[512]; size_t n; while ((n = fread(buf,1,sizeof(buf),fp))>0) { - if (httpd_resp_send_chunk(request, buf, n) != ESP_OK) { fclose(fp); return ESP_FAIL; } + if (http_server_request_send_chunk(request, buf, n) != ERROR_NONE) { fclose(fp); return ERROR_UNDEFINED; } } fclose(fp); - httpd_resp_send_chunk(request, nullptr, 0); - return ESP_OK; + http_server_request_send_chunk_end(request); + return ERROR_NONE; } -esp_err_t WebServerService::handleFsUpload(httpd_req_t* request) { +error_t WebServerService::handleFsUpload(HttpServerRequest* request, void*) { std::string path; // Log raw query and decoded path for diagnostics - size_t qlen = httpd_req_get_url_query_len(request) + 1; - if (qlen > 1) { - std::unique_ptr qbuf(new char[qlen]); - if (httpd_req_get_url_query_str(request, qbuf.get(), qlen) == ESP_OK) { - LOG_I(TAG, "POST /fs/upload raw query: %s", qbuf.get()); - } + char qbuf[256]; + if (http_server_request_get_query(request, qbuf, sizeof(qbuf)) > 0) { + LOG_I(TAG, "POST /fs/upload raw query: %s", qbuf); } if (!getQueryParam(request, "path", path) || path.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "path required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "path required"); + return ERROR_UNDEFINED; } // Log decoded path and headers char content_type[64] = {0}; - httpd_req_get_hdr_value_str(request, "Content-Type", content_type, sizeof(content_type)); + http_server_request_get_header(request, "Content-Type", content_type, sizeof(content_type)); std::string norm = normalizePath(path); - LOG_I(TAG, "POST /fs/upload decoded path: '%s' normalized: '%s' Content-Length: %d Content-Type: %s", path.c_str(), norm.c_str(), (int)request->content_len, content_type[0] ? content_type : "(null)"); + uint64_t content_length = http_server_request_get_content_length(request); + LOG_I(TAG, "POST /fs/upload decoded path: '%s' normalized: '%s' Content-Length: %d Content-Type: %s", path.c_str(), norm.c_str(), (int)content_length, content_type[0] ? content_type : "(null)"); if (!isAllowedBasePath(norm)) { LOG_W(TAG, "POST /fs/upload - invalid path requested: '%s' normalized: '%s'", path.c_str(), norm.c_str()); - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "invalid path"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "invalid path"); + return ERROR_UNDEFINED; } - if (request->content_len > MAX_UPLOAD_SIZE) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "file too large"); - return ESP_FAIL; + if (content_length > MAX_UPLOAD_SIZE) { + http_server_request_send_error(request, 400, "file too large"); + return ERROR_UNDEFINED; } // Ensure parent directory exists (after size check to avoid creating dirs for rejected uploads) if (!file::findOrCreateParentDirectory(norm, 0755)) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "failed to create parent directory"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "failed to create parent directory"); + return ERROR_UNDEFINED; } FILE* fp = fopen(norm.c_str(), "wb"); - if (!fp) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "open failed"); return ESP_FAIL; } - char buf[512]; int remaining = request->content_len; int received=0; - constexpr int MAX_TIMEOUT_RETRIES = 5; - int timeout_retries = 0; + if (!fp) { http_server_request_send_error(request, 500, "open failed"); return ERROR_UNDEFINED; } + char buf[512]; int remaining = static_cast(content_length); int received=0; while (remaining > 0) { int to_read = remaining > (int)sizeof(buf) ? (int)sizeof(buf) : remaining; - int ret = httpd_req_recv(request, buf, to_read); - if (ret == HTTPD_SOCK_ERR_TIMEOUT) { - // Timeout - retry with backoff - timeout_retries++; - if (timeout_retries >= MAX_TIMEOUT_RETRIES) { - LOG_E(TAG, "Upload recv timeout after %d retries", timeout_retries); - fclose(fp); - remove(norm.c_str()); // Clean up partial file - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "recv timeout"); - return ESP_FAIL; - } - LOG_W(TAG, "Upload recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES); - vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Linear backoff - continue; - } + int ret = http_server_request_receive(request, buf, to_read); if (ret <= 0) { LOG_E(TAG, "Upload recv failed with error %d", ret); fclose(fp); remove(norm.c_str()); // Clean up partial file - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "recv failed"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "recv failed"); + return ERROR_UNDEFINED; } - // Successful read - reset timeout counter - timeout_retries = 0; size_t written = fwrite(buf, 1, ret, fp); if (written != (size_t)ret) { fclose(fp); remove(norm.c_str()); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "write failed"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "write failed"); + return ERROR_UNDEFINED; } remaining -= ret; received += ret; } fclose(fp); - httpd_resp_set_type(request, "text/plain"); + http_server_request_set_content_type(request, "text/plain"); std::string msg = std::string("Uploaded ") + std::to_string(received) + " bytes"; - httpd_resp_sendstr(request, msg.c_str()); - return ESP_OK; + http_server_request_send_string(request, msg.c_str()); + return ERROR_NONE; } // Generic GET dispatcher for /fs/* URIs -esp_err_t WebServerService::handleFsGenericGet(httpd_req_t* request) { +error_t WebServerService::handleFsGenericGet(HttpServerRequest* request, void* user_ctx) { // Auth check for all /fs/* endpoints (file system access is sensitive) bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } - const char* uri = request->uri; - if (uriMatches(uri, "/fs/list")) return handleFsList(request); - if (uriMatches(uri, "/fs/download")) return handleFsDownload(request); - if (uriMatches(uri, "/fs/tree")) return handleFsTree(request); + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); + if (uriMatches(uri, "/fs/list")) return handleFsList(request, user_ctx); + if (uriMatches(uri, "/fs/download")) return handleFsDownload(request, user_ctx); + if (uriMatches(uri, "/fs/tree")) return handleFsTree(request, user_ctx); LOG_W(TAG, "GET %s - not found in fs generic dispatcher", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } // Generic POST dispatcher for /fs/* URIs -esp_err_t WebServerService::handleFsGenericPost(httpd_req_t* request) { +error_t WebServerService::handleFsGenericPost(HttpServerRequest* request, void* user_ctx) { // Auth check for all /fs/* endpoints (file system access is sensitive) bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } - const char* uri = request->uri; - if (uriMatches(uri, "/fs/mkdir")) return handleFsMkdir(request); - if (uriMatches(uri, "/fs/delete")) return handleFsDelete(request); - if (uriMatches(uri, "/fs/rename")) return handleFsRename(request); - if (uriMatches(uri, "/fs/upload")) return handleFsUpload(request); + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); + if (uriMatches(uri, "/fs/mkdir")) return handleFsMkdir(request, user_ctx); + if (uriMatches(uri, "/fs/delete")) return handleFsDelete(request, user_ctx); + if (uriMatches(uri, "/fs/rename")) return handleFsRename(request, user_ctx); + if (uriMatches(uri, "/fs/upload")) return handleFsUpload(request, user_ctx); LOG_W(TAG, "POST %s - not found in fs generic dispatcher", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } // Admin dispatcher for consolidated small POST endpoints (e.g. sync, reboot) -esp_err_t WebServerService::handleAdminPost(httpd_req_t* request) { +error_t WebServerService::handleAdminPost(HttpServerRequest* request, void* user_ctx) { // Auth check for all /admin/* endpoints (admin actions are sensitive) bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } - const char* uri = request->uri; - if (strncmp(uri, "/admin/reboot", 13) == 0) return handleReboot(request); + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); + if (strncmp(uri, "/admin/reboot", 13) == 0) return handleReboot(request, user_ctx); LOG_I(TAG, "POST %s - not found in admin dispatcher", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } // API GET dispatcher - returns JSON system information // Note: /api/sysinfo is intentionally public for monitoring use cases -esp_err_t WebServerService::handleApiGet(httpd_req_t* request) { - const char* uri = request->uri; +error_t WebServerService::handleApiGet(HttpServerRequest* request, void* user_ctx) { + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); // Public endpoint: sysinfo (basic device info for monitoring) if (strncmp(uri, "/api/sysinfo", 12) == 0) { - return handleApiSysinfo(request); + return handleApiSysinfo(request, user_ctx); } // Protected endpoints require authentication bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } // Auth-protected endpoints if (strncmp(uri, "/api/apps", 9) == 0) { - return handleApiApps(request); + return handleApiApps(request, user_ctx); } if (strncmp(uri, "/api/wifi", 9) == 0) { - return handleApiWifi(request); + return handleApiWifi(request, user_ctx); } if (strncmp(uri, "/api/screenshot", 15) == 0) { - return handleApiScreenshot(request); + return handleApiScreenshot(request, user_ctx); } LOG_W(TAG, "GET %s - not found in api dispatcher", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } // API POST dispatcher - all POST endpoints require authentication -esp_err_t WebServerService::handleApiPost(httpd_req_t* request) { +error_t WebServerService::handleApiPost(HttpServerRequest* request, void* user_ctx) { bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } - const char* uri = request->uri; + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); if (strncmp(uri, "/api/apps/run", 13) == 0) { - return handleApiAppsRun(request); + return handleApiAppsRun(request, user_ctx); } if (strncmp(uri, "/api/apps/uninstall", 19) == 0) { - return handleApiAppsUninstall(request); + return handleApiAppsUninstall(request, user_ctx); } LOG_W(TAG, "POST %s - not found in api dispatcher", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } // API PUT dispatcher - all PUT endpoints require authentication -esp_err_t WebServerService::handleApiPut(httpd_req_t* request) { +error_t WebServerService::handleApiPut(HttpServerRequest* request, void* user_ctx) { bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } - const char* uri = request->uri; + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); if (strncmp(uri, "/api/apps/install", 17) == 0) { - return handleApiAppsInstall(request); + return handleApiAppsInstall(request, user_ctx); } LOG_W(TAG, "PUT %s - not found in api dispatcher", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "not found"); + return ERROR_UNDEFINED; } -esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) { +error_t WebServerService::handleApiSysinfo(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /api/sysinfo"); std::ostringstream json; @@ -1074,13 +1090,18 @@ esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) { // Firmware info json << "\"firmware\":{"; json << "\"version\":\"" << TT_VERSION << "\","; +#ifdef ESP_PLATFORM json << "\"idf_version\":\"" << ESP_IDF_VERSION_MAJOR << "." << ESP_IDF_VERSION_MINOR << "." << ESP_IDF_VERSION_PATCH << "\""; +#else + json << "\"idf_version\":\"n/a\""; +#endif json << "},"; // Chip info + json << "\"chip\":{"; +#ifdef ESP_PLATFORM esp_chip_info_t chip_info; esp_chip_info(&chip_info); - json << "\"chip\":{"; json << "\"model\":\"" << getChipModelName(chip_info.model) << "\","; json << "\"cores\":" << (int)chip_info.cores << ","; json << "\"revision\":" << (int)chip_info.revision << ","; @@ -1122,37 +1143,51 @@ esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) { uint32_t flash_size = 0; esp_flash_get_size(nullptr, &flash_size); json << "\"flash_size\":" << flash_size; +#else + json << "\"model\":\"" << CONFIG_TT_DEVICE_ID << "\","; + json << "\"cores\":0,"; + json << "\"revision\":0,"; + json << "\"features\":[],"; + json << "\"flash_size\":0"; +#endif json << "},"; // Memory - Internal heap + json << "\"heap\":{"; +#ifdef ESP_PLATFORM size_t heap_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); size_t heap_total = heap_caps_get_total_size(MALLOC_CAP_INTERNAL); size_t heap_min_free = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL); size_t heap_largest = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); - json << "\"heap\":{"; json << "\"free\":" << heap_free << ","; json << "\"total\":" << heap_total << ","; json << "\"min_free\":" << heap_min_free << ","; json << "\"largest_block\":" << heap_largest; +#else + json << "\"free\":0,\"total\":0,\"min_free\":0,\"largest_block\":0"; +#endif json << "},"; // Memory - PSRAM (external) + json << "\"psram\":{"; +#ifdef ESP_PLATFORM size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); size_t psram_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); size_t psram_min_free = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM); size_t psram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM); - json << "\"psram\":{"; json << "\"free\":" << psram_free << ","; json << "\"total\":" << psram_total << ","; json << "\"min_free\":" << psram_min_free << ","; json << "\"largest_block\":" << psram_largest; +#else + json << "\"free\":0,\"total\":0,\"min_free\":0,\"largest_block\":0"; +#endif json << "},"; // Storage info json << "\"storage\":{"; - uint64_t storage_total = 0, storage_free = 0; struct FsIterContext { std::ostringstream& json; @@ -1173,6 +1208,7 @@ esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) { if (fs_iter_context->count != 1) json_context << ","; // add separator between json array entries json_context << "\"" << mount_path_cpp.substr(1) << "\":{"; +#ifdef ESP_PLATFORM uint64_t storage_total = 0, storage_free = 0; if (esp_vfs_fat_info(mount_path, &storage_total, &storage_free) == ESP_OK) { json_context << "\"free\":" << storage_free << ","; @@ -1181,6 +1217,10 @@ esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) { json_context << "\"free\":0,"; json_context << "\"total\":0,"; } +#else + json_context << "\"free\":0,"; + json_context << "\"total\":0,"; +#endif json_context << "\"mounted\":" << (mounted ? "true" : "false") << ""; json_context << "}"; @@ -1209,13 +1249,13 @@ esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) { json << "}"; - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, json.str().c_str()); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, json.str().c_str()); + return ERROR_NONE; } // GET /api/apps - List installed apps -esp_err_t WebServerService::handleApiApps(httpd_req_t* request) { +error_t WebServerService::handleApiApps(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /api/apps"); std::vector manifests; @@ -1248,25 +1288,25 @@ esp_err_t WebServerService::handleApiApps(httpd_req_t* request) { json << "]}"; - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, json.str().c_str()); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, json.str().c_str()); + return ERROR_NONE; } // POST /api/apps/run?id=xxx - Run an app -esp_err_t WebServerService::handleApiAppsRun(httpd_req_t* request) { +error_t WebServerService::handleApiAppsRun(HttpServerRequest* request, void*) { LOG_I(TAG, "POST /api/apps/run"); std::string appId; if (!getQueryParam(request, "id", appId) || appId.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id parameter required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "id parameter required"); + return ERROR_UNDEFINED; } AppManifest manifest; if (app_manager_find_manifest(appId.c_str(), &manifest) != ERROR_NONE) { - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "app not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "app not found"); + return ERROR_UNDEFINED; } // Every app instance gets its own task now, so there's no "stop the existing one first" - @@ -1275,54 +1315,54 @@ esp_err_t WebServerService::handleApiAppsRun(httpd_req_t* request) { app_start(appId.c_str(), 0, nullptr, &instance_id); LOG_I(TAG, "[200] /api/apps/run %s", appId.c_str()); - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } // POST /api/apps/uninstall?id=xxx - Uninstall an app -esp_err_t WebServerService::handleApiAppsUninstall(httpd_req_t* request) { +error_t WebServerService::handleApiAppsUninstall(HttpServerRequest* request, void*) { LOG_I(TAG, "POST /api/apps/uninstall"); std::string appId; if (!getQueryParam(request, "id", appId) || appId.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id parameter required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "id parameter required"); + return ERROR_UNDEFINED; } AppManifest manifest; if (app_manager_find_manifest(appId.c_str(), &manifest) != ERROR_NONE) { LOG_I(TAG, "[200] /api/apps/uninstall %s (app wasn't installed)", appId.c_str()); - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } // Only allow uninstalling external (side-loaded) apps if (manifest.location.type != APP_LOCATION_PATH) { - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "cannot uninstall system apps"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "cannot uninstall system apps"); + return ERROR_UNDEFINED; } if (app_uninstall(appId.c_str()) == ERROR_NONE) { LOG_I(TAG, "[200] /api/apps/uninstall %s", appId.c_str()); - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } else { LOG_W(TAG, "[500] /api/apps/uninstall %s", appId.c_str()); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "uninstall failed"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "uninstall failed"); + return ERROR_UNDEFINED; } } // PUT /api/apps/install - Install an app from multipart form upload -esp_err_t WebServerService::handleApiAppsInstall(httpd_req_t* request) { +error_t WebServerService::handleApiAppsInstall(HttpServerRequest* request, void*) { LOG_I(TAG, "PUT /api/apps/install"); std::string boundary; if (!network::getMultiPartBoundaryOrSendError(request, boundary)) { - return ESP_FAIL; + return ERROR_UNDEFINED; } - size_t content_left = request->content_len; + size_t content_left = http_server_request_get_content_length(request); constexpr size_t MAX_APP_UPLOAD_SIZE = 20 * 1024 * 1024; // Read headers until empty line (skip boundary line first) @@ -1330,72 +1370,74 @@ esp_err_t WebServerService::handleApiAppsInstall(httpd_req_t* request) { content_left -= content_headers_data.length(); // Split headers into lines and filter empty ones - auto content_headers = string::split(content_headers_data, "\r\n") - | std::views::filter([](const std::string& line) { - return line.length() > 0; - }) - | std::ranges::to(); + auto content_header_lines = string::split(content_headers_data, "\r\n"); + std::vector 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()) { LOG_W(TAG, "parseContentDisposition returned empty map for: %s", content_headers_data.c_str()); - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid content disposition"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "invalid content disposition"); + return ERROR_UNDEFINED; } auto filename_entry = content_disposition_map.find("filename"); if (filename_entry == content_disposition_map.end()) { LOG_W(TAG, "filename not found in content disposition map"); - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "filename parameter missing"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "filename parameter missing"); + return ERROR_UNDEFINED; } // Calculate file size auto boundary_and_newlines_after_file = std::format("\r\n--{}--\r\n", boundary); if (content_left <= boundary_and_newlines_after_file.length()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid multipart payload"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "invalid multipart payload"); + return ERROR_UNDEFINED; } auto file_size = content_left - boundary_and_newlines_after_file.length(); if (file_size == 0 || file_size > MAX_APP_UPLOAD_SIZE) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "file too large"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "file too large"); + return ERROR_UNDEFINED; } // 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 directory"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "failed to create temp directory"); + 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 save file"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "failed to save file"); + return ERROR_UNDEFINED; } content_left -= file_size; // Read and discard trailing boundary if (!network::readAndDiscardOrSendError(request, boundary_and_newlines_after_file)) { - return ESP_FAIL; + return ERROR_UNDEFINED; } // Install the app if (app_install(file_path.c_str()) != ERROR_NONE) { file::deleteFile(file_path); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "installation failed"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "installation failed"); + return ERROR_UNDEFINED; } // Cleanup temp file @@ -1404,8 +1446,8 @@ esp_err_t WebServerService::handleApiAppsInstall(httpd_req_t* request) { } LOG_I(TAG, "[200] /api/apps/install -> %s", file_path.c_str()); - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } // Helper to convert radio state to string @@ -1422,7 +1464,7 @@ static const char* radioStateToJsonString(wifi::RadioState state) { } // GET /api/wifi - WiFi status -esp_err_t WebServerService::handleApiWifi(httpd_req_t* request) { +error_t WebServerService::handleApiWifi(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /api/wifi"); auto state = wifi::getRadioState(); @@ -1440,14 +1482,14 @@ esp_err_t WebServerService::handleApiWifi(httpd_req_t* request) { json << "\"secure\":" << (secure ? "true" : "false"); json << "}"; - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, json.str().c_str()); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, json.str().c_str()); + return ERROR_NONE; } // GET /api/screenshot - Capture and return screenshot as PNG // Screenshots are saved to SD card root (if available) or /data with incrementing numbers -esp_err_t WebServerService::handleApiScreenshot(httpd_req_t* request) { +error_t WebServerService::handleApiScreenshot(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /api/screenshot"); #if TT_FEATURE_SCREENSHOT_ENABLED @@ -1465,8 +1507,8 @@ esp_err_t WebServerService::handleApiScreenshot(httpd_req_t* request) { } } if (!found_slot) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "no available screenshot slots"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "no available screenshot slots"); + return ERROR_UNDEFINED; } LOG_I(TAG, "Screenshot will be saved to: %s", screenshot_path.c_str()); @@ -1481,46 +1523,50 @@ esp_err_t WebServerService::handleApiScreenshot(httpd_req_t* request) { if (!success) { LOG_E(TAG, "lv_screenshot_create failed for path: %s", lvgl_screenshot_path.c_str()); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "screenshot capture failed"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "screenshot capture failed"); + return ERROR_UNDEFINED; } LOG_I(TAG, "Screenshot captured successfully"); } else { LOG_E(TAG, "Could not acquire LVGL lock within 100ms"); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "could not acquire LVGL lock"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "could not acquire LVGL lock"); + return ERROR_UNDEFINED; } // Send the file (use regular path for fopen, not LVGL path) - httpd_resp_set_type(request, "image/png"); + http_server_request_set_content_type(request, "image/png"); FILE* fp = fopen(screenshot_path.c_str(), "rb"); if (!fp) { - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "failed to open screenshot"); - return ESP_FAIL; + http_server_request_send_error(request, 500, "failed to open screenshot"); + return ERROR_UNDEFINED; } + if (http_server_request_send_chunk_start(request) != ERROR_NONE) { + fclose(fp); + return ERROR_UNDEFINED; + } char buf[512]; size_t n; while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { - if (httpd_resp_send_chunk(request, buf, n) != ESP_OK) { + if (http_server_request_send_chunk(request, buf, n) != ERROR_NONE) { fclose(fp); - return ESP_FAIL; + return ERROR_UNDEFINED; } } fclose(fp); - httpd_resp_send_chunk(request, nullptr, 0); + http_server_request_send_chunk_end(request); // File is kept on storage (not deleted) for user access LOG_I(TAG, "[200] /api/screenshot -> %s", screenshot_path.c_str()); - return ESP_OK; + return ERROR_NONE; #else - httpd_resp_send_err(request, HTTPD_501_METHOD_NOT_IMPLEMENTED, "screenshot feature not enabled"); - return ESP_FAIL; + http_server_request_send_error(request, 501, "screenshot feature not enabled"); + return ERROR_UNDEFINED; #endif } -esp_err_t WebServerService::handleFsTree(httpd_req_t* request) { +error_t WebServerService::handleFsTree(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /fs/tree"); @@ -1554,28 +1600,28 @@ esp_err_t WebServerService::handleFsTree(httpd_req_t* request) { } json << "]}"; - httpd_resp_set_type(request, "application/json"); - httpd_resp_sendstr(request, json.str().c_str()); - return ESP_OK; + http_server_request_set_content_type(request, "application/json"); + http_server_request_send_string(request, json.str().c_str()); + return ERROR_NONE; } // Create a directory at the specified path (POST /fs/mkdir?path=/data/newdir) -esp_err_t WebServerService::handleFsMkdir(httpd_req_t* request) { +error_t WebServerService::handleFsMkdir(HttpServerRequest* request, void*) { std::string path; if (!getQueryParam(request, "path", path) || path.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "path required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "path required"); + return ERROR_UNDEFINED; } std::string norm = normalizePath(path); LOG_I(TAG, "POST /fs/mkdir requested: '%s' normalized: '%s'", path.c_str(), norm.c_str()); if (!isAllowedBasePath(norm)) { - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "invalid path"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "invalid path"); + return ERROR_UNDEFINED; } bool ok = file::findOrCreateDirectory(norm, 0755); - if (!ok) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "mkdir failed"); return ESP_FAIL; } - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + if (!ok) { http_server_request_send_error(request, 500, "mkdir failed"); return ERROR_UNDEFINED; } + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } static bool isRootMountPoint(const std::string& path) { @@ -1583,48 +1629,48 @@ static bool isRootMountPoint(const std::string& path) { } // Delete a file or directory (POST /fs/delete?path=/data/foo) -esp_err_t WebServerService::handleFsDelete(httpd_req_t* request) { +error_t WebServerService::handleFsDelete(HttpServerRequest* request, void*) { std::string path; if (!getQueryParam(request, "path", path) || path.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "path required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "path required"); + return ERROR_UNDEFINED; } std::string norm = normalizePath(path); LOG_I(TAG, "POST /fs/delete requested: '%s' normalized: '%s'", path.c_str(), norm.c_str()); if (!isAllowedBasePath(norm)) { - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "invalid path"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "invalid path"); + return ERROR_UNDEFINED; } if (isRootMountPoint(norm)) { - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "cannot delete mount point"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "cannot delete mount point"); + return ERROR_UNDEFINED; } bool ok = true; if (file::isDirectory(norm)) ok = file::deleteRecursively(norm); else if (file::isFile(norm)) ok = file::deleteFile(norm); else ok = false; - if (!ok) { httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "delete failed"); return ESP_FAIL; } - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + if (!ok) { http_server_request_send_error(request, 500, "delete failed"); return ERROR_UNDEFINED; } + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } // Rename a file or folder (POST /fs/rename?path=/data/oldname&newName=newname) -esp_err_t WebServerService::handleFsRename(httpd_req_t* request) { +error_t WebServerService::handleFsRename(HttpServerRequest* request, void*) { std::string path; std::string newName; if (!getQueryParam(request, "path", path) || path.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "path required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "path required"); + return ERROR_UNDEFINED; } if (!getQueryParam(request, "newName", newName) || newName.empty()) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "newName required"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "newName required"); + return ERROR_UNDEFINED; } std::string norm = normalizePath(path); LOG_I(TAG, "POST /fs/rename requested: '%s' normalized: '%s' -> newName: '%s'", path.c_str(), norm.c_str(), newName.c_str()); if (!isAllowedBasePath(norm)) { - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "invalid path"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "invalid path"); + return ERROR_UNDEFINED; } // Basic validation of newName: must not contain path separators or '..' @@ -1632,8 +1678,8 @@ esp_err_t WebServerService::handleFsRename(httpd_req_t* request) { auto trim = [](std::string& s){ size_t st=0; while (stst && isspace((unsigned char)s[ed-1])) --ed; s = s.substr(st, ed-st); }; trim(newName); if (newName.empty() || newName.find('/') != std::string::npos || newName.find('\\') != std::string::npos || newName.find("..") != std::string::npos) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid newName"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "invalid newName"); + return ERROR_UNDEFINED; } // compute parent directory @@ -1644,16 +1690,16 @@ esp_err_t WebServerService::handleFsRename(httpd_req_t* request) { } if (!isAllowedBasePath(parent)) { - httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "invalid target parent"); - return ESP_FAIL; + http_server_request_send_error(request, 403, "invalid target parent"); + return ERROR_UNDEFINED; } std::string target = file::getChildPath(parent, newName); // Prevent overwrite: fail if target exists if (file::isFile(target) || file::isDirectory(target)) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "target exists"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "target exists"); + return ERROR_UNDEFINED; } // perform rename @@ -1663,132 +1709,146 @@ esp_err_t WebServerService::handleFsRename(httpd_req_t* request) { LOG_W(TAG, "rename failed errno=%d (%s) -> %s -> %s", e, strerror(e), norm.c_str(), target.c_str()); // Return errno string to client to aid debugging std::string msg = std::string("rename failed: ") + strerror(e); - httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, msg.c_str()); - return ESP_FAIL; + http_server_request_send_error(request, 500, msg.c_str()); + return ERROR_UNDEFINED; } - httpd_resp_sendstr(request, "ok"); - return ESP_OK; + http_server_request_send_string(request, "ok"); + return ERROR_NONE; } // endregion -esp_err_t WebServerService::handleReboot(httpd_req_t* request) { - +error_t WebServerService::handleReboot(HttpServerRequest* request, void*) { + LOG_I(TAG, "POST /reboot"); - httpd_resp_sendstr(request, "Rebooting..."); - + http_server_request_send_string(request, "Rebooting..."); + // Reboot after a short delay to allow response to be sent vTaskDelay(pdMS_TO_TICKS(2000)); +#ifdef ESP_PLATFORM esp_restart(); +#else + LOG_W(TAG, "Reboot is not supported on this platform"); +#endif - return ESP_OK; // Unreachable, but satisfies function signature + return ERROR_NONE; // Unreachable on ESP_PLATFORM, but satisfies function signature } -esp_err_t WebServerService::handleAssets(httpd_req_t* request) { +error_t WebServerService::handleAssets(HttpServerRequest* request, void*) { // Auth check for UI access control bool authPassed = false; - esp_err_t authResult = validateRequestAuth(request, authPassed); + error_t authResult = validateRequestAuth(request, authPassed); if (!authPassed) { return authResult; } - const char* uri = request->uri; + char uri[256]; + http_server_request_get_uri(request, uri, sizeof(uri)); LOG_I(TAG, "GET %s", uri); // Special case: serve favicon from system assets if (strcmp(uri, "/favicon.ico") == 0) { - const char* faviconPath = "/system/spinner.png"; + std::string faviconPathStr = std::string(file::MOUNT_POINT_SYSTEM) + "/spinner.png"; + const char* faviconPath = faviconPathStr.c_str(); if (file::isFile(faviconPath)) { - httpd_resp_set_type(request, "image/png"); - httpd_resp_set_hdr(request, "Cache-Control", "public, max-age=86400"); + http_server_request_set_content_type(request, "image/png"); + http_server_request_set_header(request, "Cache-Control", "public, max-age=86400"); FILE* fp = fopen(faviconPath, "rb"); if (fp) { + if (http_server_request_send_chunk_start(request) != ERROR_NONE) { + fclose(fp); + return ERROR_UNDEFINED; + } char buffer[512]; size_t bytesRead; while ((bytesRead = fread(buffer, 1, sizeof(buffer), fp)) > 0) { - if (httpd_resp_send_chunk(request, buffer, bytesRead) != ESP_OK) { + if (http_server_request_send_chunk(request, buffer, bytesRead) != ERROR_NONE) { fclose(fp); - return ESP_FAIL; + return ERROR_UNDEFINED; } } fclose(fp); - httpd_resp_send_chunk(request, nullptr, 0); + http_server_request_send_chunk_end(request); LOG_I(TAG, "[200] %s (favicon)", uri); - return ESP_OK; + return ERROR_NONE; } } // If favicon not found, return 404 silently (browsers handle this gracefully) - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "Not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "Not found"); + return ERROR_UNDEFINED; } // Special case: if requesting dashboard.html but it doesn't exist, serve default.html - std::string requestedPath = uri; - if (auto qpos = requestedPath.find('?'); qpos != std::string::npos) { - requestedPath = requestedPath.substr(0, qpos); - } - requestedPath = normalizePath(requestedPath); + std::string requestedPath = normalizePath(uri); if (requestedPath == "/.." || requestedPath.ends_with("/..") || requestedPath.find("/../") != std::string::npos) { - httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid path"); - return ESP_FAIL; + http_server_request_send_error(request, 400, "invalid path"); + return ERROR_UNDEFINED; } - std::string dataPath = std::string("/system/app/WebServer") + requestedPath; - + std::string dataPath = std::string(file::MOUNT_POINT_SYSTEM) + "/app/WebServer" + requestedPath; + if (requestedPath == "/dashboard.html" && !file::isFile(dataPath.c_str())) { LOG_I(TAG, "dashboard.html not found, serving default.html"); } - + // Try to serve from Data partition first if (file::isFile(dataPath.c_str())) { - httpd_resp_set_type(request, getContentType(dataPath)); + http_server_request_set_content_type(request, getContentType(dataPath)); FILE* fp = fopen(dataPath.c_str(), "rb"); if (fp) { + if (http_server_request_send_chunk_start(request) != ERROR_NONE) { + fclose(fp); + return ERROR_UNDEFINED; + } char buffer[512]; size_t bytesRead; while ((bytesRead = fread(buffer, 1, sizeof(buffer), fp)) > 0) { - if (httpd_resp_send_chunk(request, buffer, bytesRead) != ESP_OK) { + if (http_server_request_send_chunk(request, buffer, bytesRead) != ERROR_NONE) { fclose(fp); - return ESP_FAIL; + return ERROR_UNDEFINED; } } fclose(fp); - httpd_resp_send_chunk(request, nullptr, 0); // End of chunks + http_server_request_send_chunk_end(request); // End of chunks LOG_I(TAG, "[200] %s (from Data)", uri); - return ESP_OK; + return ERROR_NONE; } } // Fallback to SD card std::string sdPath = std::string("/sdcard/tactility/webserver") + requestedPath; if (file::isFile(sdPath.c_str())) { - httpd_resp_set_type(request, getContentType(sdPath)); + http_server_request_set_content_type(request, getContentType(sdPath)); FILE* fp = fopen(sdPath.c_str(), "rb"); if (fp) { + if (http_server_request_send_chunk_start(request) != ERROR_NONE) { + fclose(fp); + return ERROR_UNDEFINED; + } char buffer[512]; size_t bytesRead; while ((bytesRead = fread(buffer, 1, sizeof(buffer), fp)) > 0) { - if (httpd_resp_send_chunk(request, buffer, bytesRead) != ESP_OK) { + if (http_server_request_send_chunk(request, buffer, bytesRead) != ERROR_NONE) { fclose(fp); - return ESP_FAIL; + return ERROR_UNDEFINED; } } fclose(fp); - httpd_resp_send_chunk(request, nullptr, 0); // End of chunks + http_server_request_send_chunk_end(request); // End of chunks LOG_I(TAG, "[200] %s (from SD)", uri); - return ESP_OK; + return ERROR_NONE; } } - + // File not found LOG_W(TAG, "[404] %s", uri); - httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "File not found"); - return ESP_FAIL; + http_server_request_send_error(request, 404, "File not found"); + return ERROR_UNDEFINED; } extern const ServiceManifest manifest = { @@ -1812,5 +1872,3 @@ bool isWebServerEnabled() { } } // namespace - -#endif // ESP_PLATFORM