805 lines
28 KiB
C
805 lines
28 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* TLS-only HTTP server with bounded Basic authentication and status output. */
|
|
|
|
#include "web_server.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
#include "esp_http_server.h"
|
|
#include "esp_https_server.h"
|
|
#include "esp_netif_ip_addr.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "mbedtls/base64.h"
|
|
#include "mbedtls/md.h"
|
|
#include "secure_random.h"
|
|
#include "serial_config.h"
|
|
#include "serial_service.h"
|
|
#include "session_broker.h"
|
|
#include "usb_cdc_transport.h"
|
|
#include "user_database.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_BASIC_DECODED \
|
|
(USER_DATABASE_USERNAME_CAPACITY + 1U + USER_DATABASE_PASSWORD_CAPACITY)
|
|
#define WEB_SERVER_MAX_BASIC_ENCODED \
|
|
(((WEB_SERVER_MAX_BASIC_DECODED + 2U) / 3U) * 4U)
|
|
#define WEB_SERVER_MAX_AUTHORIZATION \
|
|
((sizeof("Basic ") - 1U) + WEB_SERVER_MAX_BASIC_ENCODED + 1U)
|
|
#define WEB_SERVER_STATUS_JSON_CAPACITY 3072U
|
|
#define WEB_SERVER_AUTH_CACHE_ENTRIES 4U
|
|
#define WEB_SERVER_AUTH_CACHE_KEY_LENGTH 32U
|
|
#define WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH 32U
|
|
#define WEB_SERVER_AUTH_CACHE_TTL_US 300000000LL
|
|
|
|
typedef struct {
|
|
bool active;
|
|
int64_t expires_at_us;
|
|
uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH];
|
|
user_principal_t principal;
|
|
} web_server_auth_cache_entry_t;
|
|
|
|
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 bool s_auth_cache_ready;
|
|
static uint8_t s_auth_cache_key[WEB_SERVER_AUTH_CACHE_KEY_LENGTH];
|
|
static web_server_auth_cache_entry_t
|
|
s_auth_cache[WEB_SERVER_AUTH_CACHE_ENTRIES];
|
|
|
|
static esp_err_t ensure_mutex(void)
|
|
{
|
|
if (s_server_mutex != NULL) {
|
|
return ESP_OK;
|
|
}
|
|
s_server_mutex = xSemaphoreCreateMutex();
|
|
return s_server_mutex != NULL ? ESP_OK : ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
static void increment_counter(uint64_t *counter)
|
|
{
|
|
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
|
++*counter;
|
|
xSemaphoreGive(s_server_mutex);
|
|
}
|
|
|
|
static esp_err_t set_common_headers(httpd_req_t *request)
|
|
{
|
|
esp_err_t error = httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
|
if (error == ESP_OK) {
|
|
error = httpd_resp_set_hdr(request, "X-Content-Type-Options", "nosniff");
|
|
}
|
|
if (error == ESP_OK) {
|
|
error = httpd_resp_set_hdr(request, "Referrer-Policy", "no-referrer");
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static esp_err_t send_plain_error(httpd_req_t *request,
|
|
const char *status,
|
|
const char *message)
|
|
{
|
|
esp_err_t error = httpd_resp_set_status(request, status);
|
|
if (error == ESP_OK) {
|
|
error = httpd_resp_set_type(request, "text/plain; charset=utf-8");
|
|
}
|
|
if (error == ESP_OK) {
|
|
error = set_common_headers(request);
|
|
}
|
|
if (error == ESP_OK) {
|
|
error = httpd_resp_sendstr(request, message);
|
|
}
|
|
if (error != ESP_OK) {
|
|
increment_counter(&s_counters.response_errors);
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static esp_err_t send_authentication_required(httpd_req_t *request)
|
|
{
|
|
esp_err_t error = httpd_resp_set_hdr(
|
|
request, "WWW-Authenticate",
|
|
"Basic realm=\"ESP32-SAK\", charset=\"UTF-8\"");
|
|
if (error != ESP_OK) {
|
|
increment_counter(&s_counters.response_errors);
|
|
return error;
|
|
}
|
|
return send_plain_error(request, "401 Unauthorized", "Authentication required.\n");
|
|
}
|
|
|
|
static bool constant_time_equal(const uint8_t *left, const uint8_t *right,
|
|
size_t length)
|
|
{
|
|
uint8_t difference = 0U;
|
|
for (size_t index = 0U; index < length; ++index) {
|
|
difference |= left[index] ^ right[index];
|
|
}
|
|
return difference == 0U;
|
|
}
|
|
|
|
static esp_err_t calculate_auth_cache_digest(
|
|
const char *authorization, size_t authorization_length,
|
|
uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH])
|
|
{
|
|
if (!s_auth_cache_ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
|
if (info == NULL ||
|
|
mbedtls_md_hmac(info, s_auth_cache_key, sizeof(s_auth_cache_key),
|
|
(const uint8_t *)authorization, authorization_length,
|
|
digest) != 0) {
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static bool authenticate_from_cache(
|
|
const uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH],
|
|
user_principal_t *principal)
|
|
{
|
|
int64_t now = esp_timer_get_time();
|
|
for (size_t index = 0U; index < WEB_SERVER_AUTH_CACHE_ENTRIES; ++index) {
|
|
web_server_auth_cache_entry_t *entry = &s_auth_cache[index];
|
|
if (!entry->active || entry->expires_at_us <= now ||
|
|
!constant_time_equal(entry->digest, digest, sizeof(entry->digest))) {
|
|
if (entry->active && entry->expires_at_us <= now) {
|
|
secure_wipe(entry, sizeof(*entry));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
bool current = false;
|
|
if (user_database_principal_is_current(&entry->principal, ¤t) == ESP_OK &&
|
|
current) {
|
|
*principal = entry->principal;
|
|
entry->expires_at_us = now + WEB_SERVER_AUTH_CACHE_TTL_US;
|
|
return true;
|
|
}
|
|
secure_wipe(entry, sizeof(*entry));
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void store_authenticated_request(
|
|
const uint8_t digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH],
|
|
const user_principal_t *principal)
|
|
{
|
|
int64_t now = esp_timer_get_time();
|
|
size_t selected = 0U;
|
|
int64_t earliest_expiry = INT64_MAX;
|
|
for (size_t index = 0U; index < WEB_SERVER_AUTH_CACHE_ENTRIES; ++index) {
|
|
web_server_auth_cache_entry_t *entry = &s_auth_cache[index];
|
|
if (entry->active &&
|
|
constant_time_equal(entry->digest, digest, sizeof(entry->digest))) {
|
|
selected = index;
|
|
break;
|
|
}
|
|
if (!entry->active || entry->expires_at_us <= now) {
|
|
selected = index;
|
|
earliest_expiry = INT64_MIN;
|
|
} else if (earliest_expiry != INT64_MIN &&
|
|
entry->expires_at_us < earliest_expiry) {
|
|
selected = index;
|
|
earliest_expiry = entry->expires_at_us;
|
|
}
|
|
}
|
|
|
|
web_server_auth_cache_entry_t *entry = &s_auth_cache[selected];
|
|
secure_wipe(entry, sizeof(*entry));
|
|
entry->active = true;
|
|
entry->expires_at_us = now + WEB_SERVER_AUTH_CACHE_TTL_US;
|
|
memcpy(entry->digest, digest, sizeof(entry->digest));
|
|
entry->principal = *principal;
|
|
}
|
|
|
|
static esp_err_t authenticate_request(httpd_req_t *request,
|
|
user_principal_t *principal,
|
|
bool *authenticated)
|
|
{
|
|
char authorization[WEB_SERVER_MAX_AUTHORIZATION] = {0};
|
|
uint8_t decoded[WEB_SERVER_MAX_BASIC_DECODED] = {0};
|
|
size_t decoded_length = 0U;
|
|
uint8_t cache_digest[WEB_SERVER_AUTH_CACHE_DIGEST_LENGTH] = {0};
|
|
bool cache_digest_valid = false;
|
|
esp_err_t result = ESP_OK;
|
|
memset(principal, 0, sizeof(*principal));
|
|
*authenticated = false;
|
|
|
|
increment_counter(&s_counters.requests);
|
|
size_t header_length = httpd_req_get_hdr_value_len(request, "Authorization");
|
|
if (header_length == 0U || header_length >= sizeof(authorization)) {
|
|
goto cleanup;
|
|
}
|
|
if (httpd_req_get_hdr_value_str(request, "Authorization",
|
|
authorization, sizeof(authorization)) != ESP_OK ||
|
|
header_length < 7U || strncasecmp(authorization, "Basic ", 6U) != 0) {
|
|
goto cleanup;
|
|
}
|
|
|
|
result = calculate_auth_cache_digest(authorization, header_length, cache_digest);
|
|
if (result != ESP_OK) {
|
|
goto cleanup;
|
|
}
|
|
cache_digest_valid = true;
|
|
if (authenticate_from_cache(cache_digest, principal)) {
|
|
*authenticated = true;
|
|
goto cleanup;
|
|
}
|
|
|
|
int decode_result = mbedtls_base64_decode(
|
|
decoded, sizeof(decoded), &decoded_length,
|
|
(const unsigned char *)authorization + 6U, header_length - 6U);
|
|
if (decode_result != 0 || decoded_length == 0U) {
|
|
goto cleanup;
|
|
}
|
|
|
|
uint8_t *separator = memchr(decoded, ':', decoded_length);
|
|
if (separator == NULL) {
|
|
goto cleanup;
|
|
}
|
|
size_t username_length = (size_t)(separator - decoded);
|
|
size_t password_length = decoded_length - username_length - 1U;
|
|
result = user_database_authenticate_password(
|
|
decoded, username_length, separator + 1U, password_length,
|
|
principal, authenticated);
|
|
if (result == ESP_OK && *authenticated && cache_digest_valid) {
|
|
store_authenticated_request(cache_digest, principal);
|
|
}
|
|
|
|
cleanup:
|
|
secure_wipe(authorization, sizeof(authorization));
|
|
secure_wipe(decoded, sizeof(decoded));
|
|
secure_wipe(cache_digest, sizeof(cache_digest));
|
|
if (result != ESP_OK) {
|
|
memset(principal, 0, sizeof(*principal));
|
|
return result;
|
|
}
|
|
if (*authenticated) {
|
|
increment_counter(&s_counters.authenticated_requests);
|
|
} else {
|
|
memset(principal, 0, sizeof(*principal));
|
|
increment_counter(&s_counters.authentication_failures);
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t authorize_or_respond(httpd_req_t *request,
|
|
user_principal_t *principal,
|
|
bool *authorized)
|
|
{
|
|
*authorized = false;
|
|
esp_err_t error = authenticate_request(request, principal, authorized);
|
|
if (error != ESP_OK) {
|
|
*authorized = false;
|
|
return send_plain_error(request, "503 Service Unavailable",
|
|
"Authentication service unavailable.\n");
|
|
}
|
|
return *authorized ? ESP_OK : send_authentication_required(request);
|
|
}
|
|
|
|
static esp_err_t send_authenticated_ui(httpd_req_t *request,
|
|
web_ui_resource_t resource,
|
|
uint64_t *counter)
|
|
{
|
|
user_principal_t principal = {0};
|
|
bool authorized = false;
|
|
esp_err_t error = authorize_or_respond(request, &principal, &authorized);
|
|
if (error != ESP_OK || !authorized) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
return error;
|
|
}
|
|
increment_counter(counter);
|
|
|
|
error = web_ui_send_response(request, resource);
|
|
secure_wipe(&principal, sizeof(principal));
|
|
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)
|
|
{
|
|
user_principal_t principal = {0};
|
|
bool authorized = false;
|
|
esp_err_t error = authorize_or_respond(request, &principal, &authorized);
|
|
if (error != ESP_OK || !authorized) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
return error;
|
|
}
|
|
increment_counter(&s_counters.ticket_requests);
|
|
if (request->content_len != 0U) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
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, &principal);
|
|
secure_wipe(&principal, sizeof(principal));
|
|
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";
|
|
}
|
|
|
|
static void format_ipv4(uint32_t address, char output[16])
|
|
{
|
|
if (address == 0U) {
|
|
memcpy(output, "0.0.0.0", sizeof("0.0.0.0"));
|
|
return;
|
|
}
|
|
esp_ip4_addr_t ip = {.addr = address};
|
|
int written = snprintf(output, 16U, IPSTR, IP2STR(&ip));
|
|
if (written < 0 || written >= 16) {
|
|
memcpy(output, "0.0.0.0", sizeof("0.0.0.0"));
|
|
}
|
|
}
|
|
|
|
static void format_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH],
|
|
char output[(WEB_SECURITY_SHA256_LENGTH * 3U)])
|
|
{
|
|
static const char hex[] = "0123456789ABCDEF";
|
|
size_t offset = 0U;
|
|
for (size_t index = 0U; index < WEB_SECURITY_SHA256_LENGTH; ++index) {
|
|
if (index != 0U) {
|
|
output[offset++] = ':';
|
|
}
|
|
output[offset++] = hex[fingerprint[index] >> 4U];
|
|
output[offset++] = hex[fingerprint[index] & 0x0fU];
|
|
}
|
|
output[offset] = '\0';
|
|
}
|
|
|
|
static esp_err_t status_handler(httpd_req_t *request)
|
|
{
|
|
user_principal_t principal = {0};
|
|
bool authorized = false;
|
|
esp_err_t error = authorize_or_respond(request, &principal, &authorized);
|
|
if (error != ESP_OK || !authorized) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
return error;
|
|
}
|
|
secure_wipe(&principal, sizeof(principal));
|
|
increment_counter(&s_counters.status_requests);
|
|
|
|
wifi_manager_snapshot_t wifi = {0};
|
|
serial_config_t serial_config = {0};
|
|
serial_service_counters_t serial_counters = {0};
|
|
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};
|
|
char response[WEB_SERVER_STATUS_JSON_CAPACITY];
|
|
|
|
bool wifi_available = wifi_manager_get_snapshot(&wifi) == ESP_OK;
|
|
bool serial_config_available = serial_service_get_config(&serial_config) == ESP_OK;
|
|
serial_service_get_counters(&serial_counters);
|
|
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;
|
|
|
|
format_ipv4(wifi_available ? wifi.ip : 0U, ipv4);
|
|
if (certificate_available) {
|
|
format_fingerprint(certificate.sha256_fingerprint, fingerprint);
|
|
} else {
|
|
memcpy(fingerprint, "unavailable", sizeof("unavailable"));
|
|
}
|
|
|
|
int written = snprintf(
|
|
response, sizeof(response),
|
|
"{\n"
|
|
" \"uptime_ms\":%" PRIu64 ",\n"
|
|
" \"wifi\":{\"available\":%s,\"state\":\"%s\",\"sta_ipv4\":\"%s\","
|
|
"\"rssi\":%d,\"channel\":%u,\"ap_running\":%s,\"ap_clients\":%u},\n"
|
|
" \"serial\":{\"running\":%s,\"config_available\":%s,\"baud\":%" PRIu32 ","
|
|
"\"data_bits\":\"%s\",\"parity\":\"%s\",\"stop_bits\":\"%s\","
|
|
"\"flow\":\"%s\",\"rx_bytes\":%" PRIu64 ",\"rx_dropped\":%" PRIu64 ","
|
|
"\"tx_sent\":%" PRIu64 ",\"tx_dropped\":%" PRIu64 "},\n"
|
|
" \"broker\":{\"available\":%s,\"clients\":%" PRIu32 ",\"writer\":%" PRIu32 ","
|
|
"\"uart_rx_bytes\":%" PRIu64 ",\"observer_dropped\":%" PRIu64 "},\n"
|
|
" \"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"
|
|
" \"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",
|
|
wifi_available ? wifi_manager_state_to_string(wifi.state) : "unavailable",
|
|
ipv4, wifi_available ? (int)wifi.sta_rssi : 0,
|
|
wifi_available ? (unsigned int)wifi.sta_channel : 0U,
|
|
wifi_available && wifi.ap_running ? "true" : "false",
|
|
wifi_available ? (unsigned int)wifi.ap_client_count : 0U,
|
|
serial_service_is_running() ? "true" : "false",
|
|
serial_config_available ? "true" : "false",
|
|
serial_config_available ? serial_config.baud_rate : 0U,
|
|
serial_config_available ? safe_string(serial_config_data_bits_to_string(serial_config.data_bits)) : "unknown",
|
|
serial_config_available ? safe_string(serial_config_parity_to_string(serial_config.parity)) : "unknown",
|
|
serial_config_available ? safe_string(serial_config_stop_bits_to_string(serial_config.stop_bits)) : "unknown",
|
|
serial_config_available ? safe_string(serial_config_flow_control_to_string(serial_config.flow_control)) : "unknown",
|
|
serial_counters.rx_bytes, serial_counters.rx_dropped_bytes,
|
|
serial_counters.tx_sent_to_uart_bytes, serial_counters.tx_dropped_bytes,
|
|
broker_available ? "true" : "false",
|
|
broker_available ? broker.connected_clients : 0U,
|
|
broker_available ? broker.writer_id : SESSION_BROKER_NO_CLIENT,
|
|
broker_available ? broker.counters.uart_rx_bytes : 0U,
|
|
broker_available ? broker.counters.output_dropped_bytes : 0U,
|
|
usb_available ? "true" : "false",
|
|
usb_available && usb.attached ? "true" : "false",
|
|
usb_available && usb.attached && usb.dtr ? "true" : "false",
|
|
usb_available && usb.writer ? "true" : "false",
|
|
web_available && web.running ? "true" : "false",
|
|
web_available ? web.counters.requests : 0U,
|
|
web_available ? web.counters.authenticated_requests : 0U,
|
|
web_available ? web.counters.authentication_failures : 0U,
|
|
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",
|
|
"Status response overflow.\n");
|
|
}
|
|
error = httpd_resp_set_type(request, "application/json; charset=utf-8");
|
|
if (error == ESP_OK) {
|
|
error = set_common_headers(request);
|
|
}
|
|
if (error == ESP_OK) {
|
|
error = httpd_resp_send(request, response, (ssize_t)written);
|
|
}
|
|
if (error != ESP_OK) {
|
|
increment_counter(&s_counters.response_errors);
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static const httpd_uri_t s_root_uri = {
|
|
.uri = "/",
|
|
.method = HTTP_GET,
|
|
.handler = root_handler,
|
|
.user_ctx = NULL,
|
|
};
|
|
|
|
static const httpd_uri_t s_status_uri = {
|
|
.uri = "/api/status",
|
|
.method = HTTP_GET,
|
|
.handler = status_handler,
|
|
.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 s_logo_uri = {
|
|
.uri = "/assets/logo.png",
|
|
.method = HTTP_GET,
|
|
.handler = asset_handler,
|
|
.user_ctx = (void *)(uintptr_t)WEB_UI_RESOURCE_LOGO_PNG,
|
|
};
|
|
|
|
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,
|
|
&s_logo_uri,
|
|
};
|
|
|
|
esp_err_t web_server_init(void)
|
|
{
|
|
esp_err_t error = ensure_mutex();
|
|
if (error != ESP_OK) {
|
|
return error;
|
|
}
|
|
|
|
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
|
if (!s_auth_cache_ready) {
|
|
error = secure_random_fill(s_auth_cache_key, sizeof(s_auth_cache_key));
|
|
if (error == ESP_OK) {
|
|
secure_wipe(s_auth_cache, sizeof(s_auth_cache));
|
|
s_auth_cache_ready = true;
|
|
}
|
|
}
|
|
xSemaphoreGive(s_server_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;
|
|
}
|
|
|
|
esp_err_t web_server_start(void)
|
|
{
|
|
esp_err_t error = web_server_init();
|
|
if (error != ESP_OK) {
|
|
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};
|
|
uint8_t private_key[WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY] = {0};
|
|
size_t certificate_length = 0U;
|
|
size_t private_key_length = 0U;
|
|
httpd_handle_t server = NULL;
|
|
|
|
error = web_security_copy_tls_material(
|
|
certificate, sizeof(certificate), &certificate_length,
|
|
private_key, sizeof(private_key), &private_key_length);
|
|
if (error == ESP_OK) {
|
|
httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT();
|
|
/* 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;
|
|
config.prvtkey_len = private_key_length;
|
|
config.port_secure = WEB_SERVER_PORT;
|
|
config.tls_handshake_timeout_ms = 5000U;
|
|
error = httpd_ssl_start(&server, &config);
|
|
}
|
|
secure_wipe(certificate, sizeof(certificate));
|
|
secure_wipe(private_key, sizeof(private_key));
|
|
|
|
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]);
|
|
}
|
|
|
|
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) {
|
|
esp_err_t cleanup_error = httpd_ssl_stop(server);
|
|
if (cleanup_error == ESP_OK) {
|
|
server = NULL;
|
|
} else {
|
|
/* Retain ownership so stop can retry and start cannot allocate a second server. */
|
|
error = cleanup_error;
|
|
}
|
|
}
|
|
|
|
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;
|
|
} else {
|
|
/* A non-NULL handle is a partially started server whose cleanup failed. */
|
|
s_server = server;
|
|
++s_counters.start_failures;
|
|
}
|
|
xSemaphoreGive(s_server_mutex);
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_server_stop(void)
|
|
{
|
|
if (s_server_mutex == NULL) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
|
if (s_server == NULL || s_transitioning) {
|
|
xSemaphoreGive(s_server_mutex);
|
|
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;
|
|
}
|
|
xSemaphoreGive(s_server_mutex);
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_server_get_snapshot(web_server_snapshot_t *snapshot)
|
|
{
|
|
if (snapshot == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (s_server_mutex == NULL) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
|
memset(snapshot, 0, sizeof(*snapshot));
|
|
snapshot->initialized = s_initialized;
|
|
snapshot->running = s_server != NULL;
|
|
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;
|
|
}
|
|
|
|
esp_err_t web_server_clear_counters(void)
|
|
{
|
|
if (s_server_mutex == NULL) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
|
memset(&s_counters, 0, sizeof(s_counters));
|
|
xSemaphoreGive(s_server_mutex);
|
|
return ESP_OK;
|
|
}
|