/* SPDX-License-Identifier: GPL-3.0-only */ /* TLS-only HTTP server with bounded Basic authentication and status output. */ #include "web_server.h" #include #include #include #include #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; }