Add authenticated WebSocket serial terminal - dirty commit with front-
and backend issues
This commit is contained in:
+185
-33
@@ -21,28 +21,24 @@
|
||||
#include "session_broker.h"
|
||||
#include "usb_cdc_transport.h"
|
||||
#include "web_security.h"
|
||||
#include "web_serial_transport.h"
|
||||
#include "web_ui.h"
|
||||
#include "wifi_manager.h"
|
||||
|
||||
#define WEB_SERVER_PORT 443U
|
||||
#define WEB_SERVER_MAX_AUTHORIZATION 128U
|
||||
#define WEB_SERVER_MAX_BASIC_DECODED 64U
|
||||
#define WEB_SERVER_STATUS_JSON_CAPACITY 2304U
|
||||
|
||||
static const char s_index_html[] =
|
||||
"<!doctype html><html lang=en><meta charset=utf-8>"
|
||||
"<meta name=viewport content=\"width=device-width,initial-scale=1\">"
|
||||
"<title>ESP32 Serial Swiss Army Knife</title>"
|
||||
"<style>body{font:16px system-ui;max-width:54rem;margin:3rem auto;padding:0 1rem}"
|
||||
"pre{background:#171717;color:#eee;padding:1rem;overflow:auto}</style>"
|
||||
"<h1>ESP32 Serial Swiss Army Knife</h1>"
|
||||
"<p>Authenticated HTTPS is operational. Interactive web serial access is not enabled yet.</p>"
|
||||
"<p><a href=/api/status>JSON status</a></p></html>";
|
||||
#define WEB_SERVER_STATUS_JSON_CAPACITY 3072U
|
||||
|
||||
static SemaphoreHandle_t s_server_mutex;
|
||||
static httpd_handle_t s_server;
|
||||
static bool s_initialized;
|
||||
static bool s_transitioning;
|
||||
static bool s_serial_transport_init_attempted;
|
||||
static bool s_serial_transport_initialized;
|
||||
static bool s_serial_transport_attached;
|
||||
static esp_err_t s_last_error = ESP_ERR_INVALID_STATE;
|
||||
static esp_err_t s_serial_transport_error = ESP_ERR_INVALID_STATE;
|
||||
static web_server_counters_t s_counters;
|
||||
|
||||
static esp_err_t ensure_mutex(void)
|
||||
@@ -167,33 +163,62 @@ static esp_err_t authorize_or_respond(httpd_req_t *request, bool *authorized)
|
||||
return *authorized ? ESP_OK : send_authentication_required(request);
|
||||
}
|
||||
|
||||
static esp_err_t root_handler(httpd_req_t *request)
|
||||
static esp_err_t send_authenticated_ui(httpd_req_t *request,
|
||||
web_ui_resource_t resource,
|
||||
uint64_t *counter)
|
||||
{
|
||||
bool authorized = false;
|
||||
esp_err_t error = authorize_or_respond(request, &authorized);
|
||||
if (error != ESP_OK || !authorized) {
|
||||
return error;
|
||||
}
|
||||
increment_counter(&s_counters.root_requests);
|
||||
increment_counter(counter);
|
||||
|
||||
error = httpd_resp_set_type(request, "text/html; charset=utf-8");
|
||||
if (error == ESP_OK) {
|
||||
error = set_common_headers(request);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_set_hdr(
|
||||
request, "Content-Security-Policy",
|
||||
"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'");
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_resp_send(request, s_index_html, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
error = web_ui_send_response(request, resource);
|
||||
if (error != ESP_OK) {
|
||||
increment_counter(&s_counters.response_errors);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t root_handler(httpd_req_t *request)
|
||||
{
|
||||
return send_authenticated_ui(request, WEB_UI_RESOURCE_INDEX,
|
||||
&s_counters.root_requests);
|
||||
}
|
||||
|
||||
static esp_err_t asset_handler(httpd_req_t *request)
|
||||
{
|
||||
web_ui_resource_t resource = (web_ui_resource_t)(uintptr_t)request->user_ctx;
|
||||
return send_authenticated_ui(request, resource, &s_counters.asset_requests);
|
||||
}
|
||||
|
||||
static esp_err_t ticket_handler(httpd_req_t *request)
|
||||
{
|
||||
bool authorized = false;
|
||||
esp_err_t error = authorize_or_respond(request, &authorized);
|
||||
if (error != ESP_OK || !authorized) {
|
||||
return error;
|
||||
}
|
||||
increment_counter(&s_counters.ticket_requests);
|
||||
if (request->content_len != 0U) {
|
||||
return send_plain_error(request, "400 Bad Request",
|
||||
"Ticket requests must have an empty body.\n");
|
||||
}
|
||||
|
||||
error = web_serial_transport_handle_authenticated_ticket_request(request);
|
||||
if (error == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (error == ESP_ERR_INVALID_ARG) {
|
||||
return send_plain_error(request, "400 Bad Request",
|
||||
"Invalid web-terminal ticket request.\n");
|
||||
}
|
||||
increment_counter(&s_counters.response_errors);
|
||||
return send_plain_error(request, "503 Service Unavailable",
|
||||
"Web terminal transport unavailable.\n");
|
||||
}
|
||||
|
||||
static const char *safe_string(const char *value)
|
||||
{
|
||||
return value != NULL ? value : "unknown";
|
||||
@@ -242,6 +267,7 @@ static esp_err_t status_handler(httpd_req_t *request)
|
||||
session_broker_global_snapshot_t broker = {0};
|
||||
usb_cdc_transport_snapshot_t usb = {0};
|
||||
web_server_snapshot_t web = {0};
|
||||
web_serial_transport_snapshot_t web_serial = {0};
|
||||
web_security_certificate_metadata_t certificate = {0};
|
||||
char ipv4[16] = {0};
|
||||
char fingerprint[WEB_SECURITY_SHA256_LENGTH * 3U] = {0};
|
||||
@@ -253,6 +279,8 @@ static esp_err_t status_handler(httpd_req_t *request)
|
||||
bool broker_available = session_broker_get_global_snapshot(&broker) == ESP_OK;
|
||||
bool usb_available = usb_cdc_transport_get_snapshot(&usb) == ESP_OK;
|
||||
bool web_available = web_server_get_snapshot(&web) == ESP_OK;
|
||||
bool web_serial_available =
|
||||
web_serial_transport_get_snapshot(&web_serial) == ESP_OK;
|
||||
bool certificate_available =
|
||||
web_security_get_certificate_metadata(&certificate) == ESP_OK;
|
||||
|
||||
@@ -278,7 +306,10 @@ static esp_err_t status_handler(httpd_req_t *request)
|
||||
" \"usb\":{\"available\":%s,\"attached\":%s,\"host_open\":%s,\"writer\":%s},\n"
|
||||
" \"https\":{\"running\":%s,\"requests\":%" PRIu64 ","
|
||||
"\"authenticated_requests\":%" PRIu64 ",\"authentication_failures\":%" PRIu64 ","
|
||||
"\"certificate_sha256\":\"%s\"}\n"
|
||||
"\"certificate_sha256\":\"%s\"},\n"
|
||||
" \"websocket\":{\"available\":%s,\"sessions\":%" PRIu32 ","
|
||||
"\"active_tickets\":%" PRIu32 ",\"rx_bytes\":%" PRIu64 ","
|
||||
"\"rx_rejected\":%" PRIu64 ",\"tx_bytes\":%" PRIu64 "}\n"
|
||||
"}\n",
|
||||
(uint64_t)(esp_timer_get_time() / 1000),
|
||||
wifi_available ? "true" : "false",
|
||||
@@ -309,7 +340,16 @@ static esp_err_t status_handler(httpd_req_t *request)
|
||||
web_available ? web.counters.requests : 0U,
|
||||
web_available ? web.counters.authenticated_requests : 0U,
|
||||
web_available ? web.counters.authentication_failures : 0U,
|
||||
fingerprint);
|
||||
fingerprint,
|
||||
web_serial_available ? "true" : "false",
|
||||
web_serial_available ? web_serial.active_sessions : 0U,
|
||||
web_serial_available ? web_serial.active_tickets : 0U,
|
||||
web_serial_available ? web_serial.counters.rx_ws_bytes_accepted : 0U,
|
||||
web_serial_available ? web_serial.counters.rx_ws_bytes_rejected : 0U,
|
||||
web_serial_available
|
||||
? web_serial.counters.tx_binary_bytes +
|
||||
web_serial.counters.tx_control_bytes
|
||||
: 0U);
|
||||
|
||||
if (written < 0 || (size_t)written >= sizeof(response)) {
|
||||
return send_plain_error(request, "500 Internal Server Error",
|
||||
@@ -342,18 +382,93 @@ static const httpd_uri_t s_status_uri = {
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_ticket_uri = {
|
||||
.uri = WEB_SERIAL_TRANSPORT_TICKET_URI,
|
||||
.method = HTTP_POST,
|
||||
.handler = ticket_handler,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_websocket_uri = {
|
||||
.uri = WEB_SERIAL_TRANSPORT_WS_URI,
|
||||
.method = HTTP_GET,
|
||||
.handler = web_serial_transport_ws_handler,
|
||||
.user_ctx = NULL,
|
||||
.is_websocket = true,
|
||||
.handle_ws_control_frames = false,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_xterm_js_uri = {
|
||||
.uri = "/assets/xterm.js",
|
||||
.method = HTTP_GET,
|
||||
.handler = asset_handler,
|
||||
.user_ctx = (void *)(uintptr_t)WEB_UI_RESOURCE_XTERM_JS,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_xterm_css_uri = {
|
||||
.uri = "/assets/xterm.css",
|
||||
.method = HTTP_GET,
|
||||
.handler = asset_handler,
|
||||
.user_ctx = (void *)(uintptr_t)WEB_UI_RESOURCE_XTERM_CSS,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_addon_fit_js_uri = {
|
||||
.uri = "/assets/addon-fit.js",
|
||||
.method = HTTP_GET,
|
||||
.handler = asset_handler,
|
||||
.user_ctx = (void *)(uintptr_t)WEB_UI_RESOURCE_ADDON_FIT_JS,
|
||||
};
|
||||
|
||||
static const httpd_uri_t s_app_js_uri = {
|
||||
.uri = "/assets/app.js",
|
||||
.method = HTTP_GET,
|
||||
.handler = asset_handler,
|
||||
.user_ctx = (void *)(uintptr_t)WEB_UI_RESOURCE_APP_JS,
|
||||
};
|
||||
|
||||
static const httpd_uri_t *const s_uri_handlers[] = {
|
||||
&s_root_uri,
|
||||
&s_status_uri,
|
||||
&s_ticket_uri,
|
||||
&s_websocket_uri,
|
||||
&s_xterm_js_uri,
|
||||
&s_xterm_css_uri,
|
||||
&s_addon_fit_js_uri,
|
||||
&s_app_js_uri,
|
||||
};
|
||||
|
||||
esp_err_t web_server_init(void)
|
||||
{
|
||||
esp_err_t error = ensure_mutex();
|
||||
if (error != ESP_OK) {
|
||||
return error;
|
||||
}
|
||||
|
||||
bool initialize_serial_transport = false;
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
if (!s_serial_transport_init_attempted) {
|
||||
s_serial_transport_init_attempted = true;
|
||||
initialize_serial_transport = true;
|
||||
}
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
esp_err_t serial_transport_error = ESP_OK;
|
||||
if (initialize_serial_transport) {
|
||||
serial_transport_error = web_serial_transport_init();
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
if (initialize_serial_transport) {
|
||||
s_serial_transport_error = serial_transport_error;
|
||||
s_serial_transport_initialized = serial_transport_error == ESP_OK;
|
||||
}
|
||||
s_initialized = true;
|
||||
if (s_last_error == ESP_ERR_INVALID_STATE) {
|
||||
s_last_error = ESP_OK;
|
||||
}
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
/* The Phase 5A HTTPS recovery surface remains available if WebSocket setup fails. */
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -364,12 +479,14 @@ esp_err_t web_server_start(void)
|
||||
return error;
|
||||
}
|
||||
|
||||
bool serial_transport_ready;
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
if (s_server != NULL || s_transitioning) {
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
s_transitioning = true;
|
||||
serial_transport_ready = s_serial_transport_initialized;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
uint8_t certificate[WEB_SECURITY_CERTIFICATE_DER_CAPACITY] = {0};
|
||||
@@ -383,9 +500,13 @@ esp_err_t web_server_start(void)
|
||||
private_key, sizeof(private_key), &private_key_length);
|
||||
if (error == ESP_OK) {
|
||||
httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT();
|
||||
config.httpd.max_open_sockets = 2;
|
||||
config.httpd.max_uri_handlers = 2;
|
||||
/* Two browser terminals retain room for parallel assets and status fetches. */
|
||||
config.httpd.max_open_sockets = 6;
|
||||
config.httpd.max_uri_handlers =
|
||||
sizeof(s_uri_handlers) / sizeof(s_uri_handlers[0]);
|
||||
config.httpd.lru_purge_enable = true;
|
||||
config.httpd.recv_wait_timeout = 1;
|
||||
config.httpd.send_wait_timeout = 1;
|
||||
config.servercert = certificate;
|
||||
config.servercert_len = certificate_length;
|
||||
config.prvtkey_pem = private_key;
|
||||
@@ -397,11 +518,18 @@ esp_err_t web_server_start(void)
|
||||
secure_wipe(certificate, sizeof(certificate));
|
||||
secure_wipe(private_key, sizeof(private_key));
|
||||
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_register_uri_handler(server, &s_root_uri);
|
||||
for (size_t index = 0U;
|
||||
error == ESP_OK &&
|
||||
index < sizeof(s_uri_handlers) / sizeof(s_uri_handlers[0]);
|
||||
++index) {
|
||||
error = httpd_register_uri_handler(server, s_uri_handlers[index]);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = httpd_register_uri_handler(server, &s_status_uri);
|
||||
|
||||
bool serial_transport_attached = false;
|
||||
esp_err_t attach_error = s_serial_transport_error;
|
||||
if (error == ESP_OK && serial_transport_ready) {
|
||||
attach_error = web_serial_transport_attach_server(server);
|
||||
serial_transport_attached = attach_error == ESP_OK;
|
||||
}
|
||||
if (error != ESP_OK && server != NULL) {
|
||||
(void)httpd_ssl_stop(server);
|
||||
@@ -411,6 +539,8 @@ esp_err_t web_server_start(void)
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_transitioning = false;
|
||||
s_last_error = error;
|
||||
s_serial_transport_error = attach_error;
|
||||
s_serial_transport_attached = serial_transport_attached;
|
||||
if (error == ESP_OK) {
|
||||
s_server = server;
|
||||
++s_counters.starts;
|
||||
@@ -433,14 +563,35 @@ esp_err_t web_server_stop(void)
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
httpd_handle_t server = s_server;
|
||||
bool serial_transport_attached = s_serial_transport_attached;
|
||||
esp_err_t serial_transport_error = s_serial_transport_error;
|
||||
s_transitioning = true;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
if (serial_transport_attached) {
|
||||
esp_err_t detach_error = web_serial_transport_detach_server(server);
|
||||
if (detach_error != ESP_OK && detach_error != ESP_ERR_TIMEOUT) {
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_transitioning = false;
|
||||
s_last_error = detach_error;
|
||||
s_serial_transport_error = detach_error;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return detach_error;
|
||||
}
|
||||
serial_transport_error = detach_error;
|
||||
}
|
||||
|
||||
esp_err_t error = httpd_ssl_stop(server);
|
||||
if (error != ESP_OK && serial_transport_attached) {
|
||||
/* Stay detached: old HTTPD work may still be reading static TX storage. */
|
||||
serial_transport_error = ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_transitioning = false;
|
||||
s_last_error = error;
|
||||
s_serial_transport_error = serial_transport_error;
|
||||
s_serial_transport_attached = false;
|
||||
if (error == ESP_OK) {
|
||||
s_server = NULL;
|
||||
++s_counters.stops;
|
||||
@@ -465,6 +616,7 @@ esp_err_t web_server_get_snapshot(web_server_snapshot_t *snapshot)
|
||||
snapshot->transitioning = s_transitioning;
|
||||
snapshot->port = WEB_SERVER_PORT;
|
||||
snapshot->last_error = s_last_error;
|
||||
snapshot->serial_transport_error = s_serial_transport_error;
|
||||
snapshot->counters = s_counters;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return ESP_OK;
|
||||
|
||||
Reference in New Issue
Block a user