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:
@@ -73,7 +73,7 @@ The firmware provides an interactive UART0 console at `serial-tool>`. Run `help`
|
|||||||
|
|
||||||
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
|
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
|
||||||
|
|
||||||
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. Manage role-based HTTPS/SSH passwords and SSH public keys with the `user` command group on UART0 or an authenticated admin SSH shell; interactive secrets and recovery operations remain UART0-only. `web credentials show` exposes only the legacy migration/recovery credential, not an active Phase 8B network login.
|
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. Authenticated admin SSH sessions expose the shared operational administration registry, including interactive secrets, recovery-material management, network diagnostics, and deferred reboot/SSH lifecycle commands. Only initial administrator bootstrap and explicit recovery of an unavailable user database remain UART0-only. `web credentials show` exposes only the legacy migration/recovery credential, not an active Phase 8B network login.
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Command reference
|
# Command reference
|
||||||
|
|
||||||
UART0 and authenticated `admin` SSH sessions use the same registered command implementations through one serialized dispatcher. Admin SSH exposes the registry subject to a transport policy; commands requiring physical recovery authority, raw UART0 prompts, asynchronous callback output, or mutation of the SSH service carrying the command remain UART0-only. Run `help` for root commands and `<group> help` for a group summary. Configuration changes are RAM-only unless explicitly saved.
|
UART0 and authenticated `admin` SSH sessions use the same registered command implementations through one serialized dispatcher. Admin SSH exposes the full operational registry, including interactive prompts, recovery-secret display, network diagnostics, reboot, and HTTPS/SSH material mutation. Only initial administrator bootstrap and explicit recovery of an unavailable user database remain physically bound to UART0. Run `help` for root commands and `<group> help` for a group summary. Configuration changes are RAM-only unless explicitly saved.
|
||||||
|
|
||||||
## System
|
## System
|
||||||
|
|
||||||
@@ -140,9 +140,11 @@ HTTPS listens on port 443 only. Authenticate with any current user-database user
|
|||||||
|
|
||||||
SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25519` and `ecdsa-sha2-nistp256` public keys. wolfSSH verifies key possession after the database authorizes the username/key pair; unsigned key probes do not complete authentication. A `user` receives the broker-backed UART1 serial stream. An `admin` receives the administration shell instead, does not become a broker client, and cannot acquire a UART1 writer lease.
|
SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25519` and `ecdsa-sha2-nistp256` public keys. wolfSSH verifies key possession after the database authorizes the username/key pair; unsigned key probes do not complete authentication. A `user` receives the broker-backed UART1 serial stream. An `admin` receives the administration shell instead, does not become a broker client, and cannot acquire a UART1 writer lease.
|
||||||
|
|
||||||
UART0 and admin SSH submit to one bounded queue, and one dispatcher task is the sole caller of `esp_console_run()`. Consequently, allowed SSH commands execute the canonical UART0 handlers and produce the same status and mutation behavior rather than using a second command implementation. Remote output is routed into the authenticated session's bounded output ring; only the SSH transport task accesses wolfSSH.
|
UART0 and admin SSH submit to one bounded queue, and one dispatcher task is the sole caller of `esp_console_run()`. Consequently, SSH commands execute the canonical UART0 handlers and produce the same status and mutation behavior rather than using a second command implementation. Remote output is routed into the authenticated session's bounded output ring; only the SSH transport task accesses wolfSSH.
|
||||||
|
|
||||||
The transport policy keeps `reboot`, `ping`/`wifi ping`, user bootstrap/recovery and interactive password/key forms, Wi-Fi secret entry/display, web recovery credentials/certificate/reset operations, and SSH start/stop/disconnect/reset/host-key mutation on physical UART0. Use generated user/password forms and `user key add <username> <type> <base64>` remotely. A connected administrator cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems. Verify the host fingerprint from `ssh host-key info` before connecting.
|
Admin SSH supports four-entry per-session command history with Up/Down, bounded whole-line Tab completion, Backspace/Ctrl-C, and visible or no-echo interactive prompts. History is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
||||||
|
|
||||||
|
`reboot`, `ssh stop`, session disconnect, and SSH host-key reset/rotation are deferred until the command acknowledgement has left both the administration output ring and transport TX buffer. The shell stops accepting another command while such an action is pending. SSH host-key replacement or service stop closes all SSH sessions; reconnect and verify the new fingerprint where applicable. Web recovery credentials/certificates, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. Only `user bootstrap` and `user recover --force` remain UART0-only. A connected administrator still cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||||
|
|
||||||
## Hardware diagnostics
|
## Hardware diagnostics
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -267,10 +267,11 @@ Implementation sequence:
|
|||||||
- Authenticated `admin` SSH shell sessions route to a bounded administration worker and never create a broker client or acquire a serial writer lease. Normal `user` sessions retain the existing broker-backed serial stream.
|
- Authenticated `admin` SSH shell sessions route to a bounded administration worker and never create a broker client or acquire a serial writer lease. Normal `user` sessions retain the existing broker-backed serial stream.
|
||||||
- UART0 and admin SSH now submit complete lines to one fixed-length request queue. A single dispatcher task is the sole caller of ESP-IDF's non-reentrant `esp_console_run()` and therefore executes the same registered command handlers for both entry routes. The former separately implemented reduced SSH command dispatcher has been removed.
|
- UART0 and admin SSH now submit complete lines to one fixed-length request queue. A single dispatcher task is the sole caller of ESP-IDF's non-reentrant `esp_console_run()` and therefore executes the same registered command handlers for both entry routes. The former separately implemented reduced SSH command dispatcher has been removed.
|
||||||
- The worker uses fixed per-session command/input and output buffers. Queue records contain copied secret-free principals and generation-tagged session tokens; late work is discarded after disconnect, slot reuse, role change, password/key mutation, or deletion. Task-local standard streams route canonical handler output into the applicable bounded SSH ring, and only the SSH owner task calls wolfSSH APIs.
|
- The worker uses fixed per-session command/input and output buffers. Queue records contain copied secret-free principals and generation-tagged session tokens; late work is discarded after disconnect, slot reuse, role change, password/key mutation, or deletion. Task-local standard streams route canonical handler output into the applicable bounded SSH ring, and only the SSH owner task calls wolfSSH APIs.
|
||||||
- Transport policy, rather than duplicate command implementations, restricts operations requiring physical recovery authority, raw UART0 prompts, asynchronous callback output, or mutation of the active SSH service. Remote-safe generated account/password operations and non-interactive Ed25519/ECDSA key import use the canonical `user` handler. Bootstrap, database recovery, manual password/key prompts, recovery credentials, Wi-Fi secrets, reboot, ping, and SSH identity/service mutation remain physical-console-only.
|
- Transport-neutral bounded prompts now support interactive user passwords/keys and Wi-Fi secrets over admin SSH without exposing hidden input or allowing another command while a prompt is active. Ping callbacks enqueue typed bounded events and the dispatcher alone formats their output. Four-entry per-session history and whole-line Tab completion are RAM-only and wiped on disconnect.
|
||||||
|
- Authenticated administrators receive the full operational registry, including recovery-secret display, HTTPS material rotation/reset, reboot, ping, and SSH lifecycle/session/host-key mutation. Self-terminating reboot and SSH actions are deferred until acknowledgement output drains, block further shell input, and are executed through the existing synchronous owner APIs from a separate bounded control task. Only initial `user bootstrap` and explicit `user recover --force` remain physical-UART0 operations.
|
||||||
- `ssh sessions` and `ssh counters` identify broker versus admin-console routes, worker command state, queued admin output, admission failures, and input backpressure. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation.
|
- `ssh sessions` and `ssh counters` identify broker versus admin-console routes, worker command state, queued admin output, admission failures, and input backpressure. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation.
|
||||||
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
|
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
|
||||||
- Pending target-hardware validation: route separation from the broker, administrative command input/editing/output/backpressure, generated user/password/key management including the longest ECDSA P-256 import, physical-only command rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation.
|
- Pending target-hardware validation: route separation from the broker, history/Tab editing, interactive visible/hidden prompts, output/backpressure, generated and entered user/password/key management including the longest ECDSA P-256 import, ping event routing, deferred reboot/SSH lifecycle drain behavior, bootstrap/recovery rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation.
|
||||||
4. **Phase 8D — Web user administration — Planned**
|
4. **Phase 8D — Web user administration — Planned**
|
||||||
- Add an admin-only user-management interface and typed, bounded APIs for account CRUD, roles, password generation/change, SSH-key management, and revocation. Never expose a generic HTTP endpoint that executes arbitrary CLI text.
|
- Add an admin-only user-management interface and typed, bounded APIs for account CRUD, roles, password generation/change, SSH-key management, and revocation. Never expose a generic HTTP endpoint that executes arbitrary CLI text.
|
||||||
- Hide administrative navigation and controls for normal users, and enforce every authorization decision server-side so hidden UI is not treated as a security boundary.
|
- Hide administrative navigation and controls for normal users, and enforce every authorization decision server-side so hidden UI is not treated as a security boundary.
|
||||||
|
|||||||
@@ -133,7 +133,11 @@ Use one disposable `admin` and one disposable `user`. Keep UART0 attached throug
|
|||||||
|
|
||||||
### 2. Bounded command processing
|
### 2. Bounded command processing
|
||||||
|
|
||||||
Exercise printable input, backspace, Ctrl-C, CR/LF, an empty line, and a line longer than the documented limit. Confirm the command line is bounded, overflow is discarded through a clear diagnostic, and a new prompt remains usable. Run `help`, `user list`, and `broker clients` in a normal ANSI terminal and confirm every line starts in column zero: canonical LF output must be normalized to CRLF without doubling handlers that already emit CRLF. Run an unsupported command and confirm it is rejected without affecting UART0 or the serial broker. Run the full root `help` output to exercise output-ring draining. With the SSH client temporarily unable to read output, confirm the worker applies input backpressure rather than accepting an unbounded command/output backlog; inspect `ssh counters` for admin-console admission and input-backpressure values.
|
Exercise printable input, backspace, Ctrl-C, CR/LF, an empty line, and a line longer than the documented limit. Confirm the command line is bounded, overflow is discarded through a clear diagnostic, and a new prompt remains usable. Run `help`, `user list`, and `broker clients` in a normal ANSI terminal and confirm every line starts in column zero: canonical LF output must be normalized to CRLF without doubling handlers that already emit CRLF.
|
||||||
|
|
||||||
|
Run at least five distinct commands, then use Up/Down to navigate the four-entry per-session history, return to a saved draft with Down, and confirm older entries are bounded out. Verify history does not survive reconnect and is not shared with a second administrator. Exercise Tab on root and nested prefixes such as `us`, `user l`, `wifi ap sh`, and `ssh host-key i`; confirm unique/common prefixes redraw cleanly without inserting escape-sequence bytes into the command.
|
||||||
|
|
||||||
|
Run an unsupported command and confirm it is rejected without affecting UART0 or the serial broker. Run the full root `help` output to exercise output-ring draining. With the SSH client temporarily unable to read output, confirm the worker applies input backpressure rather than accepting an unbounded command/output backlog; inspect `ssh counters` for admin-console admission and input-backpressure values.
|
||||||
|
|
||||||
### 3. Remote account administration
|
### 3. Remote account administration
|
||||||
|
|
||||||
@@ -148,9 +152,11 @@ user key add <username> ecdsa-sha2-nistp256 <base64-blob>
|
|||||||
|
|
||||||
Confirm the full ECDSA P-256 command is accepted, fingerprints appear in `user show`, a duplicate on the same account is rejected, and the same key can be imported for a second account. Verify subsequent private-key SSH login uses the selected SSH username.
|
Confirm the full ECDSA P-256 command is accepted, fingerprints appear in `user show`, a duplicate on the same account is rejected, and the same key can be imported for a second account. Verify subsequent private-key SSH login uses the selected SSH username.
|
||||||
|
|
||||||
### 4. Physical-only boundary and revocation
|
### 4. Interactive administration, lifecycle actions, and revocation
|
||||||
|
|
||||||
Confirm `user bootstrap`, `user recover --force`, manually entered password/key workflows, Wi-Fi secret entry/display, legacy recovery credentials, web certificate/reset operations, `reboot`, `ping`, and SSH service/identity mutation remain unavailable from SSH and continue to work only on UART0 where applicable. Their rejection must occur before any handler attempts to read raw UART0 input.
|
Confirm only `user bootstrap` and `user recover --force` remain unavailable from SSH and continue to work through physical UART0. From admin SSH, exercise manually entered user passwords and public keys, Wi-Fi station/AP secret entry, AP secret display, legacy web recovery credential display/rotation, HTTPS certificate rotation/reset, and both `ping` and `wifi ping`. Hidden characters must not echo or enter command history; visible key input must support Backspace and Ctrl-C; ping lines must remain ordered and correctly attributed to the invoking SSH session.
|
||||||
|
|
||||||
|
Exercise `ssh disconnect` for another session and the current session. For the other-session case, confirm the acknowledgement drains, the target closes, and the source shell returns. Separately test `reboot`, `ssh stop`, `ssh host-key rotate --force`, and `ssh reset --force`: each must acknowledge scheduling, stop accepting another command, drain output, and then close/reboot as appropriate. Reconnect after key replacement and verify the new fingerprint. Simulate an unread SSH output window and confirm the destructive action cancels after its bounded drain timeout rather than remaining pending forever.
|
||||||
|
|
||||||
While an administrative command is queued or running, use UART0 to change that admin's role/password/key or delete it. Confirm no second remote administrative command runs after the mutation, the SSH session is revoked promptly, and queued output is not delivered to a reused SSH slot. Repeat with a different account mutation and confirm the administrator remains connected.
|
While an administrative command is queued or running, use UART0 to change that admin's role/password/key or delete it. Confirm no second remote administrative command runs after the mutation, the SSH session is revoked promptly, and queued output is not delivered to a reused SSH slot. Repeat with a different account mutation and confirm the administrator remains connected.
|
||||||
|
|
||||||
|
|||||||
+434
-45
@@ -7,12 +7,16 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "console_completion.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
|
#include "esp_system.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "linenoise/linenoise.h"
|
#include "linenoise/linenoise.h"
|
||||||
#include "secure_random.h"
|
#include "secure_random.h"
|
||||||
|
#include "ssh_transport.h"
|
||||||
#include "user_database.h"
|
#include "user_database.h"
|
||||||
|
|
||||||
#define ADMIN_SSH_CONSOLE_MAX_SESSIONS 2U
|
#define ADMIN_SSH_CONSOLE_MAX_SESSIONS 2U
|
||||||
@@ -24,15 +28,41 @@
|
|||||||
#define ADMIN_UART_CONSOLE_TASK_STACK_SIZE 6144U
|
#define ADMIN_UART_CONSOLE_TASK_STACK_SIZE 6144U
|
||||||
#define ADMIN_UART_CONSOLE_TASK_PRIORITY 3U
|
#define ADMIN_UART_CONSOLE_TASK_PRIORITY 3U
|
||||||
#define ADMIN_SSH_CONSOLE_MAX_ARGUMENTS 10U
|
#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 {
|
typedef struct {
|
||||||
bool active;
|
bool active;
|
||||||
bool command_pending;
|
bool command_pending;
|
||||||
bool executing;
|
bool executing;
|
||||||
|
bool deferred_action_pending;
|
||||||
admin_ssh_console_token_t token;
|
admin_ssh_console_token_t token;
|
||||||
user_principal_t principal;
|
user_principal_t principal;
|
||||||
size_t input_length;
|
size_t input_length;
|
||||||
uint8_t input[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
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_start;
|
||||||
size_t output_length;
|
size_t output_length;
|
||||||
uint8_t output[ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY];
|
uint8_t output[ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY];
|
||||||
@@ -51,6 +81,12 @@ typedef struct {
|
|||||||
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||||
} admin_request_t;
|
} 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 portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||||
static admin_session_t s_sessions[ADMIN_SSH_CONSOLE_MAX_SESSIONS];
|
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 *
|
static uint8_t s_request_queue_bytes[ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH *
|
||||||
sizeof(admin_request_t)];
|
sizeof(admin_request_t)];
|
||||||
static QueueHandle_t s_request_queue;
|
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_task;
|
||||||
static TaskHandle_t s_uart_task;
|
static TaskHandle_t s_uart_task;
|
||||||
|
static TaskHandle_t s_control_task;
|
||||||
static bool s_initialized;
|
static bool s_initialized;
|
||||||
static bool s_dispatch_ready;
|
static bool s_dispatch_ready;
|
||||||
/* Accessed only by the single dispatcher task while a callback is running. */
|
/* Accessed only by the single dispatcher task while a callback is running. */
|
||||||
static bool s_dispatch_remote;
|
static bool s_dispatch_remote;
|
||||||
static bool s_dispatch_output_previous_cr;
|
static bool s_dispatch_output_previous_cr;
|
||||||
|
static admin_ssh_console_token_t s_dispatch_token;
|
||||||
static user_principal_t s_dispatch_principal;
|
static user_principal_t s_dispatch_principal;
|
||||||
|
static ssh_transport_snapshot_t s_control_ssh_snapshot;
|
||||||
|
|
||||||
bool admin_ssh_console_dispatch_is_remote(void)
|
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> ");
|
(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)
|
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(). */
|
/* 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);
|
size_t argc = esp_console_split_argv(copy, argv, ADMIN_SSH_CONSOLE_MAX_ARGUMENTS);
|
||||||
bool allowed = argc > 0U;
|
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;
|
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));
|
secure_wipe(copy, sizeof(copy));
|
||||||
return allowed;
|
return allowed;
|
||||||
@@ -262,6 +426,7 @@ static void dispatch_registered_command(admin_request_t *request)
|
|||||||
stderr = remote_stream;
|
stderr = remote_stream;
|
||||||
s_dispatch_output_previous_cr = false;
|
s_dispatch_output_previous_cr = false;
|
||||||
s_dispatch_remote = true;
|
s_dispatch_remote = true;
|
||||||
|
s_dispatch_token = request->token;
|
||||||
s_dispatch_principal = request->principal;
|
s_dispatch_principal = request->principal;
|
||||||
} else {
|
} else {
|
||||||
s_dispatch_remote = false;
|
s_dispatch_remote = false;
|
||||||
@@ -275,6 +440,7 @@ static void dispatch_registered_command(admin_request_t *request)
|
|||||||
|
|
||||||
s_dispatch_remote = false;
|
s_dispatch_remote = false;
|
||||||
s_dispatch_output_previous_cr = 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));
|
secure_wipe(&s_dispatch_principal, sizeof(s_dispatch_principal));
|
||||||
if (remote_stream != NULL) {
|
if (remote_stream != NULL) {
|
||||||
stdout = saved_stdout;
|
stdout = saved_stdout;
|
||||||
@@ -329,7 +495,8 @@ static void worker_task(void *context)
|
|||||||
session->executing = false;
|
session->executing = false;
|
||||||
session->command_pending = false;
|
session->command_pending = false;
|
||||||
prompt = auth_error == ESP_OK && current &&
|
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 &&
|
} else if (!session->active && session->executing &&
|
||||||
token_identity_matches(session, &request.token)) {
|
token_identity_matches(session, &request.token)) {
|
||||||
/* A disconnect invalidated this executing request; erase buffered secrets. */
|
/* 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)
|
static void uart_frontend_task(void *context)
|
||||||
{
|
{
|
||||||
(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,
|
s_request_queue = xQueueCreateStatic(ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH,
|
||||||
sizeof(admin_request_t), s_request_queue_bytes,
|
sizeof(admin_request_t), s_request_queue_bytes,
|
||||||
&s_request_queue_storage);
|
&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;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
if (xTaskCreate(worker_task, "admin_ssh_console", ADMIN_SSH_CONSOLE_TASK_STACK_SIZE,
|
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;
|
s_task = NULL;
|
||||||
return ESP_ERR_NO_MEM;
|
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);
|
taskENTER_CRITICAL(&s_lock);
|
||||||
s_initialized = true;
|
s_initialized = true;
|
||||||
taskEXIT_CRITICAL(&s_lock);
|
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));
|
secure_wipe(session, sizeof(*session));
|
||||||
session->active = true;
|
session->active = true;
|
||||||
|
session->history_position = -1;
|
||||||
session->token = *token;
|
session->token = *token;
|
||||||
session->principal = *principal;
|
session->principal = *principal;
|
||||||
static const char banner[] =
|
static const char banner[] =
|
||||||
@@ -470,13 +743,21 @@ void admin_ssh_console_close(const admin_ssh_console_token_t *token)
|
|||||||
taskENTER_CRITICAL(&s_lock);
|
taskENTER_CRITICAL(&s_lock);
|
||||||
admin_session_t *session = &s_sessions[token->slot_index];
|
admin_session_t *session = &s_sessions[token->slot_index];
|
||||||
bool matched = token_matches(session, token);
|
bool matched = token_matches(session, token);
|
||||||
|
bool wake_prompt = false;
|
||||||
if (matched) {
|
if (matched) {
|
||||||
|
if (session->prompt_state == ADMIN_PROMPT_WAITING) {
|
||||||
|
session->prompt_state = ADMIN_PROMPT_DISCONNECTED;
|
||||||
|
wake_prompt = true;
|
||||||
|
}
|
||||||
session->active = false;
|
session->active = false;
|
||||||
if (!session->executing) {
|
if (!session->executing) {
|
||||||
secure_wipe(session, sizeof(*session));
|
secure_wipe(session, sizeof(*session));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
taskEXIT_CRITICAL(&s_lock);
|
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)
|
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);
|
taskENTER_CRITICAL(&s_lock);
|
||||||
admin_session_t *session = &s_sessions[token->slot_index];
|
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 -
|
session->output_length <= ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY -
|
||||||
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE;
|
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE;
|
||||||
taskEXIT_CRITICAL(&s_lock);
|
taskEXIT_CRITICAL(&s_lock);
|
||||||
@@ -506,40 +790,143 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
|||||||
bool submit = false;
|
bool submit = false;
|
||||||
taskENTER_CRITICAL(&s_lock);
|
taskENTER_CRITICAL(&s_lock);
|
||||||
admin_session_t *session = &s_sessions[token->slot_index];
|
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);
|
taskEXIT_CRITICAL(&s_lock);
|
||||||
return *consumed != 0U;
|
return *consumed != 0U;
|
||||||
}
|
}
|
||||||
uint8_t value = data[index];
|
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 -
|
if (session->output_length >= ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY -
|
||||||
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE) {
|
ADMIN_SSH_CONSOLE_RESPONSE_RESERVE) {
|
||||||
taskEXIT_CRITICAL(&s_lock);
|
taskEXIT_CRITICAL(&s_lock);
|
||||||
return *consumed != 0U;
|
return *consumed != 0U;
|
||||||
}
|
}
|
||||||
if (value == '\r' || value == '\n') {
|
if (session->discard_next_lf && value == '\n') {
|
||||||
if (value == '\n' && session->input_length == 0U) {
|
session->discard_next_lf = false;
|
||||||
++*consumed;
|
++*consumed;
|
||||||
taskEXIT_CRITICAL(&s_lock);
|
taskEXIT_CRITICAL(&s_lock);
|
||||||
continue;
|
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);
|
memcpy(request.line, session->input, session->input_length);
|
||||||
request.origin = ADMIN_REQUEST_SSH;
|
request.origin = ADMIN_REQUEST_SSH;
|
||||||
request.token = *token;
|
request.token = *token;
|
||||||
request.principal = session->principal;
|
request.principal = session->principal;
|
||||||
|
secure_wipe(session->input, sizeof(session->input));
|
||||||
session->input_length = 0U;
|
session->input_length = 0U;
|
||||||
|
session->history_position = -1;
|
||||||
session->command_pending = true;
|
session->command_pending = true;
|
||||||
(void)append_output_locked(session, (const uint8_t *)"\r\n",
|
(void)append_output_locked(session, (const uint8_t *)"\r\n",
|
||||||
sizeof("\r\n") - 1U);
|
sizeof("\r\n") - 1U);
|
||||||
submit = true;
|
submit = true;
|
||||||
} else if (value == 0x03U) {
|
} else if (value == 0x03U) {
|
||||||
|
secure_wipe(session->input, sizeof(session->input));
|
||||||
session->input_length = 0U;
|
session->input_length = 0U;
|
||||||
|
session->history_position = -1;
|
||||||
(void)append_output_locked(session, (const uint8_t *)"^C\r\n",
|
(void)append_output_locked(session, (const uint8_t *)"^C\r\n",
|
||||||
sizeof("^C\r\n") - 1U);
|
sizeof("^C\r\n") - 1U);
|
||||||
(void)append_output_locked(session, (const uint8_t *)"admin@serial-tool> ",
|
(void)append_output_locked(session, (const uint8_t *)"admin@serial-tool> ",
|
||||||
sizeof("admin@serial-tool> ") - 1U);
|
sizeof("admin@serial-tool> ") - 1U);
|
||||||
} else if (value == 0x08U || value == 0x7fU) {
|
} else if (value == 0x08U || value == 0x7fU) {
|
||||||
if (session->input_length > 0U) {
|
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",
|
(void)append_output_locked(session, (const uint8_t *)"\b \b",
|
||||||
sizeof("\b \b") - 1U);
|
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> ",
|
"\r\nCommand too long; discarded.\r\nadmin@serial-tool> ",
|
||||||
sizeof("\r\nCommand too long; discarded.\r\nadmin@serial-tool> ") - 1U);
|
sizeof("\r\nCommand too long; discarded.\r\nadmin@serial-tool> ") - 1U);
|
||||||
} else {
|
} else {
|
||||||
|
session->history_position = -1;
|
||||||
session->input[session->input_length++] = value;
|
session->input[session->input_length++] = value;
|
||||||
|
session->input[session->input_length] = 0U;
|
||||||
(void)append_output_locked(session, &value, 1U);
|
(void)append_output_locked(session, &value, 1U);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ typedef struct {
|
|||||||
uint32_t slot_generation;
|
uint32_t slot_generation;
|
||||||
} admin_ssh_console_token_t;
|
} 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 {
|
typedef struct {
|
||||||
bool active;
|
bool active;
|
||||||
bool command_pending;
|
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. */
|
/* Valid only while a registered command callback runs on the dispatcher task. */
|
||||||
bool admin_ssh_console_dispatch_is_remote(void);
|
bool admin_ssh_console_dispatch_is_remote(void);
|
||||||
const user_principal_t *admin_ssh_console_dispatch_principal(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. */
|
/* 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,
|
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "linenoise/linenoise.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. */
|
/* Keep full-line candidate strings grouped by their registered root command. */
|
||||||
static const char *const s_completion_candidates[] = {
|
static const char *const s_completion_candidates[] = {
|
||||||
/* Hardware debug commands and safe fixed arguments. */
|
/* Hardware debug commands and safe fixed arguments. */
|
||||||
@@ -197,6 +202,49 @@ static const char *const s_completion_candidates[] = {
|
|||||||
"ssh reset --force",
|
"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,
|
static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
|
||||||
void *buffer,
|
void *buffer,
|
||||||
size_t size)
|
size_t size)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
@@ -9,6 +12,9 @@ extern "C" {
|
|||||||
/* Install late-terminal upgrade handling and project-specific completion. */
|
/* Install late-terminal upgrade handling and project-specific completion. */
|
||||||
void console_completion_install(void);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#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;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
if (admin_ssh_console_dispatch_is_remote()) {
|
if (admin_ssh_console_dispatch_is_remote()) {
|
||||||
printf("Interactive input is restricted to physical UART0.\n");
|
return admin_ssh_console_dispatch_read_input(
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
prompt, output, capacity, hidden, output_length);
|
||||||
}
|
}
|
||||||
*output_length = 0U;
|
*output_length = 0U;
|
||||||
memset(output, 0, capacity);
|
memset(output, 0, capacity);
|
||||||
|
|||||||
+108
-87
@@ -14,6 +14,7 @@
|
|||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "lwip/inet.h"
|
#include "lwip/inet.h"
|
||||||
#include "lwip/inet_chksum.h"
|
#include "lwip/inet_chksum.h"
|
||||||
@@ -163,109 +164,109 @@ static int resolve_ping_target(const char *host, ip_addr_t *target,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PING_EVENT_LINE = 0,
|
||||||
|
PING_EVENT_END,
|
||||||
|
} ping_event_kind_t;
|
||||||
|
|
||||||
typedef struct {
|
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;
|
esp_err_t delete_error;
|
||||||
bool received_reply;
|
} ping_event_t;
|
||||||
bool summary_valid;
|
|
||||||
|
typedef struct {
|
||||||
|
QueueHandle_t queue;
|
||||||
} ping_wait_context_t;
|
} 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)
|
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;
|
uint16_t sequence = 0U;
|
||||||
uint8_t ttl = 0U;
|
uint8_t ttl = 0U;
|
||||||
uint32_t reply_size = 0U;
|
uint32_t reply_size = 0U;
|
||||||
uint32_t elapsed_ms = 0U;
|
uint32_t elapsed_ms = 0U;
|
||||||
ip_addr_t reply_address;
|
ip_addr_t reply_address;
|
||||||
char numeric[NUMERIC_ADDRESS_CAPACITY];
|
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
|
||||||
|
bool valid = esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
|
||||||
if (esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
|
&sequence, sizeof(sequence)) == ESP_OK &&
|
||||||
&sequence, sizeof(sequence)) != ESP_OK ||
|
esp_ping_get_profile(handle, ESP_PING_PROF_SIZE,
|
||||||
esp_ping_get_profile(handle, ESP_PING_PROF_SIZE,
|
&reply_size, sizeof(reply_size)) == ESP_OK &&
|
||||||
&reply_size, sizeof(reply_size)) != ESP_OK ||
|
esp_ping_get_profile(handle, ESP_PING_PROF_TIMEGAP,
|
||||||
esp_ping_get_profile(handle, ESP_PING_PROF_TIMEGAP,
|
&elapsed_ms, sizeof(elapsed_ms)) == ESP_OK &&
|
||||||
&elapsed_ms, sizeof(elapsed_ms)) != ESP_OK ||
|
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
|
||||||
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
|
&reply_address, sizeof(reply_address)) == ESP_OK &&
|
||||||
&reply_address, sizeof(reply_address)) != ESP_OK ||
|
ipaddr_ntoa_r(&reply_address, numeric, (int)sizeof(numeric)) != NULL;
|
||||||
ipaddr_ntoa_r(&reply_address, numeric, (int)sizeof(numeric)) == NULL) {
|
if (!valid) {
|
||||||
printf("ping: received a reply but could not read its profile\n");
|
strlcpy(event.line, "ping: received a reply but could not read its profile",
|
||||||
return;
|
sizeof(event.line));
|
||||||
}
|
} else if (IP_IS_V4(&reply_address) &&
|
||||||
|
esp_ping_get_profile(handle, ESP_PING_PROF_TTL,
|
||||||
if (IP_IS_V4(&reply_address) &&
|
&ttl, sizeof(ttl)) == ESP_OK) {
|
||||||
esp_ping_get_profile(handle, ESP_PING_PROF_TTL,
|
snprintf(event.line, sizeof(event.line),
|
||||||
&ttl, sizeof(ttl)) == ESP_OK) {
|
"%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
|
||||||
printf("%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
|
" ttl=%u time=%" PRIu32 " ms",
|
||||||
" ttl=%u time=%" PRIu32 " ms\n",
|
reply_size, numeric, sequence, (unsigned int)ttl, elapsed_ms);
|
||||||
reply_size, numeric, sequence, (unsigned int)ttl, elapsed_ms);
|
|
||||||
} else {
|
} else {
|
||||||
printf("%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
|
snprintf(event.line, sizeof(event.line),
|
||||||
" time=%" PRIu32 " ms\n",
|
"%" PRIu32 " bytes from %s: icmp_seq=%" PRIu16
|
||||||
reply_size, numeric, sequence, elapsed_ms);
|
" 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)
|
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;
|
uint16_t sequence = 0U;
|
||||||
ip_addr_t target_address;
|
ip_addr_t target_address;
|
||||||
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
|
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
|
||||||
|
|
||||||
if (esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
|
if (esp_ping_get_profile(handle, ESP_PING_PROF_SEQNO,
|
||||||
&sequence, sizeof(sequence)) == ESP_OK &&
|
&sequence, sizeof(sequence)) == ESP_OK &&
|
||||||
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
|
esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
|
||||||
&target_address, sizeof(target_address)) == ESP_OK) {
|
&target_address, sizeof(target_address)) == ESP_OK) {
|
||||||
(void)ipaddr_ntoa_r(&target_address, numeric, (int)sizeof(numeric));
|
(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)
|
static void ping_on_end(esp_ping_handle_t handle, void *arguments)
|
||||||
{
|
{
|
||||||
ping_wait_context_t *context = (ping_wait_context_t *)arguments;
|
ping_wait_context_t *context = arguments;
|
||||||
uint32_t transmitted = 0U;
|
ping_event_t event = {.kind = PING_EVENT_END, .profile_error = ESP_OK};
|
||||||
uint32_t received = 0U;
|
|
||||||
uint32_t duration_ms = 0U;
|
|
||||||
ip_addr_t target_address;
|
ip_addr_t target_address;
|
||||||
char numeric[NUMERIC_ADDRESS_CAPACITY] = "?";
|
strlcpy(event.address, "?", sizeof(event.address));
|
||||||
|
event.profile_error = esp_ping_get_profile(
|
||||||
esp_err_t profile_error = esp_ping_get_profile(
|
handle, ESP_PING_PROF_REQUEST, &event.transmitted, sizeof(event.transmitted));
|
||||||
handle, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
|
if (event.profile_error == ESP_OK) {
|
||||||
if (profile_error == ESP_OK) {
|
event.profile_error = esp_ping_get_profile(
|
||||||
profile_error = esp_ping_get_profile(
|
handle, ESP_PING_PROF_REPLY, &event.received, sizeof(event.received));
|
||||||
handle, ESP_PING_PROF_REPLY, &received, sizeof(received));
|
|
||||||
}
|
}
|
||||||
if (profile_error == ESP_OK) {
|
if (event.profile_error == ESP_OK) {
|
||||||
profile_error = esp_ping_get_profile(
|
event.profile_error = esp_ping_get_profile(
|
||||||
handle, ESP_PING_PROF_DURATION, &duration_ms, sizeof(duration_ms));
|
handle, ESP_PING_PROF_DURATION, &event.duration_ms, sizeof(event.duration_ms));
|
||||||
}
|
}
|
||||||
if (esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
|
if (esp_ping_get_profile(handle, ESP_PING_PROF_IPADDR,
|
||||||
&target_address, sizeof(target_address)) == ESP_OK) {
|
&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));
|
||||||
}
|
}
|
||||||
|
event.delete_error = esp_ping_delete_session(handle);
|
||||||
if (profile_error == ESP_OK) {
|
(void)xQueueSend(context->queue, &event, 0U);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int execute_ping(int argc, char **argv)
|
static int execute_ping(int argc, char **argv)
|
||||||
@@ -288,12 +289,17 @@ static int execute_ping(int argc, char **argv)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ping_wait_context_t context = {
|
if (s_ping_queue == NULL) {
|
||||||
.waiting_task = xTaskGetCurrentTaskHandle(),
|
s_ping_queue = xQueueCreateStatic(PING_EVENT_QUEUE_LENGTH, sizeof(ping_event_t),
|
||||||
.delete_error = ESP_FAIL,
|
s_ping_queue_bytes, &s_ping_queue_storage);
|
||||||
};
|
} else {
|
||||||
/* Remove any unrelated notification before this command begins waiting. */
|
(void)xQueueReset(s_ping_queue);
|
||||||
(void)ulTaskNotifyTake(pdTRUE, 0U);
|
}
|
||||||
|
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();
|
esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
|
||||||
config.count = count;
|
config.count = count;
|
||||||
@@ -321,21 +327,36 @@ static int execute_ping(int argc, char **argv)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Finite count guarantees on_ping_end; blocking keeps console output ordered. */
|
for (;;) {
|
||||||
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) == 0U) {
|
ping_event_t event;
|
||||||
printf("ping: wait for session completion failed\n");
|
if (xQueueReceive(s_ping_queue, &event, portMAX_DELAY) != pdTRUE) {
|
||||||
(void)esp_ping_stop(session);
|
printf("ping: wait for session completion failed\n");
|
||||||
(void)esp_ping_delete_session(session);
|
return 1;
|
||||||
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,
|
static bool socket_addresses_equal(const struct addrinfo *left,
|
||||||
|
|||||||
+47
-1
@@ -9,6 +9,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "admin_ssh_console.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "mbedtls/base64.h"
|
#include "mbedtls/base64.h"
|
||||||
#include "secure_random.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)
|
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};
|
ssh_security_metadata_t before = {0};
|
||||||
bool had_before = ssh_security_get_metadata(&before) == ESP_OK;
|
bool had_before = ssh_security_get_metadata(&before) == ESP_OK;
|
||||||
esp_err_t error = ssh_transport_replace_host_key(reset);
|
esp_err_t error = ssh_transport_replace_host_key(reset);
|
||||||
@@ -256,6 +269,16 @@ static int command_ssh(int argc, char **argv)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (argc == 2 && strcmp(argv[1], "stop") == 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();
|
esp_err_t error = ssh_transport_stop();
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
printf("Could not stop SSH: %s\n", esp_err_to_name(error));
|
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");
|
printf("Session ID must be a nonzero decimal integer.\n");
|
||||||
return 1;
|
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) {
|
if (error != ESP_OK) {
|
||||||
printf("Could not disconnect SSH session: %s\n", esp_err_to_name(error));
|
printf("Could not disconnect SSH session: %s\n", esp_err_to_name(error));
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "admin_ssh_console.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
@@ -47,6 +48,15 @@ static int command_reboot(int argc, char **argv)
|
|||||||
return 1;
|
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");
|
printf("Rebooting now; unsaved RAM-only configuration changes will be lost.\n");
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
/* Give the UART driver time to transmit the acknowledgement before reset. */
|
/* 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();
|
print_usage();
|
||||||
return 1;
|
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);
|
return add_user(argv[2], argv[3], generated);
|
||||||
}
|
}
|
||||||
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
|
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
|
||||||
@@ -464,10 +460,6 @@ static int command_user_inner(int argc, char **argv)
|
|||||||
print_usage();
|
print_usage();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (remote && !generated) {
|
|
||||||
printf("Interactive password entry is restricted to physical UART0; use --generate.\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (remote && generated && principal != NULL &&
|
if (remote && generated && principal != NULL &&
|
||||||
strlen(argv[2]) == principal->username_length &&
|
strlen(argv[2]) == principal->username_length &&
|
||||||
memcmp(argv[2], principal->username, principal->username_length) == 0) {
|
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 &&
|
if (argc == 4 && strcmp(argv[1], "key") == 0 &&
|
||||||
strcmp(argv[2], "add") == 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]);
|
return add_key(argv[3]);
|
||||||
}
|
}
|
||||||
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
|
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
|
||||||
|
|||||||
+12
-68
@@ -9,19 +9,16 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "admin_ssh_console.h"
|
#include "console_input.h"
|
||||||
#include "driver/uart.h"
|
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_netif_ip_addr.h"
|
#include "esp_netif_ip_addr.h"
|
||||||
#include "esp_wifi_types.h"
|
#include "esp_wifi_types.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/task.h"
|
|
||||||
#include "network_console.h"
|
#include "network_console.h"
|
||||||
#include "wifi_config.h"
|
#include "wifi_config.h"
|
||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
|
|
||||||
#define WIFI_CONSOLE_UART UART_NUM_0
|
|
||||||
#define WIFI_CONSOLE_SECRET_CAPACITY WIFI_CONFIG_PSK_MAX_LEN
|
#define WIFI_CONSOLE_SECRET_CAPACITY WIFI_CONFIG_PSK_MAX_LEN
|
||||||
|
|
||||||
static void print_usage(void)
|
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)
|
static esp_err_t read_secret_no_echo(uint8_t *secret, uint8_t *secret_len)
|
||||||
{
|
{
|
||||||
if (admin_ssh_console_dispatch_is_remote()) {
|
uint8_t buffer[WIFI_CONSOLE_SECRET_CAPACITY + 1U] = {0};
|
||||||
printf("Interactive secret entry is restricted to physical UART0.\n");
|
|
||||||
return ESP_ERR_NOT_SUPPORTED;
|
|
||||||
}
|
|
||||||
uint8_t buffer[WIFI_CONSOLE_SECRET_CAPACITY];
|
|
||||||
size_t length = 0U;
|
size_t length = 0U;
|
||||||
memset(buffer, 0, sizeof(buffer));
|
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,
|
||||||
* esp_console may execute on CR while the terminal's trailing LF is still
|
WIFI_CONFIG_PSK_MAX_LEN, &length);
|
||||||
* arriving. Let that line ending settle, then discard only pre-prompt RX so
|
if (error == ESP_OK) {
|
||||||
* it cannot be mistaken for an immediately submitted empty secret.
|
memset(secret, 0, WIFI_CONFIG_PSK_MAX_LEN);
|
||||||
*/
|
memcpy(secret, buffer, length);
|
||||||
vTaskDelay(1U);
|
*secret_len = (uint8_t)length;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
wifi_config_secure_wipe(buffer, sizeof(buffer));
|
||||||
return ESP_OK;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int set_profile(char **argv)
|
static int set_profile(char **argv)
|
||||||
|
|||||||
Reference in New Issue
Block a user