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:
@@ -0,0 +1,138 @@
|
||||
/* Typed deferred work exercises the production dispatcher/control state machine. */
|
||||
static admin_ssh_console_token_t token={.session_id=7, .slot_generation=1,
|
||||
.transport=ADMIN_CONSOLE_TRANSPORT_WEB};
|
||||
static user_principal_t principal={.role=USER_ROLE_ADMIN, .auth_generation=1};
|
||||
static bool live=true, close_in_action;
|
||||
static unsigned validations, invalidate_at;
|
||||
static esp_err_t action_result;
|
||||
static bool current(const admin_ssh_console_token_t *t, const user_principal_t *p) {
|
||||
assert(!lock_depth && t->session_id==7 && p->auth_generation==1);
|
||||
if (++validations==invalidate_at) live=false;
|
||||
return live;
|
||||
}
|
||||
static bool drained(const admin_ssh_console_token_t *t) {
|
||||
assert(!lock_depth && current_task==s_control_task && t->session_id==7);
|
||||
return owner_drained;
|
||||
}
|
||||
static esp_err_t perform(const admin_ssh_console_token_t *t,
|
||||
admin_ssh_deferred_action_type_t action, uint32_t arg);
|
||||
static const admin_console_owner_t owner={
|
||||
.supported_actions=1U << ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE,
|
||||
.dispatcher_actions=1U << ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE,
|
||||
.is_current=current, .drained=drained, .perform=perform,
|
||||
};
|
||||
static esp_err_t perform(const admin_ssh_console_token_t *t,
|
||||
admin_ssh_deferred_action_type_t action, uint32_t arg) {
|
||||
assert(!lock_depth && current_task==s_task && current_task!=s_control_task);
|
||||
assert(action==ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE && arg==0);
|
||||
assert(s_sessions[0].executing && s_sessions[0].deferred_action_pending);
|
||||
assert(!admin_ssh_console_accepts_input(t));
|
||||
size_t consumed=99;
|
||||
assert(!admin_ssh_console_feed_input(t,(const uint8_t *)"ignored",7,&consumed) && !consumed);
|
||||
++actions;
|
||||
if (close_in_action) {
|
||||
admin_ssh_console_close(t);
|
||||
admin_ssh_console_token_t replacement=*t; ++replacement.slot_generation;
|
||||
assert(admin_ssh_console_open_owned(&replacement,&principal,&owner)==ESP_ERR_INVALID_STATE);
|
||||
}
|
||||
return action_result;
|
||||
}
|
||||
static void pump(void (*task)(void *)) {
|
||||
current_task=task==control_task ? s_control_task : s_task;
|
||||
if (!setjmp(loop_done)) task(NULL);
|
||||
}
|
||||
static void clear_output(void) {
|
||||
uint8_t data[4096]; size_t n;
|
||||
assert(admin_ssh_console_read_output(&token,data,sizeof(data),&n)==ESP_OK);
|
||||
}
|
||||
static void reopen_certificate_session(void) {
|
||||
admin_ssh_console_close(&token); ++token.slot_generation;
|
||||
live=principal_current=owner_drained=true; validations=invalidate_at=0;
|
||||
close_in_action=false; action_result=ESP_OK; ticks=0;
|
||||
assert(admin_ssh_console_open_owned(&token,&principal,&owner)==ESP_OK);
|
||||
clear_output();
|
||||
}
|
||||
static esp_err_t schedule(void) {
|
||||
current_task=s_task; s_dispatch_remote=true; s_dispatch_token=token;
|
||||
s_dispatch_principal=principal;
|
||||
s_sessions[0].executing=s_sessions[0].command_pending=true;
|
||||
esp_err_t result=admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE,0);
|
||||
s_sessions[0].executing=s_sessions[0].command_pending=false;
|
||||
s_dispatch_remote=false;
|
||||
return result;
|
||||
}
|
||||
static void revoke_delay(void) { if (ticks>=200) live=false; }
|
||||
static void reuse_delay(void) {
|
||||
if (ticks>=200) { delay_hook=NULL; reopen_certificate_session(); }
|
||||
}
|
||||
static void assert_pending(void) {
|
||||
admin_ssh_console_session_snapshot_t snapshot;
|
||||
assert(admin_ssh_console_get_session_snapshot(&token,&snapshot)==ESP_OK);
|
||||
assert(snapshot.deferred_action_pending && !admin_ssh_console_accepts_input(&token));
|
||||
}
|
||||
static void uart_observes_pending(void) {
|
||||
assert(current_task==s_task && actions==0); assert_pending();
|
||||
}
|
||||
int main(void) {
|
||||
assert(admin_ssh_console_init()==ESP_OK);
|
||||
assert(admin_ssh_console_start_uart_frontend()==ESP_OK);
|
||||
s_task=(void *)1; s_control_task=(void *)2;
|
||||
/* Union overlay preserves the old queue item allocation on this ABI. */
|
||||
struct old_request { admin_request_origin_t origin; admin_ssh_console_token_t token;
|
||||
user_principal_t principal; TaskHandle_t completion_task;
|
||||
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY+1U]; };
|
||||
assert(sizeof(admin_request_t)==sizeof(struct old_request));
|
||||
assert(s_request_queue->capacity==4 && s_control_queue->capacity==2);
|
||||
reopen_certificate_session(); queue_full=true;
|
||||
assert(schedule()==ESP_ERR_TIMEOUT && !s_sessions[0].deferred_action_pending && !actions);
|
||||
queue_full=false;
|
||||
assert(schedule()==ESP_OK); assert_pending();
|
||||
/* Drain waits for acknowledgement, then times out without enqueue/mutation. */
|
||||
s_sessions[0].output_length=1; pump(control_task);
|
||||
assert(ticks==10000 && !actions && !s_request_queue->count && !s_sessions[0].deferred_action_pending);
|
||||
clear_output(); ticks=0;
|
||||
assert(schedule()==ESP_OK);
|
||||
admin_request_t uart={.origin=ADMIN_REQUEST_UART0, .line="memory"};
|
||||
for (unsigned i=0;i<4;++i) assert(xQueueSend(s_request_queue,&uart,0));
|
||||
pump(control_task);
|
||||
assert(!actions && !s_sessions[0].deferred_action_pending && s_request_queue->count==4);
|
||||
assert(s_sessions[0].output_length); pump(worker_task); clear_output();
|
||||
puts("PASS: unchanged queue item/depths, admission and handoff queue failure before mutation, ack drain cancellation");
|
||||
|
||||
assert(schedule()==ESP_OK);
|
||||
assert(xQueueSend(s_request_queue,&uart,0));
|
||||
pump(control_task); assert_pending(); assert(!actions && s_request_queue->count==2);
|
||||
command_hook=uart_observes_pending; pump(worker_task); command_hook=NULL;
|
||||
assert(actions==1 && !s_sessions[0].deferred_action_pending && !s_sessions[0].executing);
|
||||
assert(runs==5); /* Typed work never calls esp_console_run. */
|
||||
puts("PASS: control only hands off, queued UART first, crypto callback exclusively serialized on dispatcher, input gated through callback");
|
||||
|
||||
for (unsigned cancellation=0;cancellation<7;++cancellation) {
|
||||
reopen_certificate_session(); assert(schedule()==ESP_OK);
|
||||
if (cancellation==0) delay_hook=revoke_delay;
|
||||
if (cancellation==1) delay_hook=reuse_delay;
|
||||
pump(control_task); delay_hook=NULL;
|
||||
if (cancellation==2) live=false;
|
||||
if (cancellation==3) principal_current=false;
|
||||
if (cancellation==4) reopen_certificate_session();
|
||||
if (cancellation==5) admin_ssh_console_close(&token);
|
||||
if (cancellation==6) invalidate_at=2; /* Last check after executing reservation. */
|
||||
pump(worker_task);
|
||||
assert(actions==1 && !s_sessions[0].executing);
|
||||
if (cancellation==1 || cancellation==4) {
|
||||
assert(s_sessions[0].active && !s_sessions[0].deferred_action_pending && !s_sessions[0].output_length);
|
||||
}
|
||||
}
|
||||
puts("PASS: delay/queued revoke, account revoke, close/reuse, final execution check; no output into replacements");
|
||||
|
||||
reopen_certificate_session(); action_result=ESP_ERR_NO_MEM; assert(schedule()==ESP_OK);
|
||||
pump(control_task); pump(worker_task);
|
||||
assert(actions==2 && !s_sessions[0].deferred_action_pending && admin_ssh_console_accepts_input(&token));
|
||||
uint8_t out[512]={0}; size_t n;
|
||||
assert(admin_ssh_console_read_output(&token,out,sizeof(out)-1,&n)==ESP_OK);
|
||||
assert(strstr((char *)out,"Deferred remote action failed: fake"));
|
||||
reopen_certificate_session(); close_in_action=true; assert(schedule()==ESP_OK);
|
||||
pump(control_task); pump(worker_task);
|
||||
admin_session_t empty={0}; assert(!memcmp(&empty,&s_sessions[0],sizeof(empty)) && actions==3);
|
||||
puts("PASS: action error reaches deferred result, input resumes on failure, self-detach reserves slot until return and wipes state");
|
||||
}
|
||||
Reference in New Issue
Block a user