Files
ESP32_Serial_Swiss_Army_Knife/tests/admin_console_boundary/test.c
T

369 lines
21 KiB
C

static admin_ssh_console_token_t a = { .slot_index=0, .session_id=7, .slot_generation=1 };
static admin_ssh_console_token_t b = { .slot_index=1, .session_id=7, .slot_generation=1, .transport=1 };
static user_principal_t admin = { .role=USER_ROLE_ADMIN };
static bool live[2] = {true, true};
static void (*current_hook)(void);
static bool is_current(const admin_ssh_console_token_t *t, const user_principal_t *p)
{
assert(!lock_depth && p->role==USER_ROLE_ADMIN);
if (current_hook) current_hook();
return live[t->slot_index];
}
static bool drained(const admin_ssh_console_token_t *t)
{ assert(!lock_depth); assert(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)
{ (void)t; (void)arg; assert(!lock_depth); assert(action==ADMIN_CONSOLE_DEFER_SELF_CLOSE); ++actions; return ESP_OK; }
static const admin_console_owner_t owner = {
.supported_actions=1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE, .drained=drained, .perform=perform,
.is_current=is_current,
};
static void pump(void (*task)(void *)) { if (!setjmp(loop_done)) task(NULL); }
static void feed(const admin_ssh_console_token_t *t, const char *s)
{ size_t n=0; assert(admin_ssh_console_feed_input(t,(const uint8_t *)s,strlen(s),&n)); assert(n==strlen(s)); }
static void clear_output(const admin_ssh_console_token_t *t)
{ uint8_t out[4096]; size_t n; assert(admin_ssh_console_read_output(t,out,sizeof(out),&n)==ESP_OK); }
static void competing_completion(void)
{
size_t n=99;
assert(!admin_ssh_console_feed_input(&b,(const uint8_t *)"\t",1,&n));
assert(n==0 && s_completion_busy);
}
static void reopen_during_completion(void)
{
assert(!lock_depth && s_completion_busy);
admin_ssh_console_close(&a);
++a.slot_generation;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
clear_output(&a);
}
static void hidden_reply(void) { feed(&a,"secret\r"); }
static void cancel_reply(void) { feed(&a,"secret\x03"); }
static void close_prompt(void) { admin_ssh_console_close(&a); }
static void close_during_delay(void) { if (ticks>=200) admin_ssh_console_close(&a); }
static void close_during_command(void)
{
assert(s_sessions[0].executing);
admin_ssh_console_close(&a);
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE);
}
static void setup_dispatch(void)
{ s_dispatch_remote=true; s_dispatch_token=a; s_dispatch_principal=admin;
s_sessions[0].executing=true; s_sessions[0].command_pending=true; }
static void reopen_during_current(void)
{
current_hook=NULL;
admin_ssh_console_close(&a);
++a.slot_generation;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
live[0]=false; /* Failed old validation must not close the replacement. */
}
static void revoked_reply(void) { hidden_reply(); live[0]=false; }
static unsigned checks;
static void stale_at_execution(void) { if (++checks==2) live[0]=false; }
static void account_revoked_reply(void) { hidden_reply(); principal_current=false; }
static void closed_reply(void) { hidden_reply(); close_prompt(); }
static unsigned waits;
static void unanswered(void)
{
++waits;
if (waits==1) xSemaphoreGive(s_prompt_done); /* Stale wake while still waiting. */
if (waits==3) live[0]=false; /* No close notification. */
}
static void prompt_command(void)
{
uint8_t answer[32]; size_t n=99;
assert(admin_ssh_console_dispatch_read_input("Password: ",answer,sizeof(answer),true,&n)==ESP_ERR_NOT_FOUND);
assert(n==0);
for (size_t i=0;i<sizeof(answer);++i) assert(!answer[i]);
assert(!s_sessions[0].active && !s_sessions[0].prompt_length);
for (size_t i=0;i<sizeof(s_sessions[0].prompt_input);++i) assert(!s_sessions[0].prompt_input[i]);
}
static void test_currentness(void)
{
++a.slot_generation;
admin_console_owner_t missing=owner; missing.is_current=NULL;
assert(admin_ssh_console_open_owned(&a,&admin,&missing)==ESP_ERR_INVALID_ARG);
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
unsigned before=runs;
feed(&a,"owner stale\r"); live[0]=false; pump(worker_task);
assert(runs==before && !s_sessions[0].active && principal_current);
feed(&b,"isolated\r"); pump(worker_task); assert(runs==++before);
live[0]=true; ++a.slot_generation;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
feed(&a,"reuse\r"); current_hook=reopen_during_current; pump(worker_task);
assert(runs==before && token_matches(&s_sessions[0],&a));
live[0]=true;
feed(&a,"last check\r"); checks=0; current_hook=stale_at_execution;
pump(worker_task); current_hook=NULL;
assert(checks==2 && runs==before && !s_sessions[0].active);
live[0]=true; ++a.slot_generation;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
void (*hooks[])(void)={revoked_reply,account_revoked_reply,closed_reply,unanswered};
for (size_t i=0;i<sizeof(hooks)/sizeof(hooks[0]);++i) {
clear_output(&a); ticks=0; waits=0;
feed(&a,"prompt\r"); prompt_hook=hooks[i]; command_hook=prompt_command;
pump(worker_task); prompt_hook=NULL; command_hook=NULL;
assert(runs==++before);
admin_session_t empty={0}; assert(!memcmp(&empty,&s_sessions[0],sizeof(empty)));
if (i==3) assert(waits==3 && ticks==750);
/* Dispatcher recovered, so trusted UART0 work still runs. */
admin_request_t uart={.origin=ADMIN_REQUEST_UART0};
assert(xQueueSend(s_request_queue,&uart,0)); pump(worker_task); assert(runs==++before);
live[0]=true; principal_current=true; ++a.slot_generation;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
}
setup_dispatch(); clear_output(&a); live[0]=false;
uint8_t answer[32]; size_t n=99;
assert(admin_ssh_console_dispatch_read_input("Not published",answer,sizeof(answer),true,&n)==ESP_ERR_NOT_FOUND);
assert(!n && !s_sessions[0].output_length);
secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); live[0]=true;
++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
clear_output(&a);
s_sessions[0].output_start=4094;
assert(worker_write(&a,"abcdef"));
uint8_t out[8];
assert(admin_ssh_console_read_output(&a,out,3,&n)==ESP_OK && n==3 && !memcmp(out,"abc",3));
assert(!s_sessions[0].output[4094] && !s_sessions[0].output[4095] && !s_sessions[0].output[0]);
assert(!memcmp(s_sessions[0].output+1,"def",3));
assert(admin_ssh_console_read_output(&a,out,sizeof(out),&n)==ESP_OK && n==3 && !memcmp(out,"def",3));
for (size_t i=0;i<sizeof(s_sessions[0].output);++i) assert(!s_sessions[0].output[i]);
admin_ssh_console_close(&a);
puts("PASS: owner stale/account current isolation, callback close/reuse, revoked submitted prompts, periodic unanswered invalidation/stale wake, UART recovery, consumed output wiping");
}
static void test_dispatch_currentness(void)
{
++a.slot_generation; live[0]=true; principal_current=true;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
setup_dispatch();
assert(admin_ssh_console_dispatch_is_current());
current_task=(void *)2;
assert(!admin_ssh_console_dispatch_is_current());
current_task=s_task;
live[0]=false;
assert(!admin_ssh_console_dispatch_is_current());
assert(!s_sessions[0].active);
secure_wipe(&s_sessions[0],sizeof(s_sessions[0]));
++a.slot_generation; live[0]=true;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
setup_dispatch(); principal_current=false;
assert(!admin_ssh_console_dispatch_is_current());
assert(!s_sessions[0].active);
secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); principal_current=true;
++a.slot_generation;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
setup_dispatch(); s_dispatch_token.slot_generation--;
assert(!admin_ssh_console_dispatch_is_current());
assert(s_sessions[0].active); /* Stale dispatch cannot close replacement. */
secure_wipe(&s_sessions[0],sizeof(s_sessions[0]));
s_dispatch_remote=false;
assert(admin_ssh_console_dispatch_is_current()); /* Trusted UART0. */
puts("PASS: handler currentness API rejects wrong task, stale owner/account/token; preserves replacement and UART0");
}
static void test_shared_admission(void)
{
admin_ssh_console_token_t web={.slot_index=255, .session_id=7,
.slot_generation=42, .transport=ADMIN_CONSOLE_TRANSPORT_WEB};
admin_ssh_console_token_t ssh=web; ssh.transport=ADMIN_CONSOLE_TRANSPORT_SSH;
static const admin_console_owner_t second_owner={
.supported_actions=1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE,
.drained=drained, .perform=perform, .is_current=is_current,
};
assert(admin_ssh_console_open_available(&web,&admin,&owner)==ESP_OK);
assert(web.slot_index==0 && web.session_id==7 && web.slot_generation==42 &&
web.transport==ADMIN_CONSOLE_TRANSPORT_WEB);
assert(admin_ssh_console_open_available(&ssh,&admin,&second_owner)==ESP_OK);
assert(ssh.slot_index==1 && ssh.session_id==7 && ssh.slot_generation==42 && !ssh.transport);
assert(s_sessions[0].owner==&owner && s_sessions[1].owner==&second_owner);
unsigned before=runs;
clear_output(&web);
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);
assert(strstr((char *)diagnostic,"unavailable from the web console"));
feed(&web,"\"user\" \"password\" admin --generate\r"); pump(worker_task);
assert(runs==before && !s_control_queue->count);
feed(&web,"\"web\" \"status\"\r"); pump(worker_task); assert(runs==before+1);
/* UART0 bypasses remote policy and remains the recovery path. */
admin_request_t uart={.origin=ADMIN_REQUEST_UART0, .line="user recover --force"};
assert(xQueueSend(s_request_queue,&uart,0)); pump(worker_task); assert(runs==before+2);
runs=before;
admin_ssh_console_token_t full=web; full.slot_index=99;
assert(admin_ssh_console_open_available(&full,&admin,&owner)==ESP_ERR_INVALID_STATE);
assert(full.slot_index==99);
admin_ssh_console_token_t stale=web;
s_sessions[0].executing=true;
admin_ssh_console_close(&web);
assert(admin_ssh_console_open_available(&full,&admin,&owner)==ESP_ERR_INVALID_STATE);
assert(full.slot_index==99); /* Inactive executing slots still consume capacity. */
s_sessions[0].executing=false;
++web.slot_generation;
assert(admin_ssh_console_open_available(&web,&admin,&owner)==ESP_OK);
admin_ssh_console_close(&stale);
assert(!admin_ssh_console_accepts_input(&stale) && admin_ssh_console_accepts_input(&web));
assert(admin_ssh_console_accepts_input(&ssh));
admin_ssh_console_close(&web); admin_ssh_console_close(&ssh);
puts("PASS: two-owner shared admission, colliding preferred indices/IDs, full capacity, executing reservation and stale tokens");
}
int main(void)
{
assert(admin_ssh_console_init()==ESP_OK);
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE);
assert(admin_ssh_console_start_uart_frontend()==ESP_OK);
test_shared_admission();
principal_current=false;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE);
principal_current=true;
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
assert(admin_ssh_console_open_owned(&b,&admin,&owner)==ESP_OK);
admin_ssh_console_token_t other=a; other.transport=1;
assert(admin_ssh_console_open_owned(&other,&admin,&owner)==ESP_ERR_INVALID_STATE);
admin_ssh_console_close(&other);
assert(!admin_ssh_console_accepts_input(&other));
other=a; ++other.slot_generation; admin_ssh_console_close(&other);
assert(admin_ssh_console_accepts_input(&a));
clear_output(&a); clear_output(&b);
completion_hook=competing_completion; feed(&a,"\t"); completion_hook=NULL;
assert(!s_completion_busy && s_sessions[0].output_length && !s_sessions[1].output_length);
/* Keep the in-flight token unchanged; only the reopened session advances. */
admin_ssh_console_token_t completing=a;
completion_hook=reopen_during_completion; feed(&completing,"\t"); completion_hook=NULL;
assert(a.slot_generation==completing.slot_generation+1);
assert(!s_completion_busy && !s_sessions[0].output_length && !s_sessions[0].input_length);
assert(!admin_ssh_console_accepts_input(&completing));
feed(&a,"\t"); assert(s_sessions[0].output_length); clear_output(&a);
feed(&a,"help\r"); assert(runs==0); pump(worker_task); assert(runs==1);
feed(&a,"\x1b[A"); assert(!strcmp((char *)s_sessions[0].input,"help"));
feed(&a,"\x03");
feed(&a,"stale\r"); admin_ssh_console_close(&a);
++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
pump(worker_task); assert(runs==1);
feed(&a,"revoked\r"); principal_current=false; pump(worker_task); assert(runs==1); principal_current=true;
++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
admin_request_t uart={ .origin=ADMIN_REQUEST_UART0 };
assert(xQueueSend(s_request_queue,&uart,0)); pump(worker_task); assert(runs==2);
feed(&a,"close\r"); command_hook=close_during_command; pump(worker_task); command_hook=NULL;
admin_session_t empty={0}; assert(!memcmp(&empty,&s_sessions[0],sizeof(empty)));
++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
setup_dispatch(); clear_output(&a); prompt_hook=hidden_reply;
uint8_t secret[32]; size_t n;
uint8_t history_before[sizeof(s_sessions[0].history)];
memcpy(history_before,s_sessions[0].history,sizeof(history_before));
assert(admin_ssh_console_dispatch_is_current());
assert(admin_ssh_console_dispatch_read_input("Password: ",secret,sizeof(secret),true,&n)==ESP_OK);
assert(!memcmp(history_before,s_sessions[0].history,sizeof(history_before)));
for (size_t i=0;i<sizeof(s_sessions[0].prompt_input);++i) assert(!s_sessions[0].prompt_input[i]);
assert(n==6 && !memcmp(secret,"secret",6));
assert(s_sessions[0].output_length==strlen("Password: \r\n"));
clear_output(&a);
assert(admin_ssh_console_dispatch_read_input("Visible: ",secret,sizeof(secret),false,&n)==ESP_OK);
assert(s_sessions[0].output_length==strlen("Visible: secret\r\n"));
prompt_hook=cancel_reply;
assert(admin_ssh_console_dispatch_read_input("Password: ",secret,sizeof(secret),true,&n)==ESP_ERR_INVALID_STATE);
assert(n==0 && secret[0]==0 && s_sessions[0].prompt_input[0]==0);
prompt_hook=close_prompt;
assert(admin_ssh_console_dispatch_read_input("Password: ",secret,sizeof(secret),true,&n)==ESP_ERR_NOT_FOUND);
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE);
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);
clear_output(&a); owner_drained=true;
/* The fake esp_console_run does not invoke registered command callbacks. */
assert(command_exit(1,NULL)==0);
assert(s_control_queue->count==1);
admin_control_request_t exit_request;
memcpy(&exit_request,s_control_queue->bytes,sizeof(exit_request));
assert(exit_request.action==ADMIN_CONSOLE_DEFER_SELF_CLOSE);
assert(token_matches(&s_sessions[0],&exit_request.token));
assert(exit_request.owner==&owner && exit_request.argument==a.session_id);
ticks=0; pump(control_task); assert(ticks==200 && actions==1);
clear_output(&a);
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_SELF_CLOSE,0)==ESP_OK);
ticks=0; delay_hook=close_during_delay; pump(control_task); delay_hook=NULL; assert(actions==1);
secure_wipe(&s_sessions[0],sizeof(s_sessions[0]));
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
s_sessions[0].output_length=4096; ticks=0;
assert(ssh_output_write(&a,"x",1)==-1 && errno==EAGAIN && ticks==5000);
clear_output(&a); s_dispatch_output_previous_cr=false;
assert(ssh_output_write(&a,"a\nb\r\n",6)==6);
assert(s_sessions[0].output_length==7);
admin_ssh_console_close(&a);
assert(ssh_output_write(&a,"x",1)==-1 && errno==EPIPE);
assert(!lock_depth);
test_currentness();
test_dispatch_currentness();
unsigned before_serial = runs;
assert(admin_ssh_console_submit_serial_settings(0) == ESP_ERR_INVALID_STATE);
s_dispatch_ready = false;
assert(admin_ssh_console_submit_serial_settings(1) == ESP_ERR_INVALID_STATE);
s_dispatch_ready = true; queue_full = true;
assert(admin_ssh_console_submit_serial_settings(1) == ESP_ERR_TIMEOUT);
queue_full = false;
admin_request_t preceding_uart = {.origin = ADMIN_REQUEST_UART0};
assert(xQueueSend(s_request_queue, &preceding_uart, 0));
queue_send_wait = portMAX_DELAY;
assert(admin_ssh_console_submit_serial_settings(17) == ESP_OK && !serial_settings_executed);
assert(queue_send_wait == 0);
admin_request_t typed;
memcpy(&typed, s_request_queue->bytes + sizeof(typed), sizeof(typed));
assert(typed.origin == ADMIN_REQUEST_SERIAL_SETTINGS && typed.serial_settings_id == 17);
assert(s_request_queue->capacity == 4 && sizeof(typed.line) == 257);
pump(worker_task);
assert(serial_settings_executed == 17 && runs == before_serial + 1 && !s_request_queue->count);
assert(serial_settings_preceding_runs == before_serial + 1);
for (unsigned i = 0; i < s_request_queue->capacity; ++i)
assert(xQueueSend(s_request_queue, &preceding_uart, 0));
queue_send_wait = portMAX_DELAY;
assert(admin_ssh_console_submit_serial_settings(18) == ESP_ERR_TIMEOUT);
assert(queue_send_wait == 0 && s_request_queue->count == 4 && serial_settings_executed == 17);
pump(worker_task);
assert(runs == before_serial + 5 && serial_settings_executed == 17 && !s_request_queue->count);
puts("PASS: typed Serial admission uses zero wait on success/full queue, preserves all four queued UART requests and FIFO execution, no command-string dispatch");
assert(admin_ssh_console_submit_account_settings(0) == ESP_ERR_INVALID_STATE);
s_dispatch_ready = false;
assert(admin_ssh_console_submit_account_settings(1) == ESP_ERR_INVALID_STATE);
s_dispatch_ready = true; queue_full = true;
assert(admin_ssh_console_submit_account_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0);
queue_full = false;
assert(admin_ssh_console_submit_serial_settings(21) == ESP_OK);
assert(admin_ssh_console_submit_account_settings(22) == ESP_OK && queue_send_wait == 0);
pump(worker_task);
assert(serial_settings_executed == 21 && account_settings_executed == 22 && runs == before_serial + 5);
puts("PASS: typed Accounts uses same bounded queue with nonblocking admission and isolated dispatcher routing");
assert(admin_ssh_console_submit_network_settings(0) == ESP_ERR_INVALID_STATE);
s_dispatch_ready = false;
assert(admin_ssh_console_submit_network_settings(1) == ESP_ERR_INVALID_STATE);
s_dispatch_ready = true; queue_full = true;
assert(admin_ssh_console_submit_network_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0);
queue_full = false;
assert(admin_ssh_console_submit_serial_settings(31) == ESP_OK);
assert(admin_ssh_console_submit_network_settings(32) == ESP_OK && queue_send_wait == 0);
assert(admin_ssh_console_submit_account_settings(33) == ESP_OK);
pump(worker_task);
assert(serial_settings_executed == 31 && network_settings_executed == 32 && account_settings_executed == 33);
assert(runs == before_serial + 5 && s_request_queue->capacity == 4);
puts("PASS: typed Network queues only an ID, shares unchanged queue, executes outside lock without command runner");
puts("PASS: admission/identity, two owners, completion contention/reopen, history, queued stale/revoked work, UART dispatch, hidden/disconnected prompts, exit-to-SELF_CLOSE, deferred rejection/drain/close, 5s output backpressure");
}