Expand admin SSH command capabilities

Add per-session history, tab completion, interactive prompts, and
bounded input handling. Support deferred lifecycle and host-key actions
after output drains, and document the expanded administration workflow.
This commit is contained in:
2026-08-30 18:34:01 +02:00
parent 0a1bbd6782
commit c2c11fee4e
14 changed files with 699 additions and 224 deletions
+434 -45
View File
@@ -7,12 +7,16 @@
#include <stdio.h>
#include <string.h>
#include "console_completion.h"
#include "esp_console.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "linenoise/linenoise.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "user_database.h"
#define ADMIN_SSH_CONSOLE_MAX_SESSIONS 2U
@@ -24,15 +28,41 @@
#define ADMIN_UART_CONSOLE_TASK_STACK_SIZE 6144U
#define ADMIN_UART_CONSOLE_TASK_PRIORITY 3U
#define ADMIN_SSH_CONSOLE_MAX_ARGUMENTS 10U
#define ADMIN_SSH_CONSOLE_HISTORY_DEPTH 4U
#define ADMIN_SSH_CONTROL_QUEUE_LENGTH 2U
#define ADMIN_SSH_CONTROL_TASK_STACK_SIZE 4096U
#define ADMIN_SSH_CONTROL_TASK_PRIORITY 3U
typedef enum {
ADMIN_PROMPT_NONE = 0,
ADMIN_PROMPT_WAITING,
ADMIN_PROMPT_SUBMITTED,
ADMIN_PROMPT_CANCELLED,
ADMIN_PROMPT_DISCONNECTED,
} admin_prompt_state_t;
typedef struct {
bool active;
bool command_pending;
bool executing;
bool deferred_action_pending;
admin_ssh_console_token_t token;
user_principal_t principal;
size_t input_length;
uint8_t input[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
uint8_t history[ADMIN_SSH_CONSOLE_HISTORY_DEPTH]
[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
uint8_t history_count;
int8_t history_position;
uint8_t draft[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
size_t draft_length;
uint8_t escape_state;
bool discard_next_lf;
admin_prompt_state_t prompt_state;
bool prompt_hidden;
size_t prompt_capacity;
size_t prompt_length;
uint8_t prompt_input[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
size_t output_start;
size_t output_length;
uint8_t output[ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY];
@@ -51,6 +81,12 @@ typedef struct {
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
} admin_request_t;
typedef struct {
admin_ssh_deferred_action_type_t action;
admin_ssh_console_token_t token;
uint32_t argument;
} admin_control_request_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static admin_session_t s_sessions[ADMIN_SSH_CONSOLE_MAX_SESSIONS];
@@ -58,14 +94,23 @@ static StaticQueue_t s_request_queue_storage;
static uint8_t s_request_queue_bytes[ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH *
sizeof(admin_request_t)];
static QueueHandle_t s_request_queue;
static StaticQueue_t s_control_queue_storage;
static uint8_t s_control_queue_bytes[ADMIN_SSH_CONTROL_QUEUE_LENGTH *
sizeof(admin_control_request_t)];
static QueueHandle_t s_control_queue;
static StaticSemaphore_t s_prompt_done_storage;
static SemaphoreHandle_t s_prompt_done;
static TaskHandle_t s_task;
static TaskHandle_t s_uart_task;
static TaskHandle_t s_control_task;
static bool s_initialized;
static bool s_dispatch_ready;
/* Accessed only by the single dispatcher task while a callback is running. */
static bool s_dispatch_remote;
static bool s_dispatch_output_previous_cr;
static admin_ssh_console_token_t s_dispatch_token;
static user_principal_t s_dispatch_principal;
static ssh_transport_snapshot_t s_control_ssh_snapshot;
bool admin_ssh_console_dispatch_is_remote(void)
{
@@ -137,6 +182,158 @@ static void print_prompt(const admin_ssh_console_token_t *token)
(void)worker_write(token, "admin@serial-tool> ");
}
static bool redraw_line_locked(admin_session_t *session)
{
static const char prefix[] = "\r\x1b[2Kadmin@serial-tool> ";
size_t required = sizeof(prefix) - 1U + session->input_length;
if (required > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length) {
return append_output_locked(session, (const uint8_t *)"\a", 1U);
}
(void)append_output_locked(session, (const uint8_t *)prefix, sizeof(prefix) - 1U);
return append_output_locked(session, session->input, session->input_length);
}
static void history_commit_locked(admin_session_t *session)
{
if (session->input_length == 0U ||
(session->history_count > 0U &&
strcmp((const char *)session->history[0], (const char *)session->input) == 0)) {
return;
}
for (size_t index = ADMIN_SSH_CONSOLE_HISTORY_DEPTH - 1U; index > 0U; --index) {
memcpy(session->history[index], session->history[index - 1U],
sizeof(session->history[index]));
}
memcpy(session->history[0], session->input, sizeof(session->history[0]));
if (session->history_count < ADMIN_SSH_CONSOLE_HISTORY_DEPTH) {
++session->history_count;
}
}
static void history_move_locked(admin_session_t *session, bool older)
{
if (older) {
if (session->history_count == 0U ||
session->history_position + 1 >= (int8_t)session->history_count) {
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
return;
}
if (session->history_position < 0) {
memcpy(session->draft, session->input, sizeof(session->draft));
session->draft_length = session->input_length;
}
++session->history_position;
memcpy(session->input, session->history[session->history_position],
sizeof(session->input));
session->input_length = strlen((const char *)session->input);
} else {
if (session->history_position < 0) {
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
return;
}
--session->history_position;
if (session->history_position < 0) {
memcpy(session->input, session->draft, sizeof(session->input));
session->input_length = session->draft_length;
} else {
memcpy(session->input, session->history[session->history_position],
sizeof(session->input));
session->input_length = strlen((const char *)session->input);
}
}
(void)redraw_line_locked(session);
}
esp_err_t admin_ssh_console_dispatch_read_input(
const char *prompt, uint8_t *output, size_t capacity,
bool hidden, size_t *output_length)
{
if (!admin_ssh_console_dispatch_is_remote() || prompt == NULL || output == NULL ||
output_length == NULL || capacity == 0U ||
capacity > ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U) {
return ESP_ERR_INVALID_ARG;
}
*output_length = 0U;
memset(output, 0, capacity);
(void)xSemaphoreTake(s_prompt_done, 0U);
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[s_dispatch_token.slot_index];
if (!token_matches(session, &s_dispatch_token) || !session->executing ||
session->prompt_state != ADMIN_PROMPT_NONE) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
secure_wipe(session->prompt_input, sizeof(session->prompt_input));
session->prompt_length = 0U;
session->prompt_capacity = capacity;
session->prompt_hidden = hidden;
session->prompt_state = ADMIN_PROMPT_WAITING;
bool published = append_output_locked(session, (const uint8_t *)prompt, strlen(prompt));
if (!published) {
session->prompt_state = ADMIN_PROMPT_NONE;
session->prompt_capacity = 0U;
}
taskEXIT_CRITICAL(&s_lock);
if (!published) {
return ESP_ERR_NO_MEM;
}
if (xSemaphoreTake(s_prompt_done, portMAX_DELAY) != pdTRUE) {
return ESP_FAIL;
}
esp_err_t result = ESP_ERR_INVALID_STATE;
taskENTER_CRITICAL(&s_lock);
session = &s_sessions[s_dispatch_token.slot_index];
if (session->prompt_state == ADMIN_PROMPT_SUBMITTED) {
memcpy(output, session->prompt_input, session->prompt_length);
*output_length = session->prompt_length;
result = ESP_OK;
} else if (session->prompt_state == ADMIN_PROMPT_DISCONNECTED) {
result = ESP_ERR_NOT_FOUND;
}
secure_wipe(session->prompt_input, sizeof(session->prompt_input));
session->prompt_length = 0U;
session->prompt_capacity = 0U;
session->prompt_hidden = false;
session->prompt_state = ADMIN_PROMPT_NONE;
taskEXIT_CRITICAL(&s_lock);
return result;
}
esp_err_t admin_ssh_console_dispatch_defer(
admin_ssh_deferred_action_type_t action, uint32_t argument)
{
if (!admin_ssh_console_dispatch_is_remote() || action == ADMIN_SSH_DEFER_NONE) {
return ESP_ERR_INVALID_STATE;
}
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[s_dispatch_token.slot_index];
bool valid = token_matches(session, &s_dispatch_token) &&
!session->deferred_action_pending;
if (valid) {
session->deferred_action_pending = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!valid) {
return ESP_ERR_INVALID_STATE;
}
admin_control_request_t request = {
.action = action,
.token = s_dispatch_token,
.argument = argument,
};
if (xQueueSend(s_control_queue, &request, 0U) == pdTRUE) {
return ESP_OK;
}
taskENTER_CRITICAL(&s_lock);
session = &s_sessions[s_dispatch_token.slot_index];
if (token_matches(session, &s_dispatch_token)) {
session->deferred_action_pending = false;
}
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_TIMEOUT;
}
static int ssh_output_write(void *cookie, const char *buffer, int length)
{
@@ -193,42 +390,9 @@ static bool remote_command_allowed(const admin_request_t *request)
/* Use exactly the same quote/escape parser as esp_console_run(). */
size_t argc = esp_console_split_argv(copy, argv, ADMIN_SSH_CONSOLE_MAX_ARGUMENTS);
bool allowed = argc > 0U;
if (allowed && strcmp(argv[0], "reboot") == 0) {
if (allowed && strcmp(argv[0], "user") == 0 && argc >= 2U &&
(strcmp(argv[1], "bootstrap") == 0 || strcmp(argv[1], "recover") == 0)) {
allowed = false;
} else if (allowed && strcmp(argv[0], "ping") == 0) {
allowed = false;
} else if (allowed && strcmp(argv[0], "user") == 0 && argc >= 2U) {
if (strcmp(argv[1], "bootstrap") == 0 || strcmp(argv[1], "recover") == 0) {
allowed = false;
} else if (strcmp(argv[1], "add") == 0) {
allowed = argc == 5U && strcmp(argv[4], "--generate") == 0;
} else if (strcmp(argv[1], "password") == 0) {
allowed = argc == 4U && strcmp(argv[3], "--generate") == 0;
} else if (strcmp(argv[1], "key") == 0 && argc >= 3U &&
strcmp(argv[2], "add") == 0) {
allowed = argc == 6U;
}
} else if (allowed && strcmp(argv[0], "wifi") == 0 && argc >= 2U) {
if (strcmp(argv[1], "ping") == 0) {
allowed = false;
} else if (strcmp(argv[1], "profile") == 0 && argc >= 3U &&
strcmp(argv[2], "secret") == 0) {
allowed = false;
} else if (strcmp(argv[1], "ap") == 0 && argc >= 3U &&
(strcmp(argv[2], "secret") == 0 ||
strcmp(argv[2], "show-secret") == 0)) {
allowed = false;
}
} else if (allowed && strcmp(argv[0], "web") == 0 && argc >= 2U) {
allowed = strcmp(argv[1], "credentials") != 0 &&
strcmp(argv[1], "certificate") != 0 &&
strcmp(argv[1], "reset") != 0;
} else if (allowed && strcmp(argv[0], "ssh") == 0 && argc >= 2U) {
allowed = strcmp(argv[1], "start") != 0 && strcmp(argv[1], "stop") != 0 &&
strcmp(argv[1], "disconnect") != 0 && strcmp(argv[1], "reset") != 0;
if (allowed && strcmp(argv[1], "host-key") == 0) {
allowed = argc == 3U && strcmp(argv[2], "info") == 0;
}
}
secure_wipe(copy, sizeof(copy));
return allowed;
@@ -262,6 +426,7 @@ static void dispatch_registered_command(admin_request_t *request)
stderr = remote_stream;
s_dispatch_output_previous_cr = false;
s_dispatch_remote = true;
s_dispatch_token = request->token;
s_dispatch_principal = request->principal;
} else {
s_dispatch_remote = false;
@@ -275,6 +440,7 @@ static void dispatch_registered_command(admin_request_t *request)
s_dispatch_remote = false;
s_dispatch_output_previous_cr = false;
secure_wipe(&s_dispatch_token, sizeof(s_dispatch_token));
secure_wipe(&s_dispatch_principal, sizeof(s_dispatch_principal));
if (remote_stream != NULL) {
stdout = saved_stdout;
@@ -329,7 +495,8 @@ static void worker_task(void *context)
session->executing = false;
session->command_pending = false;
prompt = auth_error == ESP_OK && current &&
request.principal.role == USER_ROLE_ADMIN;
request.principal.role == USER_ROLE_ADMIN &&
!session->deferred_action_pending;
} else if (!session->active && session->executing &&
token_identity_matches(session, &request.token)) {
/* A disconnect invalidated this executing request; erase buffered secrets. */
@@ -343,6 +510,100 @@ static void worker_task(void *context)
}
}
static void finish_deferred_request(const admin_control_request_t *request,
esp_err_t result, bool cancelled)
{
char message[160];
if (cancelled) {
snprintf(message, sizeof(message),
"Deferred action cancelled before SSH output drained.\r\nadmin@serial-tool> ");
} else if (result == ESP_OK) {
snprintf(message, sizeof(message),
"Deferred SSH action completed.\r\nadmin@serial-tool> ");
} else {
snprintf(message, sizeof(message),
"Deferred SSH action failed: %s\r\nadmin@serial-tool> ",
esp_err_to_name(result));
}
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[request->token.slot_index];
if (token_matches(session, &request->token)) {
(void)append_output_locked(session, (const uint8_t *)message, strlen(message));
session->deferred_action_pending = false;
}
taskEXIT_CRITICAL(&s_lock);
}
static void control_task(void *context)
{
(void)context;
for (;;) {
admin_control_request_t request;
if (xQueueReceive(s_control_queue, &request, portMAX_DELAY) != pdTRUE) {
continue;
}
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(10000U);
bool drained = false;
while ((int32_t)(xTaskGetTickCount() - deadline) < 0) {
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[request.token.slot_index];
bool current = token_matches(session, &request.token);
bool console_drained = current && !session->command_pending &&
session->output_length == 0U;
taskEXIT_CRITICAL(&s_lock);
if (!current) {
break;
}
bool transport_drained = false;
if (console_drained &&
ssh_transport_get_snapshot(&s_control_ssh_snapshot) == ESP_OK) {
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
const ssh_transport_session_snapshot_t *slot =
&s_control_ssh_snapshot.sessions[index];
if (slot->active && slot->session_id == request.token.session_id) {
transport_drained = !slot->tx_pending;
break;
}
}
}
if (console_drained && transport_drained) {
drained = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(10U));
}
if (!drained) {
finish_deferred_request(&request, ESP_ERR_TIMEOUT, true);
secure_wipe(&request, sizeof(request));
continue;
}
vTaskDelay(pdMS_TO_TICKS(200U));
esp_err_t result = ESP_OK;
switch (request.action) {
case ADMIN_SSH_DEFER_REBOOT:
esp_restart();
break;
case ADMIN_SSH_DEFER_STOP:
result = ssh_transport_stop();
break;
case ADMIN_SSH_DEFER_DISCONNECT:
result = ssh_transport_disconnect(request.argument);
break;
case ADMIN_SSH_DEFER_HOST_KEY_ROTATE:
result = ssh_transport_replace_host_key(false);
break;
case ADMIN_SSH_DEFER_HOST_KEY_RESET:
result = ssh_transport_replace_host_key(true);
break;
default:
result = ESP_ERR_NOT_SUPPORTED;
break;
}
finish_deferred_request(&request, result, false);
secure_wipe(&request, sizeof(request));
}
}
static void uart_frontend_task(void *context)
{
(void)context;
@@ -388,7 +649,11 @@ esp_err_t admin_ssh_console_init(void)
s_request_queue = xQueueCreateStatic(ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH,
sizeof(admin_request_t), s_request_queue_bytes,
&s_request_queue_storage);
if (s_request_queue == NULL) {
s_control_queue = xQueueCreateStatic(ADMIN_SSH_CONTROL_QUEUE_LENGTH,
sizeof(admin_control_request_t),
s_control_queue_bytes, &s_control_queue_storage);
s_prompt_done = xSemaphoreCreateBinaryStatic(&s_prompt_done_storage);
if (s_request_queue == NULL || s_control_queue == NULL || s_prompt_done == NULL) {
return ESP_ERR_NO_MEM;
}
if (xTaskCreate(worker_task, "admin_ssh_console", ADMIN_SSH_CONSOLE_TASK_STACK_SIZE,
@@ -396,6 +661,13 @@ esp_err_t admin_ssh_console_init(void)
s_task = NULL;
return ESP_ERR_NO_MEM;
}
if (xTaskCreate(control_task, "admin_ssh_control", ADMIN_SSH_CONTROL_TASK_STACK_SIZE,
NULL, ADMIN_SSH_CONTROL_TASK_PRIORITY, &s_control_task) != pdPASS) {
s_control_task = NULL;
vTaskDelete(s_task);
s_task = NULL;
return ESP_ERR_NO_MEM;
}
taskENTER_CRITICAL(&s_lock);
s_initialized = true;
taskEXIT_CRITICAL(&s_lock);
@@ -450,6 +722,7 @@ esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
}
secure_wipe(session, sizeof(*session));
session->active = true;
session->history_position = -1;
session->token = *token;
session->principal = *principal;
static const char banner[] =
@@ -470,13 +743,21 @@ void admin_ssh_console_close(const admin_ssh_console_token_t *token)
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[token->slot_index];
bool matched = token_matches(session, token);
bool wake_prompt = false;
if (matched) {
if (session->prompt_state == ADMIN_PROMPT_WAITING) {
session->prompt_state = ADMIN_PROMPT_DISCONNECTED;
wake_prompt = true;
}
session->active = false;
if (!session->executing) {
secure_wipe(session, sizeof(*session));
}
}
taskEXIT_CRITICAL(&s_lock);
if (wake_prompt) {
(void)xSemaphoreGive(s_prompt_done);
}
}
bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token)
@@ -486,7 +767,10 @@ bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token)
}
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[token->slot_index];
bool accepts = token_matches(session, token) && !session->command_pending &&
bool shell_input = !session->command_pending && !session->deferred_action_pending;
bool prompt_input = session->command_pending && session->executing &&
session->prompt_state == ADMIN_PROMPT_WAITING;
bool accepts = token_matches(session, token) && (shell_input || prompt_input) &&
session->output_length <= ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY -
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE;
taskEXIT_CRITICAL(&s_lock);
@@ -506,40 +790,143 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
bool submit = false;
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[token->slot_index];
if (!token_matches(session, token) || session->command_pending) {
if (!token_matches(session, token)) {
taskEXIT_CRITICAL(&s_lock);
return *consumed != 0U;
}
uint8_t value = data[index];
if (session->command_pending) {
if (!session->executing || session->prompt_state != ADMIN_PROMPT_WAITING) {
taskEXIT_CRITICAL(&s_lock);
return *consumed != 0U;
}
bool wake_prompt = false;
if (session->discard_next_lf && value == '\n') {
session->discard_next_lf = false;
} else {
session->discard_next_lf = false;
if (value == '\r' || value == '\n') {
session->discard_next_lf = value == '\r';
session->prompt_state = ADMIN_PROMPT_SUBMITTED;
(void)append_output_locked(session, (const uint8_t *)"\r\n", 2U);
wake_prompt = true;
} else if (value == 0x03U) {
secure_wipe(session->prompt_input, sizeof(session->prompt_input));
session->prompt_length = 0U;
session->prompt_state = ADMIN_PROMPT_CANCELLED;
(void)append_output_locked(session, (const uint8_t *)"^C\r\n", 4U);
wake_prompt = true;
} else if (value == 0x08U || value == 0x7fU) {
if (session->prompt_length > 0U) {
session->prompt_input[--session->prompt_length] = 0U;
if (!session->prompt_hidden) {
(void)append_output_locked(session,
(const uint8_t *)"\b \b", 3U);
}
}
} else if (value >= 0x20U && value <= 0x7eU) {
if (session->prompt_length + 1U < session->prompt_capacity) {
session->prompt_input[session->prompt_length++] = value;
if (!session->prompt_hidden) {
(void)append_output_locked(session, &value, 1U);
}
} else {
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
}
}
}
++*consumed;
taskEXIT_CRITICAL(&s_lock);
if (wake_prompt) {
(void)xSemaphoreGive(s_prompt_done);
}
continue;
}
if (session->output_length >= ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY -
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE) {
taskEXIT_CRITICAL(&s_lock);
return *consumed != 0U;
}
if (value == '\r' || value == '\n') {
if (value == '\n' && session->input_length == 0U) {
++*consumed;
taskEXIT_CRITICAL(&s_lock);
continue;
if (session->discard_next_lf && value == '\n') {
session->discard_next_lf = false;
++*consumed;
taskEXIT_CRITICAL(&s_lock);
continue;
}
session->discard_next_lf = false;
if (session->escape_state != 0U) {
if (session->escape_state == 1U && (value == '[' || value == 'O')) {
session->escape_state = 2U;
} else if (session->escape_state == 2U) {
if (value == 'A' || value == 'B') {
history_move_locked(session, value == 'A');
}
session->escape_state = 0U;
} else {
session->escape_state = 0U;
}
++*consumed;
taskEXIT_CRITICAL(&s_lock);
continue;
}
if (value == 0x1bU) {
session->escape_state = 1U;
++*consumed;
taskEXIT_CRITICAL(&s_lock);
continue;
}
if (value == '\t') {
char current[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
memcpy(current, session->input, sizeof(current));
++*consumed;
taskEXIT_CRITICAL(&s_lock);
char completed[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U] = {0};
bool expanded = console_completion_expand(current, completed,
sizeof(completed));
taskENTER_CRITICAL(&s_lock);
session = &s_sessions[token->slot_index];
if (token_matches(session, token) && !session->command_pending &&
memcmp(current, session->input, sizeof(current)) == 0) {
if (expanded) {
strlcpy((char *)session->input, completed, sizeof(session->input));
session->input_length = strlen((const char *)session->input);
session->history_position = -1;
(void)redraw_line_locked(session);
} else {
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
}
}
taskEXIT_CRITICAL(&s_lock);
secure_wipe(current, sizeof(current));
secure_wipe(completed, sizeof(completed));
continue;
}
if (value == '\r' || value == '\n') {
session->discard_next_lf = value == '\r';
history_commit_locked(session);
memcpy(request.line, session->input, session->input_length);
request.origin = ADMIN_REQUEST_SSH;
request.token = *token;
request.principal = session->principal;
secure_wipe(session->input, sizeof(session->input));
session->input_length = 0U;
session->history_position = -1;
session->command_pending = true;
(void)append_output_locked(session, (const uint8_t *)"\r\n",
sizeof("\r\n") - 1U);
submit = true;
} else if (value == 0x03U) {
secure_wipe(session->input, sizeof(session->input));
session->input_length = 0U;
session->history_position = -1;
(void)append_output_locked(session, (const uint8_t *)"^C\r\n",
sizeof("^C\r\n") - 1U);
(void)append_output_locked(session, (const uint8_t *)"admin@serial-tool> ",
sizeof("admin@serial-tool> ") - 1U);
} else if (value == 0x08U || value == 0x7fU) {
if (session->input_length > 0U) {
--session->input_length;
session->input[--session->input_length] = 0U;
session->history_position = -1;
(void)append_output_locked(session, (const uint8_t *)"\b \b",
sizeof("\b \b") - 1U);
}
@@ -550,7 +937,9 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
"\r\nCommand too long; discarded.\r\nadmin@serial-tool> ",
sizeof("\r\nCommand too long; discarded.\r\nadmin@serial-tool> ") - 1U);
} else {
session->history_position = -1;
session->input[session->input_length++] = value;
session->input[session->input_length] = 0U;
(void)append_output_locked(session, &value, 1U);
}
}
+14
View File
@@ -23,6 +23,15 @@ typedef struct {
uint32_t slot_generation;
} admin_ssh_console_token_t;
typedef enum {
ADMIN_SSH_DEFER_NONE = 0,
ADMIN_SSH_DEFER_REBOOT,
ADMIN_SSH_DEFER_STOP,
ADMIN_SSH_DEFER_DISCONNECT,
ADMIN_SSH_DEFER_HOST_KEY_ROTATE,
ADMIN_SSH_DEFER_HOST_KEY_RESET,
} admin_ssh_deferred_action_type_t;
typedef struct {
bool active;
bool command_pending;
@@ -40,6 +49,11 @@ esp_err_t admin_ssh_console_start_uart_frontend(void);
/* Valid only while a registered command callback runs on the dispatcher task. */
bool admin_ssh_console_dispatch_is_remote(void);
const user_principal_t *admin_ssh_console_dispatch_principal(void);
esp_err_t admin_ssh_console_dispatch_read_input(
const char *prompt, uint8_t *output, size_t capacity,
bool hidden, size_t *output_length);
esp_err_t admin_ssh_console_dispatch_defer(
admin_ssh_deferred_action_type_t action, uint32_t argument);
/* The token and principal are copied; no SSH or socket objects cross this boundary. */
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
+48
View File
@@ -10,6 +10,11 @@
#include "esp_console.h"
#include "linenoise/linenoise.h"
static const char *const s_root_candidates[] = {
"help", "status", "debug", "display", "serial", "broker", "usb", "user",
"wifi", "web", "ssh", "ping", "nslookup", "traceroute", "reboot", "memory",
};
/* Keep full-line candidate strings grouped by their registered root command. */
static const char *const s_completion_candidates[] = {
/* Hardware debug commands and safe fixed arguments. */
@@ -197,6 +202,49 @@ static const char *const s_completion_candidates[] = {
"ssh reset --force",
};
bool console_completion_expand(const char *line, char *completed, size_t capacity)
{
if (line == NULL || completed == NULL || capacity == 0U) {
return false;
}
size_t line_length = strlen(line);
const char *const *candidates = strchr(line, ' ') == NULL
? s_root_candidates
: s_completion_candidates;
size_t candidate_count = strchr(line, ' ') == NULL
? sizeof(s_root_candidates) / sizeof(s_root_candidates[0])
: sizeof(s_completion_candidates) /
sizeof(s_completion_candidates[0]);
const char *first = NULL;
size_t common_length = 0U;
for (size_t index = 0U; index < candidate_count; ++index) {
const char *candidate = candidates[index];
if (strncmp(candidate, line, line_length) != 0) {
continue;
}
if (first == NULL) {
first = candidate;
common_length = strlen(candidate);
continue;
}
size_t candidate_length = strlen(candidate);
if (common_length > candidate_length) {
common_length = candidate_length;
}
size_t offset = line_length;
while (offset < common_length && first[offset] == candidate[offset]) {
++offset;
}
common_length = offset;
}
if (first == NULL || common_length <= line_length || common_length >= capacity) {
return false;
}
memcpy(completed, first, common_length);
completed[common_length] = '\0';
return true;
}
static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
void *buffer,
size_t size)
+6
View File
@@ -2,6 +2,9 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
@@ -9,6 +12,9 @@ extern "C" {
/* Install late-terminal upgrade handling and project-specific completion. */
void console_completion_install(void);
/* Bounded longest-prefix completion shared by the UART and admin SSH frontends. */
bool console_completion_expand(const char *line, char *completed, size_t capacity);
#ifdef __cplusplus
}
#endif
+2 -2
View File
@@ -33,8 +33,8 @@ static esp_err_t read_input(const char *prompt, uint8_t *output, size_t capacity
return ESP_ERR_INVALID_ARG;
}
if (admin_ssh_console_dispatch_is_remote()) {
printf("Interactive input is restricted to physical UART0.\n");
return ESP_ERR_NOT_SUPPORTED;
return admin_ssh_console_dispatch_read_input(
prompt, output, capacity, hidden, output_length);
}
*output_length = 0U;
memset(output, 0, capacity);
+108 -87
View File
@@ -14,6 +14,7 @@
#include "esp_err.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "lwip/inet.h"
#include "lwip/inet_chksum.h"
@@ -163,109 +164,109 @@ static int resolve_ping_target(const char *host, ip_addr_t *target,
return 0;
}
typedef enum {
PING_EVENT_LINE = 0,
PING_EVENT_END,
} ping_event_kind_t;
typedef struct {
TaskHandle_t waiting_task;
ping_event_kind_t kind;
char line[128];
char address[NUMERIC_ADDRESS_CAPACITY];
uint32_t transmitted;
uint32_t received;
uint32_t duration_ms;
esp_err_t profile_error;
esp_err_t delete_error;
bool received_reply;
bool summary_valid;
} ping_event_t;
typedef struct {
QueueHandle_t queue;
} ping_wait_context_t;
#define PING_EVENT_QUEUE_LENGTH (PING_MAX_COUNT + 1U)
static StaticQueue_t s_ping_queue_storage;
static uint8_t s_ping_queue_bytes[PING_EVENT_QUEUE_LENGTH * sizeof(ping_event_t)];
static QueueHandle_t s_ping_queue;
static void ping_on_success(esp_ping_handle_t handle, void *arguments)
{
(void)arguments;
ping_wait_context_t *context = arguments;
ping_event_t event = {.kind = PING_EVENT_LINE};
uint16_t sequence = 0U;
uint8_t ttl = 0U;
uint32_t reply_size = 0U;
uint32_t elapsed_ms = 0U;
ip_addr_t reply_address;
char numeric[NUMERIC_ADDRESS_CAPACITY];
if (esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
&sequence, sizeof(sequence)) != ESP_OK ||
esp_ping_get_profile(handle, ESP_PING_PROF_SIZE,
&reply_size, sizeof(reply_size)) != ESP_OK ||
esp_ping_get_profile(handle, ESP_PING_PROF_TIMEGAP,
&elapsed_ms, sizeof(elapsed_ms)) != ESP_OK ||
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
&reply_address, sizeof(reply_address)) != ESP_OK ||
ipaddr_ntoa_r(&reply_address, numeric, (int)sizeof(numeric)) == NULL) {
printf("ping: received a reply but could not read its profile\n");
return;
}
if (IP_IS_V4(&reply_address) &&
esp_ping_get_profile(handle, ESP_PING_PROF_TTL,
&ttl, sizeof(ttl)) == ESP_OK) {
printf("%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
" ttl=%u time=%" PRIu32 " ms\n",
reply_size, numeric, sequence, (unsigned int)ttl, elapsed_ms);
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
bool valid = esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
&sequence, sizeof(sequence)) == ESP_OK &&
esp_ping_get_profile(handle, ESP_PING_PROF_SIZE,
&reply_size, sizeof(reply_size)) == ESP_OK &&
esp_ping_get_profile(handle, ESP_PING_PROF_TIMEGAP,
&elapsed_ms, sizeof(elapsed_ms)) == ESP_OK &&
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
&reply_address, sizeof(reply_address)) == ESP_OK &&
ipaddr_ntoa_r(&reply_address, numeric, (int)sizeof(numeric)) != NULL;
if (!valid) {
strlcpy(event.line, "ping: received a reply but could not read its profile",
sizeof(event.line));
} else if (IP_IS_V4(&reply_address) &&
esp_ping_get_profile(handle, ESP_PING_PROF_TTL,
&ttl, sizeof(ttl)) == ESP_OK) {
snprintf(event.line, sizeof(event.line),
"%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
" ttl=%u time=%" PRIu32 " ms",
reply_size, numeric, sequence, (unsigned int)ttl, elapsed_ms);
} else {
printf("%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
" time=%" PRIu32 " ms\n",
reply_size, numeric, sequence, elapsed_ms);
snprintf(event.line, sizeof(event.line),
"%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
" time=%" PRIu32 " ms",
reply_size, numeric, sequence, elapsed_ms);
}
(void)xQueueSend(context->queue, &event, 0U);
}
static void ping_on_timeout(esp_ping_handle_t handle, void *arguments)
{
(void)arguments;
ping_wait_context_t *context = arguments;
ping_event_t event = {.kind = PING_EVENT_LINE};
uint16_t sequence = 0U;
ip_addr_t target_address;
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
if (esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
&sequence, sizeof(sequence)) == ESP_OK &&
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
&target_address, sizeof(target_address)) == ESP_OK) {
(void)ipaddr_ntoa_r(&target_address, numeric, (int)sizeof(numeric));
}
printf("From %s: icmp_seq=%" PRIu16 " timeout\n", numeric, sequence);
snprintf(event.line, sizeof(event.line), "From %s: icmp_seq=%" PRIu16 " timeout",
numeric, sequence);
(void)xQueueSend(context->queue, &event, 0U);
}
static void ping_on_end(esp_ping_handle_t handle, void *arguments)
{
ping_wait_context_t *context = (ping_wait_context_t *)arguments;
uint32_t transmitted = 0U;
uint32_t received = 0U;
uint32_t duration_ms = 0U;
ping_wait_context_t *context = arguments;
ping_event_t event = {.kind = PING_EVENT_END, .profile_error = ESP_OK};
ip_addr_t target_address;
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
esp_err_t profile_error = esp_ping_get_profile(
handle, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
if (profile_error == ESP_OK) {
profile_error = esp_ping_get_profile(
handle, ESP_PING_PROF_REPLY, &received, sizeof(received));
strlcpy(event.address, "?", sizeof(event.address));
event.profile_error = esp_ping_get_profile(
handle, ESP_PING_PROF_REQUEST, &event.transmitted, sizeof(event.transmitted));
if (event.profile_error == ESP_OK) {
event.profile_error = esp_ping_get_profile(
handle, ESP_PING_PROF_REPLY, &event.received, sizeof(event.received));
}
if (profile_error == ESP_OK) {
profile_error = esp_ping_get_profile(
handle, ESP_PING_PROF_DURATION, &duration_ms, sizeof(duration_ms));
if (event.profile_error == ESP_OK) {
event.profile_error = esp_ping_get_profile(
handle, ESP_PING_PROF_DURATION, &event.duration_ms, sizeof(event.duration_ms));
}
if (esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
&target_address, sizeof(target_address)) == ESP_OK) {
(void)ipaddr_ntoa_r(&target_address, numeric, (int)sizeof(numeric));
(void)ipaddr_ntoa_r(&target_address, event.address, (int)sizeof(event.address));
}
if (profile_error == ESP_OK) {
context->received_reply = received > 0U;
context->summary_valid = true;
uint32_t loss_percent = transmitted == 0U
? 0U
: ((transmitted - received) * 100U) / transmitted;
printf("\n--- %s ping statistics ---\n", numeric);
printf("%" PRIu32 " packets transmitted, %" PRIu32
" received, %" PRIu32 "%% packet loss, time %" PRIu32 " ms\n",
transmitted, received, loss_percent, duration_ms);
} else {
printf("ping: session ended, but summary profile retrieval failed: %s\n",
esp_err_to_name(profile_error));
}
/* Stop ping_sock's task before waking the higher-priority console caller. */
context->delete_error = esp_ping_delete_session(handle);
xTaskNotifyGive(context->waiting_task);
event.delete_error = esp_ping_delete_session(handle);
(void)xQueueSend(context->queue, &event, 0U);
}
static int execute_ping(int argc, char **argv)
@@ -288,12 +289,17 @@ static int execute_ping(int argc, char **argv)
return 1;
}
ping_wait_context_t context = {
.waiting_task = xTaskGetCurrentTaskHandle(),
.delete_error = ESP_FAIL,
};
/* Remove any unrelated notification before this command begins waiting. */
(void)ulTaskNotifyTake(pdTRUE, 0U);
if (s_ping_queue == NULL) {
s_ping_queue = xQueueCreateStatic(PING_EVENT_QUEUE_LENGTH, sizeof(ping_event_t),
s_ping_queue_bytes, &s_ping_queue_storage);
} else {
(void)xQueueReset(s_ping_queue);
}
if (s_ping_queue == NULL) {
printf("ping: could not allocate event queue\n");
return 1;
}
ping_wait_context_t context = {.queue = s_ping_queue};
esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
config.count = count;
@@ -321,21 +327,36 @@ static int execute_ping(int argc, char **argv)
return 1;
}
/* Finite count guarantees on_ping_end; blocking keeps console output ordered. */
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) == 0U) {
printf("ping: wait for session completion failed\n");
(void)esp_ping_stop(session);
(void)esp_ping_delete_session(session);
return 1;
for (;;) {
ping_event_t event;
if (xQueueReceive(s_ping_queue, &event, portMAX_DELAY) != pdTRUE) {
printf("ping: wait for session completion failed\n");
return 1;
}
if (event.kind == PING_EVENT_LINE) {
printf("%s\n", event.line);
continue;
}
if (event.profile_error != ESP_OK) {
printf("ping: session ended, but summary profile retrieval failed: %s\n",
esp_err_to_name(event.profile_error));
return 1;
}
uint32_t loss_percent = event.transmitted == 0U
? 0U
: ((event.transmitted - event.received) * 100U) /
event.transmitted;
printf("\n--- %s ping statistics ---\n", event.address);
printf("%" PRIu32 " packets transmitted, %" PRIu32
" received, %" PRIu32 "%% packet loss, time %" PRIu32 " ms\n",
event.transmitted, event.received, loss_percent, event.duration_ms);
if (event.delete_error != ESP_OK) {
printf("ping: could not delete session: %s\n",
esp_err_to_name(event.delete_error));
return 1;
}
return event.received > 0U ? 0 : 1;
}
bool command_succeeded = context.summary_valid && context.received_reply;
if (context.delete_error != ESP_OK) {
printf("ping: could not delete session: %s\n",
esp_err_to_name(context.delete_error));
return 1;
}
return command_succeeded ? 0 : 1;
}
static bool socket_addresses_equal(const struct addrinfo *left,
+47 -1
View File
@@ -9,6 +9,7 @@
#include <stdlib.h>
#include <string.h>
#include "admin_ssh_console.h"
#include "esp_console.h"
#include "mbedtls/base64.h"
#include "secure_random.h"
@@ -207,6 +208,18 @@ static bool parse_session_id(const char *text, uint32_t *session_id)
static int replace_host_key(bool reset)
{
if (admin_ssh_console_dispatch_is_remote()) {
esp_err_t deferred = admin_ssh_console_dispatch_defer(
reset ? ADMIN_SSH_DEFER_HOST_KEY_RESET : ADMIN_SSH_DEFER_HOST_KEY_ROTATE, 0U);
if (deferred != ESP_OK) {
printf("Could not schedule SSH host-key replacement: %s\n",
esp_err_to_name(deferred));
return 1;
}
printf("SSH host-key %s scheduled after output drains; all SSH sessions will close.\n",
reset ? "reset" : "rotation");
return 0;
}
ssh_security_metadata_t before = {0};
bool had_before = ssh_security_get_metadata(&before) == ESP_OK;
esp_err_t error = ssh_transport_replace_host_key(reset);
@@ -256,6 +269,16 @@ static int command_ssh(int argc, char **argv)
return 0;
}
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
if (admin_ssh_console_dispatch_is_remote()) {
esp_err_t deferred = admin_ssh_console_dispatch_defer(
ADMIN_SSH_DEFER_STOP, 0U);
if (deferred != ESP_OK) {
printf("Could not schedule SSH stop: %s\n", esp_err_to_name(deferred));
return 1;
}
printf("SSH stop scheduled after output drains; all SSH sessions will close.\n");
return 0;
}
esp_err_t error = ssh_transport_stop();
if (error != ESP_OK) {
printf("Could not stop SSH: %s\n", esp_err_to_name(error));
@@ -282,7 +305,30 @@ static int command_ssh(int argc, char **argv)
printf("Session ID must be a nonzero decimal integer.\n");
return 1;
}
esp_err_t error = ssh_transport_disconnect(session_id);
esp_err_t error;
if (admin_ssh_console_dispatch_is_remote()) {
ssh_transport_snapshot_t snapshot;
error = ssh_transport_get_snapshot(&snapshot);
bool found = false;
if (error == ESP_OK) {
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (snapshot.sessions[index].active &&
snapshot.sessions[index].session_id == session_id) {
found = true;
break;
}
}
if (!found) {
error = ESP_ERR_NOT_FOUND;
}
}
if (error == ESP_OK) {
error = admin_ssh_console_dispatch_defer(
ADMIN_SSH_DEFER_DISCONNECT, session_id);
}
} else {
error = ssh_transport_disconnect(session_id);
}
if (error != ESP_OK) {
printf("Could not disconnect SSH session: %s\n", esp_err_to_name(error));
return 1;
+10
View File
@@ -6,6 +6,7 @@
#include <stdint.h>
#include <stdio.h>
#include "admin_ssh_console.h"
#include "esp_console.h"
#include "esp_heap_caps.h"
#include "esp_system.h"
@@ -47,6 +48,15 @@ static int command_reboot(int argc, char **argv)
return 1;
}
if (admin_ssh_console_dispatch_is_remote()) {
esp_err_t error = admin_ssh_console_dispatch_defer(ADMIN_SSH_DEFER_REBOOT, 0U);
if (error != ESP_OK) {
printf("Could not schedule reboot: %s\n", esp_err_to_name(error));
return 1;
}
printf("Reboot scheduled after SSH output drains; unsaved changes will be lost.\n");
return 0;
}
printf("Rebooting now; unsaved RAM-only configuration changes will be lost.\n");
fflush(stdout);
/* Give the UART driver time to transmit the acknowledgement before reset. */
-12
View File
@@ -421,10 +421,6 @@ static int command_user_inner(int argc, char **argv)
print_usage();
return 1;
}
if (remote && !generated) {
printf("Interactive password entry is restricted to physical UART0; use --generate.\n");
return 1;
}
return add_user(argv[2], argv[3], generated);
}
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
@@ -464,10 +460,6 @@ static int command_user_inner(int argc, char **argv)
print_usage();
return 1;
}
if (remote && !generated) {
printf("Interactive password entry is restricted to physical UART0; use --generate.\n");
return 1;
}
if (remote && generated && principal != NULL &&
strlen(argv[2]) == principal->username_length &&
memcmp(argv[2], principal->username, principal->username_length) == 0) {
@@ -478,10 +470,6 @@ static int command_user_inner(int argc, char **argv)
}
if (argc == 4 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "add") == 0) {
if (remote) {
printf("Interactive SSH key entry is restricted to physical UART0; provide type and Base64 arguments.\n");
return 1;
}
return add_key(argv[3]);
}
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
+12 -68
View File
@@ -9,19 +9,16 @@
#include <stdlib.h>
#include <string.h>
#include "admin_ssh_console.h"
#include "driver/uart.h"
#include "console_input.h"
#include "esp_console.h"
#include "esp_err.h"
#include "esp_netif_ip_addr.h"
#include "esp_wifi_types.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "network_console.h"
#include "wifi_config.h"
#include "wifi_manager.h"
#define WIFI_CONSOLE_UART UART_NUM_0
#define WIFI_CONSOLE_SECRET_CAPACITY WIFI_CONFIG_PSK_MAX_LEN
static void print_usage(void)
@@ -243,72 +240,19 @@ static esp_err_t apply_candidate(wifi_app_config_t *candidate)
static esp_err_t read_secret_no_echo(uint8_t *secret, uint8_t *secret_len)
{
if (admin_ssh_console_dispatch_is_remote()) {
printf("Interactive secret entry is restricted to physical UART0.\n");
return ESP_ERR_NOT_SUPPORTED;
}
uint8_t buffer[WIFI_CONSOLE_SECRET_CAPACITY];
uint8_t buffer[WIFI_CONSOLE_SECRET_CAPACITY + 1U] = {0};
size_t length = 0U;
memset(buffer, 0, sizeof(buffer));
/*
* esp_console may execute on CR while the terminal's trailing LF is still
* arriving. Let that line ending settle, then discard only pre-prompt RX so
* it cannot be mistaken for an immediately submitted empty secret.
*/
vTaskDelay(1U);
esp_err_t flush_error = uart_flush_input(WIFI_CONSOLE_UART);
if (flush_error != ESP_OK) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("Could not prepare secret input: %s\n", esp_err_to_name(flush_error));
return flush_error;
esp_err_t error = console_input_read_hidden(
"Enter 8..63 printable ASCII characters (input hidden, Ctrl-C cancels): ",
buffer, sizeof(buffer), WIFI_CONFIG_PSK_MIN_LEN,
WIFI_CONFIG_PSK_MAX_LEN, &length);
if (error == ESP_OK) {
memset(secret, 0, WIFI_CONFIG_PSK_MAX_LEN);
memcpy(secret, buffer, length);
*secret_len = (uint8_t)length;
}
printf("Enter 8..63 printable ASCII characters (input hidden, Ctrl-C cancels): ");
fflush(stdout);
for (;;) {
uint8_t byte = 0U;
int received = uart_read_bytes(WIFI_CONSOLE_UART, &byte, 1U, portMAX_DELAY);
if (received != 1) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("\nSecret input failed.\n");
return ESP_FAIL;
}
if (byte == 0x03U) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("\nCancelled.\n");
return ESP_ERR_INVALID_STATE;
}
if (byte == '\r' || byte == '\n') {
break;
}
if (byte == 0x08U || byte == 0x7fU) {
if (length > 0U) {
buffer[--length] = 0U;
}
continue;
}
if (byte < 0x20U || byte > 0x7eU || length >= sizeof(buffer)) {
putchar('\a');
fflush(stdout);
continue;
}
buffer[length++] = byte;
}
putchar('\n');
if (length < WIFI_CONFIG_PSK_MIN_LEN || length > WIFI_CONFIG_PSK_MAX_LEN) {
wifi_config_secure_wipe(buffer, sizeof(buffer));
printf("Secret length must be 8..63 characters.\n");
return ESP_ERR_INVALID_ARG;
}
memset(secret, 0, WIFI_CONFIG_PSK_MAX_LEN);
memcpy(secret, buffer, length);
*secret_len = (uint8_t)length;
wifi_config_secure_wipe(buffer, sizeof(buffer));
return ESP_OK;
return error;
}
static int set_profile(char **argv)