1174 lines
45 KiB
C
1174 lines
45 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* Serialized, bounded administrative SSH command worker. */
|
|
|
|
#include "admin_ssh_console.h"
|
|
|
|
#include <errno.h>
|
|
#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
|
|
#define ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY 4096U
|
|
#define ADMIN_SSH_CONSOLE_RESPONSE_RESERVE 512U
|
|
#define ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH 4U
|
|
#define ADMIN_SSH_CONSOLE_TASK_STACK_SIZE 12288U
|
|
#define ADMIN_SSH_CONSOLE_TASK_PRIORITY 4U
|
|
#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;
|
|
size_t input_cursor;
|
|
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;
|
|
uint8_t escape_parameters[4];
|
|
size_t escape_parameter_length;
|
|
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];
|
|
} admin_session_t;
|
|
|
|
typedef enum {
|
|
ADMIN_REQUEST_SSH = 0,
|
|
ADMIN_REQUEST_UART0,
|
|
} admin_request_origin_t;
|
|
|
|
typedef struct {
|
|
admin_request_origin_t origin;
|
|
admin_ssh_console_token_t token;
|
|
user_principal_t principal;
|
|
TaskHandle_t completion_task;
|
|
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];
|
|
/* admin_ssh_console_feed_input() is called only by the sole SSH owner task. */
|
|
static char s_completion_output[CONSOLE_COMPLETION_OUTPUT_CAPACITY];
|
|
|
|
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)
|
|
{
|
|
return xTaskGetCurrentTaskHandle() == s_task && s_dispatch_remote;
|
|
}
|
|
|
|
const user_principal_t *admin_ssh_console_dispatch_principal(void)
|
|
{
|
|
return admin_ssh_console_dispatch_is_remote() ? &s_dispatch_principal : NULL;
|
|
}
|
|
|
|
static bool token_valid(const admin_ssh_console_token_t *token)
|
|
{
|
|
return token != NULL && token->slot_index < ADMIN_SSH_CONSOLE_MAX_SESSIONS &&
|
|
token->session_id != 0U && token->slot_generation != 0U;
|
|
}
|
|
|
|
static bool token_identity_matches(const admin_session_t *session,
|
|
const admin_ssh_console_token_t *token)
|
|
{
|
|
return token_valid(token) && session->token.session_id == token->session_id &&
|
|
session->token.slot_generation == token->slot_generation;
|
|
}
|
|
|
|
static bool token_matches(const admin_session_t *session,
|
|
const admin_ssh_console_token_t *token)
|
|
{
|
|
return session->active && token_identity_matches(session, token);
|
|
}
|
|
|
|
static bool append_output_locked(admin_session_t *session,
|
|
const uint8_t *data, size_t length)
|
|
{
|
|
if (data == NULL || length > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length) {
|
|
return false;
|
|
}
|
|
size_t write_offset = (session->output_start + session->output_length) %
|
|
ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY;
|
|
size_t first = length;
|
|
if (first > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - write_offset) {
|
|
first = ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - write_offset;
|
|
}
|
|
memcpy(session->output + write_offset, data, first);
|
|
if (length > first) {
|
|
memcpy(session->output, data + first, length - first);
|
|
}
|
|
session->output_length += length;
|
|
return true;
|
|
}
|
|
|
|
static bool worker_write(const admin_ssh_console_token_t *token,
|
|
const char *text)
|
|
{
|
|
if (!token_valid(token) || text == NULL) {
|
|
return false;
|
|
}
|
|
size_t length = strlen(text);
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
bool written = token_matches(session, token) &&
|
|
append_output_locked(session, (const uint8_t *)text, length);
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return written;
|
|
}
|
|
|
|
|
|
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> ";
|
|
char cursor_back[16] = {0};
|
|
size_t tail_length = session->input_length - session->input_cursor;
|
|
size_t cursor_back_length = 0U;
|
|
if (tail_length > 0U) {
|
|
int written = snprintf(cursor_back, sizeof(cursor_back), "\x1b[%uD",
|
|
(unsigned int)tail_length);
|
|
if (written < 0 || (size_t)written >= sizeof(cursor_back)) {
|
|
return false;
|
|
}
|
|
cursor_back_length = (size_t)written;
|
|
}
|
|
size_t required = sizeof(prefix) - 1U + session->input_length + cursor_back_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);
|
|
(void)append_output_locked(session, session->input, session->input_length);
|
|
return cursor_back_length == 0U ||
|
|
append_output_locked(session, (const uint8_t *)cursor_back, cursor_back_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);
|
|
|
|
static void editor_key_locked(admin_session_t *session, uint8_t key)
|
|
{
|
|
if (key == 'A' || key == 'B') {
|
|
history_move_locked(session, key == 'A');
|
|
return;
|
|
}
|
|
if (key == 'C' && session->input_cursor < session->input_length) {
|
|
++session->input_cursor;
|
|
} else if (key == 'D' && session->input_cursor > 0U) {
|
|
--session->input_cursor;
|
|
} else if (key == 'H') {
|
|
session->input_cursor = 0U;
|
|
} else if (key == 'F') {
|
|
session->input_cursor = session->input_length;
|
|
} else if (key == 'X' && session->input_cursor < session->input_length) {
|
|
memmove(session->input + session->input_cursor,
|
|
session->input + session->input_cursor + 1U,
|
|
session->input_length - session->input_cursor);
|
|
--session->input_length;
|
|
session->history_position = -1;
|
|
} else {
|
|
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
|
|
return;
|
|
}
|
|
(void)redraw_line_locked(session);
|
|
}
|
|
|
|
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);
|
|
session->input_cursor = session->input_length;
|
|
} 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;
|
|
session->input_cursor = session->input_length;
|
|
} else {
|
|
memcpy(session->input, session->history[session->history_position],
|
|
sizeof(session->input));
|
|
session->input_length = strlen((const char *)session->input);
|
|
session->input_cursor = session->input_length;
|
|
}
|
|
}
|
|
(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)
|
|
{
|
|
const admin_ssh_console_token_t *token = cookie;
|
|
if (!token_valid(token) || buffer == NULL || length <= 0) {
|
|
return length == 0 ? 0 : -1;
|
|
}
|
|
|
|
size_t offset = 0U;
|
|
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(5000U);
|
|
while (offset < (size_t)length) {
|
|
const uint8_t value = (uint8_t)buffer[offset];
|
|
uint8_t translated[2] = {value, 0U};
|
|
size_t translated_length = 1U;
|
|
if (value == '\n' && !s_dispatch_output_previous_cr) {
|
|
translated[0] = '\r';
|
|
translated[1] = '\n';
|
|
translated_length = 2U;
|
|
}
|
|
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
bool active = token_matches(session, token);
|
|
size_t available = active
|
|
? ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length
|
|
: 0U;
|
|
bool written = active && available >= translated_length &&
|
|
append_output_locked(session, translated, translated_length);
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
|
|
if (!active) {
|
|
errno = EPIPE;
|
|
return offset == 0U ? -1 : (int)offset;
|
|
}
|
|
if (written) {
|
|
s_dispatch_output_previous_cr = value == '\r';
|
|
++offset;
|
|
continue;
|
|
}
|
|
if ((int32_t)(xTaskGetTickCount() - deadline) >= 0) {
|
|
errno = EAGAIN;
|
|
return offset == 0U ? -1 : (int)offset;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(5U));
|
|
}
|
|
return length;
|
|
}
|
|
|
|
static bool remote_command_allowed(const admin_request_t *request)
|
|
{
|
|
char copy[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
|
memcpy(copy, request->line, sizeof(copy));
|
|
char *argv[ADMIN_SSH_CONSOLE_MAX_ARGUMENTS] = {0};
|
|
/* 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], "user") == 0 && argc >= 2U &&
|
|
(strcmp(argv[1], "bootstrap") == 0 || strcmp(argv[1], "recover") == 0)) {
|
|
allowed = false;
|
|
}
|
|
secure_wipe(copy, sizeof(copy));
|
|
return allowed;
|
|
}
|
|
|
|
static void report_command_result(esp_err_t error, int command_result)
|
|
{
|
|
if (error == ESP_ERR_NOT_FOUND) {
|
|
printf("Unrecognized command\n");
|
|
} else if (error == ESP_OK && command_result != 0) {
|
|
printf("Command returned non-zero error code: 0x%x (%s)\n",
|
|
command_result, esp_err_to_name(command_result));
|
|
} else if (error != ESP_OK && error != ESP_ERR_INVALID_ARG) {
|
|
printf("Internal error: %s\n", esp_err_to_name(error));
|
|
}
|
|
}
|
|
|
|
static int command_exit(int argc, char **argv)
|
|
{
|
|
(void)argv;
|
|
if (argc != 1) {
|
|
printf("Usage: exit\n");
|
|
return 1;
|
|
}
|
|
if (!admin_ssh_console_dispatch_is_remote()) {
|
|
printf("The exit command is available only from an administrative SSH session.\n");
|
|
return 1;
|
|
}
|
|
|
|
esp_err_t error = admin_ssh_console_dispatch_defer(
|
|
ADMIN_SSH_DEFER_DISCONNECT, s_dispatch_token.session_id);
|
|
if (error != ESP_OK) {
|
|
printf("Could not schedule SSH session close: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("SSH session close scheduled after output drains.\n");
|
|
return 0;
|
|
}
|
|
|
|
static void dispatch_registered_command(admin_request_t *request)
|
|
{
|
|
FILE *saved_stdout = stdout;
|
|
FILE *saved_stderr = stderr;
|
|
FILE *remote_stream = NULL;
|
|
if (request->origin == ADMIN_REQUEST_SSH) {
|
|
remote_stream = funopen(&request->token, NULL, ssh_output_write, NULL, NULL);
|
|
if (remote_stream == NULL) {
|
|
(void)worker_write(&request->token, "Could not create command output stream.\r\n");
|
|
return;
|
|
}
|
|
setvbuf(remote_stream, NULL, _IONBF, 0);
|
|
stdout = remote_stream;
|
|
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;
|
|
secure_wipe(&s_dispatch_principal, sizeof(s_dispatch_principal));
|
|
}
|
|
|
|
int command_result = 0;
|
|
esp_err_t error = esp_console_run((const char *)request->line, &command_result);
|
|
report_command_result(error, command_result);
|
|
fflush(stdout);
|
|
|
|
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;
|
|
stderr = saved_stderr;
|
|
fclose(remote_stream);
|
|
}
|
|
}
|
|
|
|
static void worker_task(void *context)
|
|
{
|
|
(void)context;
|
|
for (;;) {
|
|
admin_request_t request;
|
|
if (xQueueReceive(s_request_queue, &request, portMAX_DELAY) != pdTRUE) {
|
|
continue;
|
|
}
|
|
if (request.origin == ADMIN_REQUEST_UART0) {
|
|
dispatch_registered_command(&request);
|
|
if (request.completion_task != NULL) {
|
|
xTaskNotifyGive(request.completion_task);
|
|
}
|
|
secure_wipe(&request, sizeof(request));
|
|
continue;
|
|
}
|
|
|
|
bool current = false;
|
|
esp_err_t auth_error = user_database_principal_is_current(&request.principal, ¤t);
|
|
bool active;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[request.token.slot_index];
|
|
active = token_matches(session, &request.token) && session->command_pending &&
|
|
!session->executing;
|
|
if (active) {
|
|
session->executing = true;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
bool authorized = active && auth_error == ESP_OK && current &&
|
|
request.principal.role == USER_ROLE_ADMIN &&
|
|
remote_command_allowed(&request);
|
|
if (authorized) {
|
|
dispatch_registered_command(&request);
|
|
} else if (active) {
|
|
(void)worker_write(&request.token,
|
|
auth_error == ESP_OK && current
|
|
? "Command is restricted to physical UART0.\r\n"
|
|
: "Administrative authorization is no longer current; closing session.\r\n");
|
|
}
|
|
bool prompt = false;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
session = &s_sessions[request.token.slot_index];
|
|
if (token_matches(session, &request.token)) {
|
|
session->executing = false;
|
|
session->command_pending = false;
|
|
prompt = auth_error == ESP_OK && current &&
|
|
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. */
|
|
secure_wipe(session, sizeof(*session));
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (prompt) {
|
|
print_prompt(&request.token);
|
|
}
|
|
secure_wipe(&request, sizeof(request));
|
|
}
|
|
}
|
|
|
|
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;
|
|
setvbuf(stdin, NULL, _IONBF, 0);
|
|
linenoiseSetMaxLineLen(ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY);
|
|
printf("\r\nType 'help' to get the list of commands.\r\n"
|
|
"Use UP/DOWN arrows for history and TAB for completion.\r\n");
|
|
|
|
for (;;) {
|
|
char *line = linenoise("serial-tool> ");
|
|
if (line == NULL) {
|
|
continue;
|
|
}
|
|
if (line[0] != '\0') {
|
|
(void)linenoiseHistoryAdd(line);
|
|
}
|
|
|
|
admin_request_t request = {
|
|
.origin = ADMIN_REQUEST_UART0,
|
|
.completion_task = xTaskGetCurrentTaskHandle(),
|
|
};
|
|
strlcpy((char *)request.line, line, sizeof(request.line));
|
|
linenoiseFree(line);
|
|
|
|
(void)ulTaskNotifyTake(pdTRUE, 0U);
|
|
if (xQueueSend(s_request_queue, &request, portMAX_DELAY) == pdTRUE) {
|
|
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
} else {
|
|
printf("Administrative command queue unavailable.\n");
|
|
}
|
|
secure_wipe(&request, sizeof(request));
|
|
}
|
|
}
|
|
|
|
esp_err_t admin_ssh_console_init(void)
|
|
{
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_initialized) {
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_OK;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
s_request_queue = xQueueCreateStatic(ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH,
|
|
sizeof(admin_request_t), s_request_queue_bytes,
|
|
&s_request_queue_storage);
|
|
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,
|
|
NULL, ADMIN_SSH_CONSOLE_TASK_PRIORITY, &s_task) != pdPASS) {
|
|
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);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t admin_ssh_console_register_commands(void)
|
|
{
|
|
const esp_console_cmd_t command = {
|
|
.command = "exit",
|
|
.help = "Close the current administrative SSH session",
|
|
.hint = NULL,
|
|
.func = &command_exit,
|
|
.argtable = NULL,
|
|
};
|
|
return esp_console_cmd_register(&command);
|
|
}
|
|
|
|
esp_err_t admin_ssh_console_start_uart_frontend(void)
|
|
{
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool initialized = s_initialized;
|
|
bool already_started = s_dispatch_ready;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!initialized) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (already_started) {
|
|
return ESP_OK;
|
|
}
|
|
if (xTaskCreate(uart_frontend_task, "admin_uart_console",
|
|
ADMIN_UART_CONSOLE_TASK_STACK_SIZE, NULL,
|
|
ADMIN_UART_CONSOLE_TASK_PRIORITY, &s_uart_task) != pdPASS) {
|
|
s_uart_task = NULL;
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
taskENTER_CRITICAL(&s_lock);
|
|
s_dispatch_ready = true;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
|
|
const user_principal_t *principal)
|
|
{
|
|
if (!token_valid(token) || principal == NULL || principal->role != USER_ROLE_ADMIN) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool ready = s_initialized && s_dispatch_ready;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
bool current = false;
|
|
if (user_database_principal_is_current(principal, ¤t) != ESP_OK || !current) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
if (session->executing) {
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
secure_wipe(session, sizeof(*session));
|
|
session->active = true;
|
|
session->history_position = -1;
|
|
session->token = *token;
|
|
session->principal = *principal;
|
|
static const char banner[] =
|
|
"ESP32 Serial Swiss Army Knife administrative SSH shell\r\n";
|
|
static const char prompt[] =
|
|
"Run 'help' for supported remote administrative commands.\r\nadmin@serial-tool> ";
|
|
(void)append_output_locked(session, (const uint8_t *)banner, sizeof(banner) - 1U);
|
|
(void)append_output_locked(session, (const uint8_t *)prompt, sizeof(prompt) - 1U);
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_OK;
|
|
}
|
|
|
|
void admin_ssh_console_close(const admin_ssh_console_token_t *token)
|
|
{
|
|
if (!token_valid(token)) {
|
|
return;
|
|
}
|
|
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)
|
|
{
|
|
if (!token_valid(token)) {
|
|
return false;
|
|
}
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
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);
|
|
return accepts;
|
|
}
|
|
|
|
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
|
const uint8_t *data, size_t length,
|
|
size_t *consumed)
|
|
{
|
|
if (consumed == NULL || !token_valid(token) || (data == NULL && length != 0U)) {
|
|
return false;
|
|
}
|
|
*consumed = 0U;
|
|
for (size_t index = 0U; index < length; ++index) {
|
|
admin_request_t request = {0};
|
|
bool submit = false;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
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 (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) {
|
|
if (value == '[') {
|
|
session->escape_state = 2U;
|
|
session->escape_parameter_length = 0U;
|
|
} else if (value == 'O') {
|
|
session->escape_state = 3U;
|
|
} else {
|
|
session->escape_state = 0U;
|
|
}
|
|
} else if (session->escape_state == 3U) {
|
|
editor_key_locked(session, value);
|
|
session->escape_state = 0U;
|
|
} else if (value >= 'A' && value <= 'Z') {
|
|
editor_key_locked(session, value);
|
|
session->escape_state = 0U;
|
|
} else if (value == '~') {
|
|
uint8_t key = 0U;
|
|
if (session->escape_parameter_length > 0U) {
|
|
switch (session->escape_parameters[0]) {
|
|
case '1':
|
|
case '7':
|
|
key = 'H';
|
|
break;
|
|
case '3':
|
|
key = 'X';
|
|
break;
|
|
case '4':
|
|
case '8':
|
|
key = 'F';
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
editor_key_locked(session, key);
|
|
session->escape_state = 0U;
|
|
} else if ((value == ';' || (value >= '0' && value <= '9')) &&
|
|
session->escape_parameter_length < sizeof(session->escape_parameters)) {
|
|
session->escape_parameters[session->escape_parameter_length++] = value;
|
|
} 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') {
|
|
if (session->input_cursor != session->input_length) {
|
|
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
|
|
++*consumed;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
continue;
|
|
}
|
|
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};
|
|
size_t candidates_length = 0U;
|
|
bool expanded = console_completion_expand(current, completed,
|
|
sizeof(completed));
|
|
bool candidates_formatted = !expanded &&
|
|
console_completion_format_matches(current, s_completion_output,
|
|
sizeof(s_completion_output),
|
|
&candidates_length);
|
|
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->input_cursor = session->input_length;
|
|
session->history_position = -1;
|
|
(void)redraw_line_locked(session);
|
|
} else if (candidates_formatted && candidates_length > 0U &&
|
|
candidates_length + 2U + sizeof("admin@serial-tool> ") - 1U +
|
|
session->input_length <=
|
|
ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length) {
|
|
(void)append_output_locked(session, (const uint8_t *)"\r\n", 2U);
|
|
(void)append_output_locked(session, (const uint8_t *)s_completion_output,
|
|
candidates_length);
|
|
(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;
|
|
}
|
|
bool exit_requested = value == 0x04U && session->input_length == 0U;
|
|
if (value == '\r' || value == '\n' || exit_requested) {
|
|
session->discard_next_lf = value == '\r';
|
|
if (exit_requested) {
|
|
memcpy(request.line, "exit", sizeof("exit"));
|
|
} else {
|
|
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->input_cursor = 0U;
|
|
session->history_position = -1;
|
|
session->command_pending = true;
|
|
(void)append_output_locked(session,
|
|
(const uint8_t *)(exit_requested ? "^D\r\n" : "\r\n"),
|
|
exit_requested ? sizeof("^D\r\n") - 1U
|
|
: sizeof("\r\n") - 1U);
|
|
submit = true;
|
|
} else if (value == 0x03U) {
|
|
secure_wipe(session->input, sizeof(session->input));
|
|
session->input_length = 0U;
|
|
session->input_cursor = 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_cursor > 0U) {
|
|
memmove(session->input + session->input_cursor - 1U,
|
|
session->input + session->input_cursor,
|
|
session->input_length - session->input_cursor + 1U);
|
|
--session->input_cursor;
|
|
--session->input_length;
|
|
session->history_position = -1;
|
|
(void)redraw_line_locked(session);
|
|
}
|
|
} else if (value >= 0x20U && value <= 0x7eU) {
|
|
if (session->input_length >= ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY) {
|
|
session->input_length = 0U;
|
|
session->input_cursor = 0U;
|
|
(void)append_output_locked(session, (const uint8_t *)
|
|
"\r\nCommand too long; discarded.\r\nadmin@serial-tool> ",
|
|
sizeof("\r\nCommand too long; discarded.\r\nadmin@serial-tool> ") - 1U);
|
|
} else {
|
|
memmove(session->input + session->input_cursor + 1U,
|
|
session->input + session->input_cursor,
|
|
session->input_length - session->input_cursor + 1U);
|
|
session->input[session->input_cursor++] = value;
|
|
++session->input_length;
|
|
session->history_position = -1;
|
|
if (session->input_cursor == session->input_length) {
|
|
(void)append_output_locked(session, &value, 1U);
|
|
} else {
|
|
(void)redraw_line_locked(session);
|
|
}
|
|
}
|
|
}
|
|
++*consumed;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (submit && xQueueSend(s_request_queue, &request, 0U) != pdTRUE) {
|
|
taskENTER_CRITICAL(&s_lock);
|
|
session = &s_sessions[token->slot_index];
|
|
if (token_matches(session, token)) {
|
|
session->command_pending = false;
|
|
(void)append_output_locked(session, (const uint8_t *)
|
|
"Administrative command queue is busy.\r\nadmin@serial-tool> ",
|
|
sizeof("Administrative command queue is busy.\r\nadmin@serial-tool> ") - 1U);
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
secure_wipe(&request, sizeof(request));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
esp_err_t admin_ssh_console_read_output(const admin_ssh_console_token_t *token,
|
|
uint8_t *data, size_t capacity,
|
|
size_t *received)
|
|
{
|
|
if (received == NULL || data == NULL || capacity == 0U || !token_valid(token)) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
*received = 0U;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
if (!token_matches(session, token)) {
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
size_t copied = session->output_length < capacity ? session->output_length : capacity;
|
|
size_t first = copied;
|
|
if (first > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_start) {
|
|
first = ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_start;
|
|
}
|
|
memcpy(data, session->output + session->output_start, first);
|
|
if (copied > first) {
|
|
memcpy(data + first, session->output, copied - first);
|
|
}
|
|
session->output_start = (session->output_start + copied) %
|
|
ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY;
|
|
session->output_length -= copied;
|
|
*received = copied;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t admin_ssh_console_get_session_snapshot(
|
|
const admin_ssh_console_token_t *token,
|
|
admin_ssh_console_session_snapshot_t *snapshot)
|
|
{
|
|
if (snapshot == NULL || !token_valid(token)) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
memset(snapshot, 0, sizeof(*snapshot));
|
|
taskENTER_CRITICAL(&s_lock);
|
|
admin_session_t *session = &s_sessions[token->slot_index];
|
|
if (!token_matches(session, token)) {
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
snapshot->active = true;
|
|
snapshot->command_pending = session->command_pending;
|
|
snapshot->input_pending = session->input_length != 0U;
|
|
snapshot->output_pending = session->output_length != 0U;
|
|
snapshot->input_length = session->input_length;
|
|
snapshot->output_length = session->output_length;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_OK;
|
|
}
|