Multi-platform http server (#646)
- Add server support to `http-module` - Ensure `DevelopmentService` works on all platforms (including posix) - Ensure the built-in web server with dashboard works on all platforms (inluding posix). It still has some issues with certain featuers, but the basics work.
This commit is contained in:
committed by
GitHub
parent
a0b2ee7ebc
commit
1b16184a72
@@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <http/types.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -2,6 +2,11 @@
|
||||
#include <http/download.h>
|
||||
#include <http/module.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#include <esp_http_client.h>
|
||||
@@ -14,16 +19,74 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
#include <sys/select.h>
|
||||
extern "C" {
|
||||
|
||||
enum HttpMethod : int;
|
||||
typedef uint16_t status_code_t;
|
||||
struct HttpServerRequest;
|
||||
struct HttpServer;
|
||||
struct HttpServerConfig;
|
||||
|
||||
// Deliberately not #include <http/server.h> or <http/types.h>: 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 = {
|
||||
|
||||
@@ -0,0 +1,568 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <http/server.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<const char*>(buffer) + total_sent, size - total_sent, 0);
|
||||
if (sent < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
total_sent += static_cast<size_t>(sent);
|
||||
}
|
||||
return static_cast<ssize_t>(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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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<HttpServerRequestHandler> 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<std::string, std::string>* 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<HttpServer*>(raw_server);
|
||||
LOG_I(TAG, "Listening on port %u", static_cast<unsigned>(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<sockaddr*>(&address), sizeof(address)) != 0 ||
|
||||
listen(fd, LISTEN_BACKLOG) != 0) {
|
||||
LOG_E(TAG, "Failed to bind/listen on port %u", static_cast<unsigned>(server->configured_port));
|
||||
close(fd);
|
||||
mutex_unlock(&server->mutex);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
socklen_t address_length = sizeof(address);
|
||||
getsockname(fd, reinterpret_cast<sockaddr*>(&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<size_t>(request->body_remaining);
|
||||
}
|
||||
ssize_t received = receive_retry(request->socket_fd, buffer, buffer_size);
|
||||
if (received > 0) {
|
||||
request->body_remaining -= static_cast<uint64_t>(received);
|
||||
}
|
||||
return static_cast<int>(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_t>(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"
|
||||
@@ -0,0 +1,59 @@
|
||||
#include <http/types.h>
|
||||
#include <tactility/error.h>
|
||||
#include <cstring>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <http/server.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
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<struct sockaddr*>(&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<size_t>(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<size_t>(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<size_t>(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 "<hex-len>\r\n<data>\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<struct sockaddr*>(&address), sizeof(address)), 0);
|
||||
close(fd);
|
||||
|
||||
http_server_free(server);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
#include <esp_http_server.h>
|
||||
|
||||
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<httpd_uri_t> handlers;
|
||||
|
||||
RecursiveMutex mutex;
|
||||
httpd_handle_t server = nullptr;
|
||||
|
||||
bool startInternal();
|
||||
void stopInternal();
|
||||
|
||||
public:
|
||||
|
||||
HttpServer(
|
||||
uint32_t port,
|
||||
const std::string& address,
|
||||
std::vector<httpd_uri_t> 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
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <http/server.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
// 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);
|
||||
|
||||
}
|
||||
@@ -1,31 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<char[]> 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<std::string, std::string> parseContentDisposition(const std::vector<std::string>& 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
|
||||
@@ -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;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/service/Service.h>
|
||||
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <Tactility/network/HttpServer.h>
|
||||
#include <http/server.h>
|
||||
|
||||
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<httpd_uri_t>{
|
||||
{
|
||||
.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 @@ public:
|
||||
std::shared_ptr<DevelopmentService> findService();
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/PubSub.h>
|
||||
#include <Tactility/service/Service.h>
|
||||
#include <Tactility/network/HttpServer.h>
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <http/server.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <esp_netif.h>
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tt::service::webserver {
|
||||
@@ -33,58 +35,63 @@ enum class WebServerEvent {
|
||||
class WebServerService final : public Service {
|
||||
private:
|
||||
mutable RecursiveMutex mutex;
|
||||
std::unique_ptr<network::HttpServer> httpServer;
|
||||
struct HttpServer* httpServer = nullptr;
|
||||
PubSub<WebServerEvent>::SubscriptionHandle settingsEventSubscription = nullptr;
|
||||
std::shared_ptr<PubSub<WebServerEvent>> pubsub = std::make_shared<PubSub<WebServerEvent>>();
|
||||
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<PubSub<WebServerEvent>> getPubsub();
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
@@ -120,9 +120,7 @@ namespace service {
|
||||
// Primary
|
||||
namespace audio { extern const ServiceManifest manifest; }
|
||||
namespace wifi { extern const ServiceManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace development { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||
namespace espnow { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
@@ -137,9 +135,7 @@ namespace service {
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
namespace screenshot { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace webserver { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -189,10 +185,10 @@ namespace app {
|
||||
namespace wificonnect { extern const ::AppManifest manifest; }
|
||||
namespace wifimanage { extern const ::AppManifest manifest; }
|
||||
|
||||
namespace webserversettings { extern const ::AppManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace apwebserver { extern const ::AppManifest manifest; }
|
||||
namespace crashdiagnostics { extern const ::AppManifest manifest; }
|
||||
namespace webserversettings { extern const ::AppManifest manifest; }
|
||||
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
||||
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
||||
#endif
|
||||
@@ -251,11 +247,11 @@ static void registerInternalApps() {
|
||||
app_manager_add(&app::wificonnect::manifest);
|
||||
app_manager_add(&app::wifimanage::manifest);
|
||||
|
||||
app_manager_add(&app::development::manifest);
|
||||
app_manager_add(&app::webserversettings::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
app_manager_add(&app::apwebserver::manifest);
|
||||
app_manager_add(&app::webserversettings::manifest);
|
||||
app_manager_add(&app::crashdiagnostics::manifest);
|
||||
app_manager_add(&app::development::manifest);
|
||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||
app_manager_add(&app::keyboardsettings::manifest);
|
||||
#endif
|
||||
@@ -320,16 +316,12 @@ static void registerAndStartServices() {
|
||||
addService(service::audio::manifest);
|
||||
}
|
||||
addService(service::wifi::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::development::manifest);
|
||||
#endif
|
||||
addService(service::webserver::manifest);
|
||||
|
||||
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||
addService(service::espnow::manifest);
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::webserver::manifest);
|
||||
#endif
|
||||
#if defined(ESP_PLATFORM)
|
||||
if (device_exists_of_type(&RTC_TYPE)) {
|
||||
addService(service::rtctime::manifest);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
@@ -23,6 +21,7 @@
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace tt::app::development {
|
||||
|
||||
@@ -235,5 +234,3 @@ extern const ::AppManifest manifest = {
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@@ -218,7 +218,7 @@ void View::runFile(const std::string& file_path) {
|
||||
|
||||
if (!isExecutablePath(file_path)) {
|
||||
LOG_W(TAG, "Not executable: %s", file_path.c_str());
|
||||
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
|
||||
alertdialog::start(appInstanceId, "Run failed", "\"" + file::getLastPathSegment(file_path) + "\" is not an executable.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/settings/WebServerSettings.h>
|
||||
#include <Tactility/service/webserver/WebServerService.h>
|
||||
@@ -19,9 +17,6 @@
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
namespace tt::app::webserversettings {
|
||||
|
||||
constexpr auto* TAG = "WebServerSettingsApp";
|
||||
@@ -169,16 +164,9 @@ void updateUrlDisplay(Context* ctx) {
|
||||
url += "192.168.4.1";
|
||||
} else {
|
||||
// Station mode - try to get actual IP
|
||||
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (netif != nullptr) {
|
||||
esp_netif_ip_info_t ip_info;
|
||||
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
|
||||
char ip_str[16];
|
||||
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||
url += ip_str;
|
||||
} else {
|
||||
url = "Connecting...";
|
||||
}
|
||||
std::string ip = service::wifi::getIp();
|
||||
if (!ip.empty()) {
|
||||
url += ip;
|
||||
} else {
|
||||
url = "Not connected";
|
||||
}
|
||||
@@ -440,5 +428,3 @@ extern const ::AppManifest manifest = {
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/network/HttpServer.h>
|
||||
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
constexpr auto* TAG = "HttpServer";
|
||||
|
||||
static constexpr size_t INTERNAL_URI_HANDLER_COUNT = 2;
|
||||
|
||||
bool HttpServer::startInternal() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = stackSize;
|
||||
config.server_port = port;
|
||||
config.uri_match_fn = matchUri;
|
||||
config.max_uri_handlers = handlers.size() + INTERNAL_URI_HANDLER_COUNT;
|
||||
|
||||
if (httpd_start(&server, &config) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to start http server on port %u", (unsigned)port);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allRegistered = true;
|
||||
for (std::vector<httpd_uri_t>::reference handler : handlers) {
|
||||
if (httpd_register_uri_handler(server, &handler) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to register URI handler: %s", handler.uri);
|
||||
allRegistered = false;
|
||||
}
|
||||
}
|
||||
if (!allRegistered) {
|
||||
httpd_stop(server);
|
||||
server = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Started on port %u", (unsigned)config.server_port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpServer::stopInternal() {
|
||||
LOG_I(TAG, "Stopping server");
|
||||
if (server != nullptr) {
|
||||
if (httpd_stop(server) == ESP_OK) {
|
||||
server = nullptr;
|
||||
} else {
|
||||
LOG_W(TAG, "Error while stopping");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpServer::start() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (isStarted()) {
|
||||
LOG_W(TAG, "Already started");
|
||||
return true;
|
||||
}
|
||||
|
||||
return startInternal();
|
||||
}
|
||||
|
||||
void HttpServer::stop() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (!isStarted()) {
|
||||
LOG_W(TAG, "Not started");
|
||||
return;
|
||||
}
|
||||
|
||||
stopInternal();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,132 @@
|
||||
#include <Tactility/network/HttpServerReq.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
constexpr auto* TAG = "HttpServerReq";
|
||||
|
||||
bool getHeaderOrSendError(struct HttpServerRequest* request, const std::string& name, std::string& value) {
|
||||
size_t length = http_server_request_get_header(request, name.c_str(), nullptr, 0);
|
||||
if (length == 0) {
|
||||
http_server_request_send_error(request, 400, "header missing");
|
||||
return false;
|
||||
}
|
||||
value.resize(length);
|
||||
http_server_request_get_header(request, name.c_str(), value.data(), length + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(struct HttpServerRequest* request, std::string& boundary) {
|
||||
std::string content_type;
|
||||
if (!getHeaderOrSendError(request, "Content-Type", content_type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto boundary_index = content_type.find("boundary=");
|
||||
if (boundary_index == std::string::npos) {
|
||||
http_server_request_send_error(request, 400, "boundary not found in Content-Type");
|
||||
return false;
|
||||
}
|
||||
|
||||
boundary = content_type.substr(boundary_index + 9);
|
||||
boundary = boundary.substr(0, boundary.find(';'));
|
||||
// Trim any whitespace left by the ';' cut above, then unquote (RFC 2231 allows a quoted value).
|
||||
while (!boundary.empty() && boundary.back() == ' ') {
|
||||
boundary.pop_back();
|
||||
}
|
||||
if (boundary.size() >= 2 && boundary.front() == '"' && boundary.back() == '"') {
|
||||
boundary = boundary.substr(1, boundary.size() - 2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getQueryOrSendError(struct HttpServerRequest* request, std::string& query) {
|
||||
size_t length = http_server_request_get_query(request, nullptr, 0);
|
||||
if (length == 0) {
|
||||
http_server_request_send_error(request, 400, "id not specified");
|
||||
return false;
|
||||
}
|
||||
query.resize(length);
|
||||
http_server_request_get_query(request, query.data(), length + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reads exactly `length` bytes, or fails - unlike http_server_request_receive() itself, which may
|
||||
// return fewer bytes than requested per call.
|
||||
static bool receiveExact(struct HttpServerRequest* request, void* buffer, size_t length) {
|
||||
size_t total_read = 0;
|
||||
while (total_read < length) {
|
||||
int read = http_server_request_receive(request, static_cast<char*>(buffer) + total_read, length - total_read);
|
||||
if (read <= 0) {
|
||||
return false;
|
||||
}
|
||||
total_read += static_cast<size_t>(read);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bounds a client's multipart preamble (boundary + part headers): without this, a client that
|
||||
// keeps sending bytes without the terminator would make this buffer, and re-scan, unboundedly.
|
||||
constexpr size_t MAX_PREAMBLE_LENGTH = 8192;
|
||||
|
||||
std::string receiveTextUntil(struct HttpServerRequest* request, const std::string& terminator) {
|
||||
std::string result;
|
||||
while (!result.ends_with(terminator)) {
|
||||
if (result.length() >= MAX_PREAMBLE_LENGTH) {
|
||||
return "";
|
||||
}
|
||||
char byte;
|
||||
if (!receiveExact(request, &byte, 1)) {
|
||||
return "";
|
||||
}
|
||||
result += byte;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool readAndDiscardOrSendError(struct HttpServerRequest* request, const std::string& toRead) {
|
||||
std::string buffer(toRead.length(), '\0');
|
||||
if (!receiveExact(request, buffer.data(), toRead.length())) {
|
||||
http_server_request_send_error(request, 400, "failed to read discardable data");
|
||||
return false;
|
||||
}
|
||||
if (buffer != toRead) {
|
||||
http_server_request_send_error(request, 400, "discardable data mismatch");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t receiveFile(struct HttpServerRequest* request, size_t length, const std::string& filePath) {
|
||||
constexpr size_t BUFFER_SIZE = 512;
|
||||
char buffer[BUFFER_SIZE];
|
||||
size_t bytes_received = 0;
|
||||
|
||||
auto* file = fopen(filePath.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
LOG_E(TAG, "Failed to open file for writing: %s", filePath.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (bytes_received < length) {
|
||||
size_t expected_chunk_size = std::min<size_t>(BUFFER_SIZE, length - bytes_received);
|
||||
int received = http_server_request_receive(request, buffer, expected_chunk_size);
|
||||
if (received <= 0) {
|
||||
LOG_E(TAG, "Receive failed, got 0 bytes but expected %zu more", length - bytes_received);
|
||||
break;
|
||||
}
|
||||
if (fwrite(buffer, 1, static_cast<size_t>(received), file) != static_cast<size_t>(received)) {
|
||||
LOG_E(TAG, "Failed to write all bytes");
|
||||
break;
|
||||
}
|
||||
bytes_received += static_cast<size_t>(received);
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,137 +1,10 @@
|
||||
#include <Tactility/LogMessages.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/network/HttpdReq.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
constexpr auto* TAG = "HttpdReq";
|
||||
|
||||
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) {
|
||||
size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
|
||||
if (header_size == 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "header missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto header_buffer = std::make_unique<char[]>(header_size + 1);
|
||||
if (header_buffer == nullptr) {
|
||||
LOG_E(TAG, LOG_MESSAGE_ALLOC_FAILED);
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (httpd_req_get_hdr_value_str(request, name.c_str(), header_buffer.get(), header_size + 1) != ESP_OK) {
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
value = header_buffer.get();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary) {
|
||||
std::string content_type_header;
|
||||
if (!getHeaderOrSendError(request, "Content-Type", content_type_header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto boundary_index = content_type_header.find("boundary=");
|
||||
if (boundary_index == std::string::npos) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "boundary not found in Content-Type");
|
||||
return false;
|
||||
}
|
||||
|
||||
boundary = content_type_header.substr(boundary_index + 9);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getQueryOrSendError(httpd_req_t* request, std::string& query) {
|
||||
size_t buffer_length = httpd_req_get_url_query_len(request);
|
||||
if (buffer_length == 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto buffer = std::make_unique<char[]>(buffer_length + 1);
|
||||
if (buffer.get() == nullptr || httpd_req_get_url_query_str(request, buffer.get(), buffer_length + 1) != ESP_OK) {
|
||||
httpd_resp_send_500(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
query = buffer.get();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead) {
|
||||
assert(length > 0);
|
||||
bytesRead = 0;
|
||||
|
||||
// We have to use malloc() because make_unique() throws an exception
|
||||
// and we don't have exceptions enabled in the compiler settings
|
||||
auto* buffer = static_cast<char*>(malloc(length));
|
||||
if (buffer == nullptr) {
|
||||
LOG_E(TAG, "Out of memory (failed to allocated %u bytes)", (unsigned)length);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
constexpr int MAX_TIMEOUT_RETRIES = 5;
|
||||
int timeout_retries = 0;
|
||||
while (bytesRead < length) {
|
||||
size_t read_size = length - bytesRead;
|
||||
int bytes_received = httpd_req_recv(request, buffer + bytesRead, read_size);
|
||||
if (bytes_received == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
// Timeout - retry with backoff
|
||||
timeout_retries++;
|
||||
if (timeout_retries >= MAX_TIMEOUT_RETRIES) {
|
||||
LOG_W(TAG, "Recv timeout after %d retries, read %u/%u bytes", timeout_retries, (unsigned)bytesRead, (unsigned)length);
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
LOG_W(TAG, "Recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES);
|
||||
vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Exponential backoff
|
||||
continue;
|
||||
}
|
||||
if (bytes_received <= 0) {
|
||||
LOG_W(TAG, "Received error %d after reading %u/%u bytes", bytes_received, (unsigned)bytesRead, (unsigned)length);
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Successful read - reset timeout counter
|
||||
timeout_retries = 0;
|
||||
bytesRead += bytes_received;
|
||||
}
|
||||
|
||||
return std::unique_ptr<char[]>(buffer);
|
||||
}
|
||||
|
||||
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) {
|
||||
size_t read_index = 0;
|
||||
std::stringstream result;
|
||||
while (!result.str().ends_with(terminator)) {
|
||||
char buffer;
|
||||
size_t bytes_read = httpd_req_recv(request, &buffer, 1);
|
||||
if (bytes_read <= 0) {
|
||||
return "";
|
||||
} else {
|
||||
read_index += bytes_read;
|
||||
}
|
||||
|
||||
result << buffer;
|
||||
}
|
||||
|
||||
return result.str();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> parseContentDisposition(const std::vector<std::string>& input) {
|
||||
std::map<std::string, std::string> result;
|
||||
static std::string prefix = "Content-Disposition: ";
|
||||
@@ -164,69 +37,4 @@ std::map<std::string, std::string> parseContentDisposition(const std::vector<std
|
||||
return result;
|
||||
}
|
||||
|
||||
bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead) {
|
||||
size_t bytes_read;
|
||||
auto buffer = receiveByteArray(request, toRead.length(), bytes_read);
|
||||
if (buffer == nullptr || bytes_read != toRead.length()) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "failed to read discardable data");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memcmp(buffer.get(), toRead.c_str(), bytes_read) != 0) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "discardable data mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t receiveFile(httpd_req_t* request, size_t length, const std::string& filePath) {
|
||||
constexpr auto BUFFER_SIZE = 512;
|
||||
char buffer[BUFFER_SIZE];
|
||||
size_t bytes_received = 0;
|
||||
|
||||
auto* file = fopen(filePath.c_str(), "wb");
|
||||
if (file == nullptr) {
|
||||
LOG_E(TAG, "Failed to open file for writing: %s", filePath.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr int MAX_TIMEOUT_RETRIES = 5;
|
||||
int timeout_retries = 0;
|
||||
while (bytes_received < length) {
|
||||
auto expected_chunk_size = std::min<size_t>(BUFFER_SIZE, length - bytes_received);
|
||||
int received = httpd_req_recv(request, buffer, expected_chunk_size);
|
||||
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
// Timeout - retry with backoff, same as receiveByteArray(). A large file takes many
|
||||
// more chunks (and much longer overall) than the small reads elsewhere in this file,
|
||||
// so it's far more likely to hit at least one transient stall somewhere along the way.
|
||||
timeout_retries++;
|
||||
if (timeout_retries >= MAX_TIMEOUT_RETRIES) {
|
||||
LOG_E(TAG, "Recv timeout after %d retries, wrote %zu/%zu bytes", timeout_retries, bytes_received, length);
|
||||
break;
|
||||
}
|
||||
LOG_W(TAG, "Recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES);
|
||||
vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Exponential backoff
|
||||
continue;
|
||||
}
|
||||
if (received <= 0) {
|
||||
LOG_E(TAG, "Receive failed, got 0 bytes but expected %zu more", length - bytes_received);
|
||||
break;
|
||||
}
|
||||
timeout_retries = 0;
|
||||
size_t receive_chunk_size = (size_t)received;
|
||||
|
||||
if (fwrite(buffer, 1, receive_chunk_size, file) != receive_chunk_size) {
|
||||
LOG_E(TAG, "Failed to write all bytes");
|
||||
break;
|
||||
}
|
||||
bytes_received += receive_chunk_size;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@@ -57,7 +57,8 @@ std::string urlEncode(const std::string& input) {
|
||||
// Adapted from https://stackoverflow.com/a/29962178/3848666
|
||||
std::string urlDecode(const std::string& input) {
|
||||
std::string result;
|
||||
size_t conversion_buffer, input_length = input.length();
|
||||
unsigned int conversion_buffer;
|
||||
size_t input_length = input.length();
|
||||
|
||||
for (size_t i = 0; i < input_length; i++) {
|
||||
if (input[i] != '%') {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <app/install.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/start.h>
|
||||
@@ -9,13 +7,16 @@
|
||||
#include <Tactility/DeprecatedPaths.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/network/HttpServerReq.h>
|
||||
#include <Tactility/network/HttpdReq.h>
|
||||
#include <Tactility/network/Url.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/service/development/DevelopmentService.h>
|
||||
#include <Tactility/service/development/DevelopmentSettings.h>
|
||||
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
|
||||
namespace tt::service::development {
|
||||
@@ -24,10 +25,38 @@ extern const ServiceManifest manifest;
|
||||
|
||||
constexpr auto* TAG = "DevService";
|
||||
|
||||
DevelopmentService::DevelopmentService() {
|
||||
HttpServerRequestHandler handlers[] = {
|
||||
{ .uri = "/info", .method = HTTP_METHOD_GET, .callback = handleGetInfo, .user_ctx = this },
|
||||
{ .uri = "/app/run", .method = HTTP_METHOD_POST, .callback = handleAppRun, .user_ctx = this },
|
||||
{ .uri = "/app/install", .method = HTTP_METHOD_PUT, .callback = handleAppInstall, .user_ctx = this },
|
||||
{ .uri = "/app/uninstall", .method = HTTP_METHOD_PUT, .callback = handleAppUninstall, .user_ctx = this },
|
||||
};
|
||||
HttpServerConfig config {
|
||||
.port = 6666,
|
||||
.address = "0.0.0.0",
|
||||
.stack_size = 5120,
|
||||
.handlers = handlers,
|
||||
.handler_count = std::size(handlers),
|
||||
};
|
||||
httpServer = http_server_alloc(&config);
|
||||
if (httpServer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate http server");
|
||||
}
|
||||
}
|
||||
|
||||
DevelopmentService::~DevelopmentService() {
|
||||
http_server_free(httpServer);
|
||||
}
|
||||
|
||||
bool DevelopmentService::onStart(ServiceContext& service) {
|
||||
std::stringstream stream;
|
||||
stream << "{";
|
||||
#ifdef ESP_PLATFORM
|
||||
stream << "\"cpuFamily\":\"" << CONFIG_IDF_TARGET << "\", ";
|
||||
#else
|
||||
stream << "\"cpuFamily\":\"" << CONFIG_TT_DEVICE_ID << "\", ";
|
||||
#endif
|
||||
stream << "\"osVersion\":\"" << TT_VERSION << "\", ";
|
||||
stream << "\"protocolVersion\":\"1.0.0\"";
|
||||
stream << "}";
|
||||
@@ -45,61 +74,64 @@ void DevelopmentService::onStop(ServiceContext& service) {
|
||||
// region Enable/disable
|
||||
|
||||
void DevelopmentService::setEnabled(bool enabled) {
|
||||
if (httpServer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
if (enabled) {
|
||||
if (!httpServer.isStarted()) {
|
||||
httpServer.start();
|
||||
if (!http_server_is_started(httpServer)) {
|
||||
http_server_start(httpServer);
|
||||
}
|
||||
} else {
|
||||
if (httpServer.isStarted()) {
|
||||
httpServer.stop();
|
||||
if (http_server_is_started(httpServer)) {
|
||||
http_server_stop(httpServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DevelopmentService::isEnabled() const {
|
||||
if (httpServer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
return httpServer.isStarted();
|
||||
return http_server_is_started(httpServer);
|
||||
}
|
||||
|
||||
// region endpoints
|
||||
|
||||
esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleGetInfo(HttpServerRequest* request, void* user_ctx) {
|
||||
LOG_I(TAG, "GET /device");
|
||||
|
||||
if (httpd_resp_set_type(request, "application/json") != ESP_OK) {
|
||||
LOG_W(TAG, "Failed to send header");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto* service = static_cast<DevelopmentService*>(request->user_ctx);
|
||||
|
||||
if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) {
|
||||
auto* service = static_cast<DevelopmentService*>(user_ctx);
|
||||
http_server_request_set_content_type(request, "application/json");
|
||||
if (http_server_request_send_string(request, service->deviceResponse.c_str()) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Failed to send response body");
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "[200] /device");
|
||||
return ESP_OK;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleAppRun(HttpServerRequest* request, void*) {
|
||||
LOG_I(TAG, "POST /app/run");
|
||||
|
||||
std::string query;
|
||||
if (!network::getQueryOrSendError(request, query)) {
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
auto parameters = network::parseUrlQuery(query);
|
||||
auto id_key_pos = parameters.find("id");
|
||||
if (id_key_pos == parameters.end()) {
|
||||
LOG_W(TAG, "[400] /app/run id not specified");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "id not specified");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
char app_id[32];
|
||||
@@ -117,34 +149,40 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
app_start(id_key_pos->second.c_str(), 0, nullptr, &instance_id);
|
||||
|
||||
LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
|
||||
return ESP_OK;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleAppInstall(HttpServerRequest* request, void*) {
|
||||
LOG_I(TAG, "PUT /app/install");
|
||||
|
||||
std::string boundary;
|
||||
if (!network::getMultiPartBoundaryOrSendError(request, boundary)) {
|
||||
return false;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
size_t content_left = request->content_len;
|
||||
size_t content_left = http_server_request_get_content_length(request);
|
||||
|
||||
// Skip newline after reading boundary
|
||||
auto content_headers_data = network::receiveTextUntil(request, "\r\n\r\n");
|
||||
if (content_headers_data.empty()) {
|
||||
http_server_request_send_error(request, 400, "Multipart form error: preamble too long or unterminated");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
content_left -= content_headers_data.length();
|
||||
auto content_headers = string::split(content_headers_data, "\r\n")
|
||||
| std::views::filter([](const std::string& line) {
|
||||
return line.length() > 0;
|
||||
})
|
||||
| std::ranges::to<std::vector>();
|
||||
auto content_header_lines = string::split(content_headers_data, "\r\n");
|
||||
std::vector<std::string> content_headers;
|
||||
for (auto& line : content_header_lines) {
|
||||
if (!line.empty()) {
|
||||
content_headers.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
auto content_disposition_map = network::parseContentDisposition(content_headers);
|
||||
if (content_disposition_map.empty()) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: invalid content disposition");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "Multipart form error: invalid content disposition");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
auto name_entry = content_disposition_map.find("name");
|
||||
@@ -154,8 +192,8 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
filename_entry == content_disposition_map.end() ||
|
||||
name_entry->second != "elf"
|
||||
) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: name or filename parameter missing or mismatching");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "Multipart form error: name or filename parameter missing or mismatching");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
// Receive boundary
|
||||
@@ -165,28 +203,28 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
// Create tmp directory
|
||||
const std::string tmp_path = getTempPath();
|
||||
if (!file::findOrCreateDirectory(tmp_path, 0777)) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create temp path");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to create temp path");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
std::string safe_name = file::getLastPathSegment(filename_entry->second);
|
||||
if (safe_name.empty() || safe_name.find("..") != std::string::npos ||
|
||||
safe_name.find('/') != std::string::npos || safe_name.find('\\') != std::string::npos) {
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "invalid filename");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "invalid filename");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
auto file_path = std::format("{}/{}", tmp_path, safe_name);
|
||||
if (network::receiveFile(request, file_size, file_path) != file_size) {
|
||||
file::deleteFile(file_path);
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to receive file");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
content_left -= file_size;
|
||||
|
||||
// Read and verify part
|
||||
if (!network::readAndDiscardOrSendError(request, boundary_and_newlines_after_file)) {
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
content_left -= boundary_and_newlines_after_file.length();
|
||||
|
||||
@@ -195,8 +233,8 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
}
|
||||
|
||||
if (app_install(file_path.c_str()) != ERROR_NONE) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to install");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to install");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
if (!file::deleteFile(file_path)) {
|
||||
@@ -205,42 +243,42 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
|
||||
LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str());
|
||||
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
|
||||
return ESP_OK;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppUninstall(httpd_req_t* request) {
|
||||
error_t DevelopmentService::handleAppUninstall(HttpServerRequest* request, void*) {
|
||||
LOG_I(TAG, "PUT /app/uninstall");
|
||||
|
||||
std::string query;
|
||||
if (!network::getQueryOrSendError(request, query)) {
|
||||
return ESP_FAIL;
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
auto parameters = network::parseUrlQuery(query);
|
||||
auto id_key_pos = parameters.find("id");
|
||||
if (id_key_pos == parameters.end()) {
|
||||
LOG_W(TAG, "[400] /app/uninstall id not specified");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 400, "id not specified");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
|
||||
AppManifest manifest;
|
||||
if (app_manager_find_manifest(id_key_pos->second.c_str(), &manifest) != ERROR_NONE) {
|
||||
LOG_I(TAG, "[200] /app/uninstall %s (app wasn't installed)", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (app_uninstall(id_key_pos->second.c_str()) == ERROR_NONE) {
|
||||
LOG_I(TAG, "[200] /app/uninstall %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
http_server_request_send(request, nullptr, 0);
|
||||
return ERROR_NONE;
|
||||
} else {
|
||||
LOG_W(TAG, "[500] /app/uninstall %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to uninstall");
|
||||
return ESP_FAIL;
|
||||
http_server_request_send_error(request, 500, "Failed to uninstall");
|
||||
return ERROR_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,5 +296,3 @@ extern const ServiceManifest manifest = {
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/service/development/DevelopmentSettings.h>
|
||||
@@ -72,5 +71,3 @@ bool shouldEnableOnBoot() {
|
||||
return settings.enableOnBoot;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user