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:
@@ -8,6 +8,9 @@
|
||||
#include "admin_ssh_console.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_system.h"
|
||||
#include "web_server.h"
|
||||
#include "web_security.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "secure_random.h"
|
||||
@@ -91,9 +94,34 @@ static esp_err_t owner_perform(const admin_ssh_console_token_t *token,
|
||||
admin_ssh_deferred_action_type_t action, uint32_t argument)
|
||||
{
|
||||
(void)argument;
|
||||
if (action != ADMIN_CONSOLE_DEFER_SELF_CLOSE) return ESP_ERR_NOT_SUPPORTED;
|
||||
if (action != ADMIN_CONSOLE_DEFER_SELF_CLOSE && action != ADMIN_SSH_DEFER_REBOOT &&
|
||||
action != ADMIN_CONSOLE_DEFER_WEB_STOP &&
|
||||
action != ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE) return ESP_ERR_NOT_SUPPORTED;
|
||||
/* Drain is only a delivery heuristic, not an authorization lease. The
|
||||
* execution task must recheck cookie/account binding after delay/queueing. */
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool valid = token_matches(token) && s_slot.active && s_accepting;
|
||||
user_principal_t principal = s_slot.principal;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
bool current = owner_current(token, &principal);
|
||||
secure_wipe(&principal, sizeof(principal));
|
||||
if (!current) return ESP_ERR_NOT_FOUND;
|
||||
if (action == ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE) {
|
||||
/* The owner mask routes this crypto/NVS path to the 12KiB dispatcher.
|
||||
* Commit before restart; a failed stop retains HTTPD ownership and must
|
||||
* not be followed by start. No socket IO or console handler calls here. */
|
||||
esp_err_t error = web_security_rotate_certificate();
|
||||
if (error != ESP_OK) return error;
|
||||
error = web_server_stop();
|
||||
if (error != ESP_OK) return error;
|
||||
return web_server_start();
|
||||
}
|
||||
if (action == ADMIN_CONSOLE_DEFER_WEB_STOP) return web_server_stop();
|
||||
if (action == ADMIN_SSH_DEFER_REBOOT) {
|
||||
esp_restart();
|
||||
return ESP_OK;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool valid = token_matches(token) && s_slot.active && s_accepting && !s_slot.close_requested;
|
||||
if (valid) s_slot.close_requested = true;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (valid) admin_ssh_console_close(token);
|
||||
@@ -101,7 +129,10 @@ static esp_err_t owner_perform(const admin_ssh_console_token_t *token,
|
||||
}
|
||||
|
||||
static const admin_console_owner_t s_owner = {
|
||||
.supported_actions = 1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE,
|
||||
.supported_actions = (1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE) |
|
||||
(1U << ADMIN_SSH_DEFER_REBOOT) | (1U << ADMIN_CONSOLE_DEFER_WEB_STOP) |
|
||||
(1U << ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE),
|
||||
.dispatcher_actions = 1U << ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE,
|
||||
.is_current = owner_current, .drained = owner_drained, .perform = owner_perform,
|
||||
};
|
||||
|
||||
@@ -152,11 +183,29 @@ static bool input_current(const admin_ssh_console_token_t *token,
|
||||
return false;
|
||||
}
|
||||
|
||||
static void discard_pending_input(void)
|
||||
{
|
||||
secure_wipe(s_payload->rx, sizeof(s_payload->rx));
|
||||
s_payload->rx_offset = s_payload->rx_length = 0;
|
||||
s_payload->input_deadline = 0;
|
||||
count(&s_counts.input_backpressure, 1);
|
||||
}
|
||||
|
||||
static bool feed_pending(const admin_ssh_console_token_t *token,
|
||||
const user_principal_t *principal)
|
||||
{
|
||||
if (s_payload->rx_offset == s_payload->rx_length) return true;
|
||||
if (!input_current(token, principal)) return false;
|
||||
admin_ssh_console_session_snapshot_t console;
|
||||
if (admin_ssh_console_get_session_snapshot(token, &console) != ESP_OK || !console.active) {
|
||||
request_close();
|
||||
return false;
|
||||
}
|
||||
if (console.deferred_action_pending) {
|
||||
/* Never replay buffered keystrokes if a deferred action fails/cancels. */
|
||||
discard_pending_input();
|
||||
return true;
|
||||
}
|
||||
if (esp_timer_get_time() >= s_payload->input_deadline) {
|
||||
count(&s_counts.input_backpressure, 1);
|
||||
request_close();
|
||||
@@ -425,9 +474,19 @@ static esp_err_t frame_handler(httpd_req_t *request)
|
||||
count(&s_counts.protocol_errors, 1);
|
||||
goto failure;
|
||||
}
|
||||
if (s_payload->rx_length != s_payload->rx_offset) {
|
||||
count(&s_counts.input_backpressure, 1);
|
||||
admin_ssh_console_session_snapshot_t console;
|
||||
if (admin_ssh_console_get_session_snapshot(&token, &console) != ESP_OK || !console.active)
|
||||
goto failure;
|
||||
/* Latch before the potentially blocking receive: cancellation during receive
|
||||
* must not turn input observed during deferral into a new command. */
|
||||
bool discard_frame = console.deferred_action_pending;
|
||||
if (s_payload->rx_length != s_payload->rx_offset) {
|
||||
if (!discard_frame) {
|
||||
count(&s_counts.input_backpressure, 1);
|
||||
goto failure;
|
||||
}
|
||||
/* Deferral may start before the next poll discards buffered trailing input. */
|
||||
discard_pending_input();
|
||||
}
|
||||
frame.payload = s_payload->rx;
|
||||
/* IDF treats len==0 as another header probe, not an empty payload read. */
|
||||
@@ -436,7 +495,8 @@ static esp_err_t frame_handler(httpd_req_t *request)
|
||||
s_payload->rx_length = frame.len;
|
||||
s_payload->rx_offset = 0;
|
||||
s_payload->input_deadline = esp_timer_get_time() + ADMIN_INPUT_TIMEOUT_US;
|
||||
if (!feed_pending(&token, &principal)) goto failure;
|
||||
if (discard_frame) discard_pending_input();
|
||||
else if (!feed_pending(&token, &principal)) goto failure;
|
||||
count(&s_counts.rx_bytes, (uint32_t)frame.len);
|
||||
secure_wipe(&principal, sizeof(principal));
|
||||
return ESP_OK;
|
||||
|
||||
Reference in New Issue
Block a user