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");
|
||||
}
|
||||
@@ -20,7 +20,7 @@ typedef struct {
|
||||
typedef unsigned TickType_t;
|
||||
typedef void *TaskHandle_t;
|
||||
typedef int portMUX_TYPE;
|
||||
typedef struct { size_t size; unsigned count; unsigned char bytes[2048]; } StaticQueue_t;
|
||||
typedef struct { size_t size; unsigned count, capacity; unsigned char bytes[2048]; } StaticQueue_t;
|
||||
typedef StaticQueue_t *QueueHandle_t;
|
||||
typedef int StaticSemaphore_t;
|
||||
typedef int *SemaphoreHandle_t;
|
||||
@@ -56,11 +56,13 @@ static void vTaskDelete(TaskHandle_t t) { (void)t; }
|
||||
static void xTaskNotifyGive(TaskHandle_t t) { (void)t; }
|
||||
static unsigned ulTaskNotifyTake(int b, unsigned t) { (void)b; (void)t; return 1; }
|
||||
static QueueHandle_t xQueueCreateStatic(unsigned n, size_t s, uint8_t *b, StaticQueue_t *q)
|
||||
{ (void)n; (void)b; q->size = s; return q; }
|
||||
{ (void)b; q->size = s; q->capacity = n; assert(n*s <= sizeof(q->bytes)); return q; }
|
||||
static int xQueueSend(QueueHandle_t q, const void *p, unsigned t)
|
||||
{ (void)t; if (queue_full) return 0; assert(!q->count); memcpy(q->bytes,p,q->size); q->count=1; return 1; }
|
||||
{ (void)t; if (queue_full || q->count==q->capacity) return 0;
|
||||
memcpy(q->bytes+q->count*q->size,p,q->size); ++q->count; return 1; }
|
||||
static int xQueueReceive(QueueHandle_t q, void *p, unsigned t)
|
||||
{ (void)t; if (!q->count) longjmp(loop_done,1); memcpy(p,q->bytes,q->size); q->count=0; return 1; }
|
||||
{ (void)t; if (!q->count) longjmp(loop_done,1); memcpy(p,q->bytes,q->size);
|
||||
--q->count; memmove(q->bytes,q->bytes+q->size,q->count*q->size); return 1; }
|
||||
static SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *s) { return s; }
|
||||
static int xSemaphoreTake(SemaphoreHandle_t s, unsigned t)
|
||||
{ assert(!lock_depth); if (t && !*s) { ticks+=t; if (prompt_hook) prompt_hook(); }
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Actual canonical stop/reboot handlers with deterministic side-effect doubles."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
def function(path, name):
|
||||
source = path.read_text()
|
||||
start = source.index('static int ' + name + '(')
|
||||
return source[start:source.index('\n}', start) + 2]
|
||||
|
||||
header = '\n'.join(line for line in (ROOT / 'src/admin_ssh_console.h').read_text().splitlines()
|
||||
if not line.startswith(('#include', '#pragma once')))
|
||||
prelude = r'''
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
typedef int esp_err_t;
|
||||
enum { ESP_OK=0, ESP_FAIL=-1, ESP_ERR_TIMEOUT=7 };
|
||||
typedef struct { int unused; } user_principal_t;
|
||||
'''
|
||||
fakes = r'''
|
||||
static bool remote, web;
|
||||
static unsigned stops, reboots, scheduled, waits, rotations;
|
||||
static esp_err_t schedule_result, stop_result;
|
||||
static admin_ssh_deferred_action_type_t last_action;
|
||||
bool admin_ssh_console_dispatch_is_remote(void) { return remote; }
|
||||
bool admin_ssh_console_dispatch_is_web(void) { return remote && web; }
|
||||
esp_err_t admin_ssh_console_dispatch_defer(admin_ssh_deferred_action_type_t action, uint32_t argument) {
|
||||
assert(remote && !argument); ++scheduled; last_action=action; return schedule_result;
|
||||
}
|
||||
static const char *esp_err_to_name(esp_err_t error) { (void)error; return "fake"; }
|
||||
static esp_err_t web_server_stop(void) { ++stops; return stop_result; }
|
||||
static esp_err_t web_server_start(void) { assert(false); return ESP_FAIL; }
|
||||
static esp_err_t web_server_clear_counters(void) { assert(false); return ESP_FAIL; }
|
||||
static esp_err_t web_serial_transport_clear_counters(void) { assert(false); return ESP_FAIL; }
|
||||
static void esp_restart(void) { ++reboots; }
|
||||
static void vTaskDelay(unsigned delay) { assert(delay==100); ++waits; }
|
||||
#define pdMS_TO_TICKS(ms) (ms)
|
||||
static void print_usage(void) { assert(false); }
|
||||
static int show_status(void) { assert(false); return 1; }
|
||||
static int show_counters(void) { assert(false); return 1; }
|
||||
static int show_credentials(void) { assert(false); return 1; }
|
||||
static int show_certificate(void) { assert(false); return 1; }
|
||||
static int rotate_credentials(void) { assert(false); return 1; }
|
||||
static int rotate_certificate(void) { ++rotations; return 0; }
|
||||
static int reset_material(void) { assert(false); return 1; }
|
||||
static bool force_is_present(int argc, char **argv, int expected) {
|
||||
return argc == expected && !strcmp(argv[expected - 1], "--force");
|
||||
}
|
||||
'''
|
||||
tests = r'''
|
||||
int main(void) {
|
||||
char *stop[]={"web", "stop"};
|
||||
remote=web=true;
|
||||
assert(command_web(2,stop)==0 && scheduled==1 && !stops && last_action==ADMIN_CONSOLE_DEFER_WEB_STOP);
|
||||
schedule_result=ESP_ERR_TIMEOUT;
|
||||
assert(command_web(2,stop)==1 && scheduled==2 && !stops);
|
||||
schedule_result=ESP_OK;
|
||||
web=false; /* SSH preserves its synchronous HTTPS path. */
|
||||
assert(command_web(2,stop)==0 && stops==1 && scheduled==2);
|
||||
remote=false;
|
||||
assert(command_web(2,stop)==0 && stops==2 && scheduled==2);
|
||||
stop_result=ESP_FAIL;
|
||||
assert(command_web(2,stop)==1 && stops==3);
|
||||
remote=true;
|
||||
assert(command_reboot(1,NULL)==0 && scheduled==3 && !reboots && last_action==ADMIN_SSH_DEFER_REBOOT);
|
||||
web=true;
|
||||
assert(command_reboot(1,NULL)==0 && scheduled==4 && !reboots && last_action==ADMIN_SSH_DEFER_REBOOT);
|
||||
schedule_result=ESP_FAIL;
|
||||
assert(command_reboot(1,NULL)==1 && scheduled==5 && !reboots);
|
||||
assert(command_reboot(2,NULL)==1 && scheduled==5 && !reboots);
|
||||
remote=false;
|
||||
assert(command_reboot(1,NULL)==0 && reboots==1 && waits==1 && scheduled==5);
|
||||
char *rotate[]={"web", "certificate", "rotate", "--force", "extra"};
|
||||
remote=web=true; schedule_result=ESP_OK;
|
||||
assert(command_web(4,rotate)==0 && scheduled==6 && !rotations &&
|
||||
last_action==ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE);
|
||||
schedule_result=ESP_ERR_TIMEOUT;
|
||||
assert(command_web(4,rotate)==1 && scheduled==7 && !rotations);
|
||||
assert(command_web(3,rotate)==1 && scheduled==7 && !rotations);
|
||||
assert(command_web(5,rotate)==1 && scheduled==7 && !rotations);
|
||||
web=false;
|
||||
assert(command_web(4,rotate)==0 && rotations==1 && scheduled==7);
|
||||
remote=false;
|
||||
assert(command_web(4,rotate)==0 && rotations==2 && scheduled==7);
|
||||
puts("PASS: canonical WEB stop/certificate deferred, exact force required, SSH/UART unchanged, reboot and queue failure isolation");
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='console-lifecycle-') as directory:
|
||||
tmp = Path(directory)
|
||||
(tmp / 'test.c').write_text(prelude + header + fakes +
|
||||
function(ROOT / 'src/web_console.c', 'command_web') +
|
||||
function(ROOT / 'src/system_console.c', 'command_reboot') + tests)
|
||||
subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror',
|
||||
str(tmp / 'test.c'), '-o', str(tmp / 'test')], check=True, timeout=30)
|
||||
subprocess.run([str(tmp / 'test')], check=True, timeout=10)
|
||||
@@ -27,6 +27,14 @@ with tempfile.TemporaryDirectory(prefix="admin-console-boundary-") as directory:
|
||||
"-g", str(path / "test.c"), parser,
|
||||
"-o", str(path / "test")], check=True, timeout=30)
|
||||
subprocess.run([str(path / "test")], check=True, timeout=10)
|
||||
unit = ((ROOT / "tests/admin_console_boundary/fakes.h").read_text()
|
||||
+ strip_includes(header) + "\n" + strip_includes(source)
|
||||
+ (ROOT / "tests/admin_console_boundary/certificate.c").read_text())
|
||||
(path / "certificate.c").write_text(unit)
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-g", str(path / "certificate.c"), parser,
|
||||
"-o", str(path / "certificate")], check=True, timeout=30)
|
||||
subprocess.run([str(path / "certificate")], check=True, timeout=10)
|
||||
ssh = (ROOT / "src/ssh_transport.c").read_text()
|
||||
adapter = ssh[ssh.index("static admin_ssh_console_token_t admin_console_token("):
|
||||
ssh.index("static void *ssh_malloc(")]
|
||||
|
||||
@@ -149,7 +149,7 @@ static void test_shared_admission(void)
|
||||
assert(s_sessions[0].owner==&owner && s_sessions[1].owner==&second_owner);
|
||||
unsigned before=runs;
|
||||
clear_output(&web);
|
||||
feed(&web,"\"web\" \"stop\"\r"); pump(worker_task);
|
||||
feed(&web,"\"web\" \"reset\" --force\r"); pump(worker_task);
|
||||
assert(runs==before && !s_control_queue->count);
|
||||
uint8_t diagnostic[512]={0}; size_t received=0;
|
||||
assert(admin_ssh_console_read_output(&web,diagnostic,sizeof(diagnostic)-1,&received)==ESP_OK);
|
||||
@@ -235,12 +235,21 @@ int main(void)
|
||||
secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); prompt_hook=NULL;
|
||||
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
||||
setup_dispatch(); clear_output(&a);
|
||||
assert(!admin_ssh_console_dispatch_is_web());
|
||||
s_dispatch_token.transport = ADMIN_CONSOLE_TRANSPORT_WEB;
|
||||
assert(admin_ssh_console_dispatch_is_web());
|
||||
s_dispatch_remote = false; assert(!admin_ssh_console_dispatch_is_web());
|
||||
s_dispatch_remote = true; s_dispatch_token = a;
|
||||
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_WEB_STOP,0)==ESP_ERR_NOT_SUPPORTED);
|
||||
assert(admin_ssh_console_dispatch_defer((admin_ssh_deferred_action_type_t)32,0)==ESP_ERR_NOT_SUPPORTED);
|
||||
assert(admin_ssh_console_dispatch_defer(ADMIN_SSH_DEFER_STOP,0)==ESP_ERR_NOT_SUPPORTED);
|
||||
assert(!s_sessions[0].deferred_action_pending);
|
||||
queue_full=true;
|
||||
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_SELF_CLOSE,0)==ESP_ERR_TIMEOUT);
|
||||
assert(!s_sessions[0].deferred_action_pending); queue_full=false;
|
||||
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_SELF_CLOSE,0)==ESP_OK);
|
||||
admin_ssh_console_session_snapshot_t pending;
|
||||
assert(admin_ssh_console_get_session_snapshot(&a, &pending)==ESP_OK && pending.deferred_action_pending);
|
||||
assert(!admin_ssh_console_feed_input(&a,(const uint8_t *)"x",1,&n) && n==0);
|
||||
s_sessions[0].command_pending=false; owner_drained=false; ticks=0;
|
||||
pump(control_task); assert(ticks==10000 && actions==0);
|
||||
|
||||
Reference in New Issue
Block a user