Implement HTTPS lifecycle and reboot controls

This commit is contained in:
2026-09-13 17:24:00 +02:00
parent 737bd29f9e
commit 36e80811e8
24 changed files with 1392 additions and 75 deletions
+94 -16
View File
@@ -13,6 +13,7 @@
#include "esp_log.h"
#include "esp_netif_ip_addr.h"
#include "esp_timer.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "secure_random.h"
@@ -29,6 +30,7 @@
#include "web_display_settings.h"
#include "web_broker_settings.h"
#include "web_ssh_settings.h"
#include "web_lifecycle_settings.h"
#include "web_admin_transport.h"
#include "web_session_store.h"
#include "web_cookie_auth.h"
@@ -45,6 +47,8 @@ static SemaphoreHandle_t s_server_mutex;
static httpd_handle_t s_server;
static bool s_initialized;
static bool s_transitioning;
/* Firmware-lifetime lifecycle fence, independent of counters and handle reuse. */
static uint32_t s_generation = 1U;
static bool s_serial_transport_init_attempted;
static bool s_serial_transport_initialized;
static bool s_serial_transport_attached;
@@ -416,6 +420,15 @@ static const httpd_uri_t s_account_generate_password_uri = {
static const httpd_uri_t s_network_uri = {
.uri = "/api/settings/network", .method = HTTP_GET, .handler = web_network_snapshot_handler,
};
static const httpd_uri_t s_lifecycle_settings_uri = {
.uri = "/api/settings/lifecycle", .method = HTTP_GET, .handler = web_lifecycle_settings_handler,
};
static const httpd_uri_t s_lifecycle_operation_get_uri = {
.uri = "/api/settings/lifecycle-operation", .method = HTTP_GET, .handler = web_lifecycle_operation_handler,
};
static const httpd_uri_t s_lifecycle_operation_post_uri = {
.uri = "/api/settings/lifecycle-operation", .method = HTTP_POST, .handler = web_lifecycle_operation_handler,
};
static const httpd_uri_t s_ssh_settings_uri = {
.uri = "/api/settings/ssh", .method = HTTP_GET, .handler = web_ssh_settings_handler,
};
@@ -619,30 +632,27 @@ esp_err_t web_server_init(void)
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) {
if (!s_initialized && s_last_error == ESP_ERR_INVALID_STATE) {
s_last_error = ESP_OK;
}
s_initialized = true;
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)
static esp_err_t start_server(bool reserved)
{
esp_err_t error = web_server_init();
if (error != ESP_OK) {
return error;
}
esp_err_t error;
bool serial_transport_ready;
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
if (s_server != NULL || s_transitioning) {
if (s_server != NULL || s_transitioning != reserved) {
xSemaphoreGive(s_server_mutex);
return ESP_ERR_INVALID_STATE;
}
s_transitioning = true;
if (s_generation != UINT32_MAX) ++s_generation;
serial_transport_ready = s_serial_transport_initialized;
xSemaphoreGive(s_server_mutex);
@@ -665,7 +675,7 @@ esp_err_t web_server_start(void)
config.httpd.max_open_sockets = 6;
config.httpd.max_uri_handlers =
sizeof(s_uri_handlers) / sizeof(s_uri_handlers[0]) +
sizeof(s_auth_uris) / sizeof(s_auth_uris[0]) + 22U;
sizeof(s_auth_uris) / sizeof(s_auth_uris[0]) + 25U;
/* Exhaustion rejects new sockets, never evicts an existing serial writer. */
config.httpd.lru_purge_enable = false;
config.httpd.recv_wait_timeout = 1;
@@ -741,6 +751,10 @@ esp_err_t web_server_start(void)
web_httpd_register_optional_get(server, &s_ssh_operation_get_uri) == ESP_OK &&
web_httpd_register_optional(server, &s_ssh_operation_post_uri) != ESP_OK)
(void)httpd_unregister_uri_handler(server, s_ssh_operation_get_uri.uri, HTTP_GET);
if (web_httpd_register_optional_get(server, &s_lifecycle_settings_uri) == ESP_OK &&
web_httpd_register_optional_get(server, &s_lifecycle_operation_get_uri) == ESP_OK &&
web_httpd_register_optional(server, &s_lifecycle_operation_post_uri) != ESP_OK)
(void)httpd_unregister_uri_handler(server, s_lifecycle_operation_get_uri.uri, HTTP_GET);
}
if (error != ESP_OK) {
web_cookie_auth_stop();
@@ -750,6 +764,7 @@ esp_err_t web_server_start(void)
if (cleanup_error == ESP_OK) cleanup_error = httpd_ssl_stop(server);
if (cleanup_error == ESP_OK) {
web_httpd_idle_stopped(server);
web_lifecycle_settings_stopped(server);
server = NULL;
} else {
/* Retain ownership so stop can retry and start cannot allocate a second server. */
@@ -775,14 +790,23 @@ esp_err_t web_server_start(void)
return error;
}
esp_err_t web_server_stop(void)
esp_err_t web_server_start(void)
{
esp_err_t error = web_server_init();
return error == ESP_OK ? start_server(false) : error;
}
static esp_err_t stop_server(uint32_t expected_generation, bool restart)
{
if (s_server_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
if (s_server == NULL || s_transitioning) {
if (xSemaphoreTake(s_server_mutex, expected_generation ? 0U : portMAX_DELAY) != pdTRUE)
return ESP_ERR_TIMEOUT;
if (s_server == NULL || s_transitioning ||
(expected_generation && (expected_generation != s_generation ||
s_generation == UINT32_MAX || s_last_error != ESP_OK))) {
xSemaphoreGive(s_server_mutex);
return ESP_ERR_INVALID_STATE;
}
@@ -791,6 +815,7 @@ esp_err_t web_server_stop(void)
bool admin_transport_owned = s_admin_transport_owned;
esp_err_t serial_transport_error = s_serial_transport_error;
s_transitioning = true;
if (s_generation != UINT32_MAX) ++s_generation;
xSemaphoreGive(s_server_mutex);
web_cookie_auth_stop();
@@ -829,7 +854,10 @@ esp_err_t web_server_stop(void)
}
esp_err_t error = httpd_ssl_stop(server);
if (error == ESP_OK) web_httpd_idle_stopped(server);
if (error == ESP_OK) {
web_httpd_idle_stopped(server);
web_lifecycle_settings_stopped(server);
}
if (error == ESP_OK && admin_transport_owned) web_admin_transport_stopped(server);
if (error != ESP_OK && serial_transport_attached) {
/* Stay detached: old HTTPD work may still be reading static TX storage. */
@@ -837,7 +865,8 @@ esp_err_t web_server_stop(void)
}
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
s_transitioning = false;
/* Do not expose a stopped/unreserved gap to another lifecycle caller. */
s_transitioning = error == ESP_OK && restart;
s_last_error = error;
s_serial_transport_error = serial_transport_error;
s_serial_transport_attached = false;
@@ -847,7 +876,56 @@ esp_err_t web_server_stop(void)
++s_counters.stops;
}
xSemaphoreGive(s_server_mutex);
return error;
return error == ESP_OK && restart ? start_server(true) : error;
}
esp_err_t web_server_stop(void)
{
return stop_server(0U, false);
}
esp_err_t web_server_stop_current(uint32_t expected_generation)
{
if (!expected_generation) return ESP_ERR_INVALID_ARG;
return stop_server(expected_generation, false);
}
esp_err_t web_server_restart_current(uint32_t expected_generation)
{
if (!expected_generation) return ESP_ERR_INVALID_ARG;
return stop_server(expected_generation, true);
}
esp_err_t web_server_reboot_current(uint32_t expected_generation)
{
if (!expected_generation) return ESP_ERR_INVALID_ARG;
if (s_server_mutex == NULL) return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(s_server_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT;
if (s_server == NULL || s_transitioning || s_last_error != ESP_OK ||
s_generation == UINT32_MAX || expected_generation != s_generation) {
xSemaphoreGive(s_server_mutex);
return ESP_ERR_INVALID_STATE;
}
s_transitioning = true;
++s_generation;
xSemaphoreGive(s_server_mutex);
esp_restart();
return ESP_FAIL; /* Defensive only: reset normally never returns. */
}
esp_err_t web_server_get_management_snapshot(web_server_management_snapshot_t *snapshot)
{
if (snapshot == NULL) return ESP_ERR_INVALID_ARG;
memset(snapshot, 0, sizeof(*snapshot));
if (s_server_mutex == NULL) return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(s_server_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT;
snapshot->generation = s_generation;
snapshot->running = s_server != NULL;
snapshot->transitioning = s_transitioning;
snapshot->controllable = s_initialized && s_server != NULL && !s_transitioning &&
s_last_error == ESP_OK && s_generation != UINT32_MAX;
xSemaphoreGive(s_server_mutex);
return ESP_OK;
}
esp_err_t web_server_get_snapshot(web_server_snapshot_t *snapshot)