Extend browser admin lifecycle actions

Support browser reboot and HTTPS stop through deferred control, plus
exact
`web certificate rotate --force` handoff to the dispatcher. Add typed
request
validation and focused boundary and lifecycle coverage.
This commit is contained in:
2026-09-07 09:36:38 +02:00
parent 17520b15b7
commit 326119812f
23 changed files with 699 additions and 47 deletions
+82 -14
View File
@@ -70,9 +70,17 @@ typedef struct {
uint8_t output[ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY];
} admin_session_t;
typedef struct {
admin_ssh_deferred_action_type_t action;
admin_ssh_console_token_t token;
const admin_console_owner_t *owner;
uint32_t argument;
} admin_control_request_t;
typedef enum {
ADMIN_REQUEST_SSH = 0,
ADMIN_REQUEST_UART0,
ADMIN_REQUEST_DEFERRED,
} admin_request_origin_t;
typedef struct {
@@ -80,16 +88,12 @@ typedef struct {
admin_ssh_console_token_t token;
user_principal_t principal;
TaskHandle_t completion_task;
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
union {
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
admin_control_request_t deferred;
};
} admin_request_t;
typedef struct {
admin_ssh_deferred_action_type_t action;
admin_ssh_console_token_t token;
const admin_console_owner_t *owner;
uint32_t argument;
} admin_control_request_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static admin_session_t s_sessions[ADMIN_SSH_CONSOLE_MAX_SESSIONS];
/* Claimed under s_lock, used outside it; competing TAB input is backpressured. */
@@ -122,6 +126,12 @@ bool admin_ssh_console_dispatch_is_remote(void)
return xTaskGetCurrentTaskHandle() == s_task && s_dispatch_remote;
}
bool admin_ssh_console_dispatch_is_web(void)
{
return admin_ssh_console_dispatch_is_remote() &&
s_dispatch_token.transport == ADMIN_CONSOLE_TRANSPORT_WEB;
}
const user_principal_t *admin_ssh_console_dispatch_principal(void)
{
return admin_ssh_console_dispatch_is_remote() ? &s_dispatch_principal : NULL;
@@ -401,7 +411,7 @@ esp_err_t admin_ssh_console_dispatch_defer(
bool valid = token_matches(session, &s_dispatch_token) &&
!session->deferred_action_pending;
const admin_console_owner_t *owner = valid ? session->owner : NULL;
if (valid && ((unsigned)action > ADMIN_CONSOLE_DEFER_SELF_CLOSE ||
if (valid && ((unsigned)action > ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE ||
!(owner->supported_actions & (1U << action)))) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_NOT_SUPPORTED;
@@ -496,8 +506,12 @@ static bool remote_command_allowed(const admin_request_t *request)
* User mutations remain available through UART0/SSH, subject to their policy.
*/
if (request->token.transport == ADMIN_CONSOLE_TRANSPORT_WEB && argc > 0U) {
if (strcmp(argv[0], "web") == 0 || strcmp(argv[0], "wifi") == 0 ||
strcmp(argv[0], "mdns") == 0) {
if (strcmp(argv[0], "web") == 0) {
allowed = (argc == 2U && (strcmp(argv[1], "status") == 0 ||
strcmp(argv[1], "stop") == 0)) ||
(argc == 4U && strcmp(argv[1], "certificate") == 0 &&
strcmp(argv[2], "rotate") == 0 && strcmp(argv[3], "--force") == 0);
} else if (strcmp(argv[0], "wifi") == 0 || strcmp(argv[0], "mdns") == 0) {
allowed = argc == 2U && strcmp(argv[1], "status") == 0;
} else if (strcmp(argv[0], "user") == 0) {
allowed = argc == 1U ||
@@ -505,9 +519,9 @@ static bool remote_command_allowed(const admin_request_t *request)
strcmp(argv[1], "list") == 0)) ||
(argc == 3U && strcmp(argv[1], "show") == 0);
} else if (strcmp(argv[0], "reboot") == 0) {
allowed = false;
allowed = argc == 1U;
} else if (strcmp(argv[0], "ssh") == 0 && argc >= 2U) {
/* These handlers defer for every remote; WEB supports SELF_CLOSE only. */
/* SSH-specific deferred actions are not yet supported by WEB. */
if (strcmp(argv[1], "stop") == 0 || strcmp(argv[1], "disconnect") == 0 ||
strcmp(argv[1], "reset") == 0 ||
(strcmp(argv[1], "host-key") == 0 &&
@@ -598,6 +612,8 @@ static void dispatch_registered_command(admin_request_t *request)
}
}
static void dispatch_deferred_request(admin_request_t *request);
static void worker_task(void *context)
{
(void)context;
@@ -606,6 +622,11 @@ static void worker_task(void *context)
if (xQueueReceive(s_request_queue, &request, portMAX_DELAY) != pdTRUE) {
continue;
}
if (request.origin == ADMIN_REQUEST_DEFERRED) {
dispatch_deferred_request(&request);
secure_wipe(&request, sizeof(request));
continue;
}
if (request.origin == ADMIN_REQUEST_UART0) {
dispatch_registered_command(&request);
if (request.completion_task != NULL) {
@@ -683,6 +704,34 @@ static void finish_deferred_request(const admin_control_request_t *request,
taskEXIT_CRITICAL(&s_lock);
}
static void dispatch_deferred_request(admin_request_t *request)
{
const admin_control_request_t *action = &request->deferred;
bool current = session_is_current(&action->token, &request->principal);
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[action->token.slot_index];
bool active = current && token_matches(session, &action->token) &&
session->owner == action->owner && session->deferred_action_pending &&
!session->command_pending && !session->executing;
if (active) session->executing = true;
taskEXIT_CRITICAL(&s_lock);
/* Keep the slot reserved across lifecycle callbacks, including self-detach.
* Recheck after reservation just as the canonical runner does. */
esp_err_t result = ESP_ERR_NOT_FOUND;
if (active && session_is_current(&action->token, &request->principal)) {
result = action->owner->perform(&action->token, action->action, action->argument);
}
taskENTER_CRITICAL(&s_lock);
session = &s_sessions[action->token.slot_index];
if (active && token_identity_matches(session, &action->token)) {
if (session->active) session->executing = false;
else secure_wipe(session, sizeof(*session));
}
taskEXIT_CRITICAL(&s_lock);
finish_deferred_request(action, result, false);
}
static void control_task(void *context)
{
(void)context;
@@ -718,8 +767,26 @@ static void control_task(void *context)
}
vTaskDelay(pdMS_TO_TICKS(200U));
taskENTER_CRITICAL(&s_lock);
bool current = token_matches(&s_sessions[request.token.slot_index], &request.token);
admin_session_t *session = &s_sessions[request.token.slot_index];
bool current = token_matches(session, &request.token) &&
session->owner == request.owner && session->deferred_action_pending;
admin_request_t queued = {
.origin = ADMIN_REQUEST_DEFERRED,
.deferred = request,
};
if (current) queued.principal = session->principal;
taskEXIT_CRITICAL(&s_lock);
if (current && (request.owner->dispatcher_actions & (1U << request.action))) {
/* Nonblocking handoff: a full dispatcher queue fails before mutation.
* Pending remains set until execution completes, not merely enqueue. */
if (xQueueSend(s_request_queue, &queued, 0U) != pdTRUE) {
finish_deferred_request(&request, ESP_ERR_TIMEOUT, false);
}
secure_wipe(&queued, sizeof(queued));
secure_wipe(&request, sizeof(request));
continue;
}
secure_wipe(&queued, sizeof(queued));
esp_err_t result = current ? request.owner->perform(
&request.token, request.action, request.argument) : ESP_ERR_NOT_FOUND;
finish_deferred_request(&request, result, false);
@@ -1266,6 +1333,7 @@ esp_err_t admin_ssh_console_get_session_snapshot(
snapshot->command_pending = session->command_pending;
snapshot->input_pending = session->input_length != 0U;
snapshot->output_pending = session->output_length != 0U;
snapshot->deferred_action_pending = session->deferred_action_pending;
snapshot->input_length = session->input_length;
snapshot->output_length = session->output_length;
taskEXIT_CRITICAL(&s_lock);