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:
+434
-45
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user