feat: add bounded admin WebSocket backend (Phase 8D.5)
- Require current admin cookie sessions, Origin checks and single-use tickets - Reuse the shared console with session-aware authorization and slot allocation - Add HTTPD-owned I/O, bounded buffering and revocation cleanup - Prevent LRU eviction of serial clients and stale admin socket closure - Reject unsupported web-shell mutations before side effects - Add host regressions, a smoke client and resource accounting Validated by user sign-off after a 15-minute full-client soak at 230400 baud, with a few broker drops under heavy output. Browser UI remains for Phase 8D.6; numeric memory reserves remain open.
This commit is contained in:
@@ -1,29 +1,109 @@
|
||||
|
||||
#define SSH_TRANSPORT_MAX_SESSIONS 2U
|
||||
typedef struct { bool active, tx_pending; uint32_t session_id, generation; } ssh_transport_session_snapshot_t;
|
||||
enum { SSH_TRANSPORT_SESSION_FREE=0, SSH_TRANSPORT_SESSION_ACTIVE=2,
|
||||
SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE=2 };
|
||||
enum { USER_AUTH_METHOD_PASSWORD=0 };
|
||||
typedef struct {
|
||||
uint32_t session_id, generation, broker_client_id;
|
||||
int state, route, socket_fd;
|
||||
uint8_t console_slot_index;
|
||||
bool authenticated, principal_valid, writer, close_requested;
|
||||
size_t rx_length, rx_offset, tx_length, tx_offset;
|
||||
user_principal_t principal;
|
||||
char peer[48];
|
||||
} ssh_slot_t;
|
||||
typedef struct {
|
||||
bool active, tx_pending, rx_pending, authenticated, principal_valid, close_requested;
|
||||
bool writer, admin_command_pending;
|
||||
uint32_t session_id, generation, broker_client_id, admin_output_pending;
|
||||
int state, route, socket_fd, user_role, auth_method;
|
||||
char username[USER_DATABASE_USERNAME_CAPACITY+1U], peer[48];
|
||||
} ssh_transport_session_snapshot_t;
|
||||
static ssh_transport_session_snapshot_t s_session_snapshots[2];
|
||||
static user_principal_t s_console_principals[2];
|
||||
static uint8_t s_console_slot_indices[2];
|
||||
static uint32_t s_external_close_id[2];
|
||||
static unsigned stopped, disconnected, rotated, reset, restarted;
|
||||
static esp_err_t ssh_transport_stop(void) { ++stopped; return ESP_OK; }
|
||||
static esp_err_t ssh_transport_disconnect(uint32_t id) { disconnected=id; return ESP_OK; }
|
||||
static esp_err_t ssh_transport_replace_host_key(bool r) { if(r) ++reset; else ++rotated; return ESP_OK; }
|
||||
static void esp_restart(void) { ++restarted; }
|
||||
static void publish_slot(const ssh_slot_t *, size_t);
|
||||
static bool admin_console_drained(const admin_ssh_console_token_t *);
|
||||
static bool admin_console_is_current(const admin_ssh_console_token_t *, const user_principal_t *);
|
||||
static bool consume_external_close(const ssh_slot_t *, size_t);
|
||||
static esp_err_t admin_console_perform(const admin_ssh_console_token_t *, admin_ssh_deferred_action_type_t, uint32_t);
|
||||
static void test_adapter(void)
|
||||
{
|
||||
admin_ssh_console_token_t token={ .slot_index=0, .session_id=7, .slot_generation=3 };
|
||||
user_principal_t admin={ .role=USER_ROLE_ADMIN };
|
||||
user_principal_t admin={ .role=USER_ROLE_ADMIN, .user_id=11, .auth_generation=2,
|
||||
.username_length=5, .username="admin" };
|
||||
assert(admin_ssh_console_init()==ESP_OK);
|
||||
assert(admin_ssh_console_start_uart_frontend()==ESP_OK);
|
||||
assert(admin_ssh_console_open(&token,&admin)==ESP_OK);
|
||||
assert(!admin_console_drained(&token));
|
||||
s_session_snapshots[0]=(ssh_transport_session_snapshot_t){ .active=true, .session_id=7, .generation=3 };
|
||||
assert(!admin_console_drained(&token)); /* No published console binding. */
|
||||
assert(!admin_console_is_current(&token,&admin));
|
||||
ssh_slot_t active={ .session_id=7, .generation=3, .authenticated=true,
|
||||
.principal_valid=true, .state=SSH_TRANSPORT_SESSION_ACTIVE,
|
||||
.route=SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE, .principal=admin };
|
||||
publish_slot(&active,0);
|
||||
assert(admin_console_is_current(&token,&admin));
|
||||
/* Production publication carries both console output and principal binding. */
|
||||
assert(s_session_snapshots[0].tx_pending && s_session_snapshots[0].admin_output_pending);
|
||||
assert(!strcmp(s_session_snapshots[0].username,"admin"));
|
||||
uint8_t output[4096]; size_t n;
|
||||
assert(admin_ssh_console_read_output(&token,output,sizeof(output),&n)==ESP_OK && n);
|
||||
publish_slot(&active,0);
|
||||
assert(admin_console_drained(&token));
|
||||
active.state=SSH_TRANSPORT_SESSION_FREE; active.principal_valid=false;
|
||||
publish_slot(&active,0);
|
||||
user_principal_t empty={0};
|
||||
assert(!memcmp(&s_console_principals[0],&empty,sizeof(empty)));
|
||||
assert(!admin_console_is_current(&token,&admin));
|
||||
active.state=SSH_TRANSPORT_SESSION_ACTIVE; active.principal_valid=true;
|
||||
publish_slot(&active,0);
|
||||
assert(admin_console_is_current(&token,&admin));
|
||||
admin.username[0]='A'; assert(!admin_console_is_current(&token,&admin)); admin.username[0]='a';
|
||||
active.route=0; publish_slot(&active,0); assert(!admin_console_is_current(&token,&admin));
|
||||
active.route=SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;
|
||||
active.authenticated=false; publish_slot(&active,0); assert(!admin_console_is_current(&token,&admin));
|
||||
active.authenticated=true; publish_slot(&active,0);
|
||||
s_external_close_id[0]=7; assert(!admin_console_is_current(&token,&admin));
|
||||
ssh_slot_t closing={.session_id=7, .state=SSH_TRANSPORT_SESSION_ACTIVE};
|
||||
assert(consume_external_close(&closing,0) && !s_external_close_id[0]);
|
||||
assert(!admin_console_is_current(&token,&admin));
|
||||
s_session_snapshots[0].close_requested=true;
|
||||
assert(!admin_console_is_current(&token,&admin)); s_session_snapshots[0].close_requested=false;
|
||||
++admin.auth_generation; assert(!admin_console_is_current(&token,&admin)); --admin.auth_generation;
|
||||
++admin.user_id; assert(!admin_console_is_current(&token,&admin)); --admin.user_id;
|
||||
++admin.method; assert(!admin_console_is_current(&token,&admin)); --admin.method;
|
||||
admin.username_length=1; assert(!admin_console_is_current(&token,&admin)); admin.username_length=5;
|
||||
token.transport=1; assert(!admin_console_drained(&token));
|
||||
assert(!admin_console_is_current(&token,&admin));
|
||||
assert(admin_ssh_console_open(&token,&admin)==ESP_ERR_INVALID_ARG);
|
||||
token.transport=0; token.slot_generation=4; assert(!admin_console_drained(&token));
|
||||
assert(!admin_console_is_current(&token,&admin));
|
||||
assert(admin_console_perform(&token,ADMIN_SSH_DEFER_STOP,0)==ESP_ERR_NOT_FOUND && stopped==0);
|
||||
token.slot_generation=3; s_session_snapshots[0].tx_pending=true;
|
||||
token.slot_generation=3;
|
||||
/* Same physical session can be assigned the other console slot. */
|
||||
admin_ssh_console_close(&token);
|
||||
token.slot_index=1;
|
||||
assert(admin_ssh_console_open(&token,&admin)==ESP_OK);
|
||||
active.console_slot_index=1; publish_slot(&active,0);
|
||||
assert(admin_console_is_current(&token,&admin));
|
||||
assert(s_console_slot_indices[0]==1);
|
||||
admin_ssh_console_token_t wrong=token; wrong.slot_index=0;
|
||||
assert(!admin_console_is_current(&wrong,&admin) && !admin_console_drained(&wrong));
|
||||
assert(admin_console_perform(&wrong,ADMIN_SSH_DEFER_STOP,0)==ESP_ERR_NOT_FOUND);
|
||||
/* A colliding published ID with a stale generation cannot steal the lookup. */
|
||||
s_session_snapshots[1]=s_session_snapshots[0];
|
||||
++s_session_snapshots[1].generation; s_console_slot_indices[1]=0;
|
||||
assert(admin_console_is_current(&token,&admin));
|
||||
assert(admin_ssh_console_read_output(&token,output,sizeof(output),&n)==ESP_OK && n);
|
||||
publish_slot(&active,0);
|
||||
s_session_snapshots[0].tx_pending=true;
|
||||
assert(!admin_console_drained(&token)); s_session_snapshots[0].tx_pending=false;
|
||||
assert(admin_console_perform(&token,ADMIN_CONSOLE_DEFER_SELF_CLOSE,99)==ESP_OK && disconnected==7);
|
||||
assert(admin_console_perform(&token,ADMIN_SSH_DEFER_DISCONNECT,99)==ESP_OK && disconnected==99);
|
||||
@@ -31,5 +111,5 @@ static void test_adapter(void)
|
||||
assert(admin_console_perform(&token,ADMIN_SSH_DEFER_HOST_KEY_ROTATE,0)==ESP_OK && rotated==1);
|
||||
assert(admin_console_perform(&token,ADMIN_SSH_DEFER_HOST_KEY_RESET,0)==ESP_OK && reset==1);
|
||||
assert(admin_console_perform(&token,ADMIN_SSH_DEFER_REBOOT,0)==ESP_OK && restarted==1);
|
||||
puts("PASS: actual SSH adapter identity/drain checks, legacy admission and lifecycle action routing");
|
||||
puts("PASS: actual SSH snapshot/principal publication and wiping, adapter identity/drain checks, legacy admission and lifecycle action routing");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ typedef int esp_err_t;
|
||||
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE,
|
||||
ESP_ERR_NO_MEM, ESP_ERR_TIMEOUT, ESP_ERR_NOT_SUPPORTED, ESP_ERR_NOT_FOUND };
|
||||
enum { USER_ROLE_USER, USER_ROLE_ADMIN };
|
||||
typedef struct { int role; } user_principal_t;
|
||||
#define USER_DATABASE_USERNAME_CAPACITY 16U
|
||||
typedef struct {
|
||||
uint32_t user_id, auth_generation;
|
||||
int role, method;
|
||||
size_t username_length;
|
||||
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
|
||||
} user_principal_t;
|
||||
typedef unsigned TickType_t;
|
||||
typedef void *TaskHandle_t;
|
||||
typedef int portMUX_TYPE;
|
||||
@@ -38,7 +44,7 @@ static size_t strlcpy(char *d, const char *s, size_t n) {
|
||||
memcpy(d, s, k); d[k] = 0; } return len;
|
||||
}
|
||||
static esp_err_t user_database_principal_is_current(const user_principal_t *p, bool *c)
|
||||
{ (void)p; *c = principal_current; return ESP_OK; }
|
||||
{ (void)p; assert(!lock_depth); *c = principal_current; return ESP_OK; }
|
||||
static const char *esp_err_to_name(int e) { (void)e; return "fake"; }
|
||||
static TaskHandle_t xTaskGetCurrentTaskHandle(void) { return current_task; }
|
||||
static unsigned xTaskGetTickCount(void) { return ticks; }
|
||||
@@ -57,7 +63,8 @@ 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; }
|
||||
static SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *s) { return s; }
|
||||
static int xSemaphoreTake(SemaphoreHandle_t s, unsigned t)
|
||||
{ if (t && prompt_hook) prompt_hook(); int r=*s; *s=0; return r; }
|
||||
{ assert(!lock_depth); if (t && !*s) { ticks+=t; if (prompt_hook) prompt_hook(); }
|
||||
int r=*s; *s=0; return r; }
|
||||
static int xSemaphoreGive(SemaphoreHandle_t s) { *s=1; return 1; }
|
||||
static void linenoiseSetMaxLineLen(unsigned n) { (void)n; }
|
||||
static char *linenoise(const char *p) { (void)p; return NULL; }
|
||||
@@ -67,8 +74,7 @@ static bool console_completion_expand(const char *s, char *d, size_t n)
|
||||
{ (void)s; (void)d; (void)n; if (completion_hook) completion_hook(); return false; }
|
||||
static bool console_completion_format_matches(const char *s, char *d, size_t n, size_t *len)
|
||||
{ (void)s; *len=strlcpy(d,"help\r\n",n); return true; }
|
||||
static size_t esp_console_split_argv(char *s, char **v, size_t n)
|
||||
{ (void)s; (void)v; (void)n; return 0; } /* Real parser tested by admin_ssh_policy. */
|
||||
size_t esp_console_split_argv(char *s, char **v, size_t n);
|
||||
static esp_err_t esp_console_run(const char *s, int *r)
|
||||
{ (void)s; ++runs; if (command_hook) command_hook(); *r=0; return ESP_OK; }
|
||||
typedef struct { const char *command, *help, *hint; int (*func)(int,char **); void *argtable; } esp_console_cmd_t;
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
No target scheduler, socket library, or hardware execution is claimed.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IDF = Path(os.environ.get("IDF_PATH", str(Path.home() / ".platformio/packages/framework-espidf")))
|
||||
parser = str(IDF / "components/console/split_argv.c")
|
||||
source = (ROOT / "src/admin_ssh_console.c").read_text()
|
||||
header = (ROOT / "src/admin_ssh_console.h").read_text()
|
||||
def strip_includes(text):
|
||||
@@ -21,18 +24,18 @@ with tempfile.TemporaryDirectory(prefix="admin-console-boundary-") as directory:
|
||||
+ (ROOT / "tests/admin_console_boundary/test.c").read_text())
|
||||
(path / "test.c").write_text(unit)
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-g", str(path / "test.c"),
|
||||
"-g", str(path / "test.c"), parser,
|
||||
"-o", str(path / "test")], check=True, timeout=30)
|
||||
subprocess.run([str(path / "test")], check=True, timeout=10)
|
||||
ssh = (ROOT / "src/ssh_transport.c").read_text()
|
||||
adapter = ssh[ssh.index("static bool admin_console_drained("):
|
||||
ssh.index("static bool consume_external_close(")]
|
||||
adapter = ssh[ssh.index("static admin_ssh_console_token_t admin_console_token("):
|
||||
ssh.index("static void *ssh_malloc(")]
|
||||
unit = ((ROOT / "tests/admin_console_boundary/fakes.h").read_text()
|
||||
+ strip_includes(header) + "\n" + strip_includes(source)
|
||||
+ (ROOT / "tests/admin_console_boundary/adapter.c").read_text()
|
||||
+ adapter + "\nint main(void) { test_adapter(); }\n")
|
||||
(path / "adapter.c").write_text(unit)
|
||||
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-variable", str(path / "adapter.c"),
|
||||
"-Wno-unused-variable", str(path / "adapter.c"), parser,
|
||||
"-o", str(path / "adapter")], check=True, timeout=30)
|
||||
subprocess.run([str(path / "adapter")], check=True, timeout=10)
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
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,
|
||||
@@ -9,6 +17,7 @@ static esp_err_t perform(const admin_ssh_console_token_t *t,
|
||||
{ (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)
|
||||
@@ -40,12 +49,141 @@ static void close_during_command(void)
|
||||
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_sessions[0].executing=true; s_sessions[0].command_pending=true; }
|
||||
{ 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_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\" \"stop\"\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;
|
||||
@@ -74,6 +212,7 @@ int main(void)
|
||||
++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;
|
||||
@@ -128,5 +267,6 @@ int main(void)
|
||||
admin_ssh_console_close(&a);
|
||||
assert(ssh_output_write(&a,"x",1)==-1 && errno==EPIPE);
|
||||
assert(!lock_depth);
|
||||
test_currentness();
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user