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);
}
}