Phase 8D - massive refactor and admin functions / admin shell in

webinterface. Memory and cross-origin problems.
This commit is contained in:
2026-09-03 16:44:52 +02:00
parent 31d3561d47
commit 9e17df0ac5
36 changed files with 10020 additions and 639 deletions
+3
View File
@@ -28,9 +28,12 @@ idf_component_register(
"usb_cdc_transport.c"
"usb_console.c"
"user_database.c"
"user_admin_service.c"
"user_console.c"
"web_security.c"
"web_session.c"
"web_serial_transport.c"
"web_admin_transport.c"
"web_assets_data.c"
"web_ui.c"
"web_server.c"
+161 -60
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Serialized, bounded administrative SSH command worker. */
/* Serialized, bounded remote administrative command worker. */
#include "admin_ssh_console.h"
@@ -18,8 +18,8 @@
#include "secure_random.h"
#include "ssh_transport.h"
#include "user_database.h"
#include "web_server.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
@@ -29,7 +29,8 @@
#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
/* One deferred request per fixed remote console slot can be pending. */
#define ADMIN_SSH_CONTROL_QUEUE_LENGTH ADMIN_SSH_CONSOLE_SLOT_COUNT
#define ADMIN_SSH_CONTROL_TASK_STACK_SIZE 4096U
#define ADMIN_SSH_CONTROL_TASK_PRIORITY 3U
@@ -48,6 +49,7 @@ typedef struct {
bool deferred_action_pending;
admin_ssh_console_token_t token;
user_principal_t principal;
admin_ssh_console_frontend_ops_t frontend_ops;
size_t input_length;
size_t input_cursor;
uint8_t input[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
@@ -61,6 +63,7 @@ typedef struct {
uint8_t escape_parameters[4];
size_t escape_parameter_length;
bool discard_next_lf;
bool discard_overlong_line;
admin_prompt_state_t prompt_state;
bool prompt_hidden;
size_t prompt_capacity;
@@ -72,7 +75,7 @@ typedef struct {
} admin_session_t;
typedef enum {
ADMIN_REQUEST_SSH = 0,
ADMIN_REQUEST_REMOTE = 0,
ADMIN_REQUEST_UART0,
} admin_request_origin_t;
@@ -80,6 +83,7 @@ typedef struct {
admin_request_origin_t origin;
admin_ssh_console_token_t token;
user_principal_t principal;
admin_ssh_console_frontend_ops_t frontend_ops;
TaskHandle_t completion_task;
uint8_t line[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
} admin_request_t;
@@ -87,12 +91,12 @@ typedef struct {
typedef struct {
admin_ssh_deferred_action_type_t action;
admin_ssh_console_token_t token;
admin_ssh_console_frontend_ops_t frontend_ops;
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 admin_session_t s_sessions[ADMIN_SSH_CONSOLE_SLOT_COUNT];
static char s_completion_output[CONSOLE_COMPLETION_OUTPUT_CAPACITY];
static StaticQueue_t s_request_queue_storage;
@@ -105,6 +109,8 @@ static uint8_t s_control_queue_bytes[ADMIN_SSH_CONTROL_QUEUE_LENGTH *
static QueueHandle_t s_control_queue;
static StaticSemaphore_t s_prompt_done_storage;
static SemaphoreHandle_t s_prompt_done;
static StaticSemaphore_t s_feed_mutex_storage;
static SemaphoreHandle_t s_feed_mutex;
static TaskHandle_t s_task;
static TaskHandle_t s_uart_task;
static TaskHandle_t s_control_task;
@@ -115,13 +121,27 @@ 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_is_ready(void)
{
taskENTER_CRITICAL(&s_lock);
bool ready = s_initialized && s_dispatch_ready;
taskEXIT_CRITICAL(&s_lock);
return ready;
}
bool admin_ssh_console_dispatch_is_remote(void)
{
return xTaskGetCurrentTaskHandle() == s_task && s_dispatch_remote;
}
admin_ssh_console_frontend_t admin_ssh_console_dispatch_frontend(void)
{
return admin_ssh_console_dispatch_is_remote()
? s_dispatch_token.frontend
: ADMIN_SSH_CONSOLE_FRONTEND_NONE;
}
const user_principal_t *admin_ssh_console_dispatch_principal(void)
{
return admin_ssh_console_dispatch_is_remote() ? &s_dispatch_principal : NULL;
@@ -129,14 +149,29 @@ const user_principal_t *admin_ssh_console_dispatch_principal(void)
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;
if (token == NULL || token->session_id == 0U || token->slot_generation == 0U) {
return false;
}
if (token->frontend == ADMIN_SSH_CONSOLE_FRONTEND_SSH) {
return token->slot_index < ADMIN_SSH_CONSOLE_SSH_SLOT_COUNT;
}
return token->frontend == ADMIN_SSH_CONSOLE_FRONTEND_WEB &&
token->slot_index == ADMIN_SSH_CONSOLE_WEB_SLOT_INDEX;
}
static bool frontend_ops_valid(const admin_ssh_console_frontend_ops_t *frontend_ops)
{
return frontend_ops != NULL && frontend_ops->binding_is_current != NULL &&
frontend_ops->transport_output_is_drained != NULL &&
frontend_ops->request_disconnect != NULL;
}
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 &&
return token_valid(token) && session->token.frontend == token->frontend &&
session->token.slot_index == token->slot_index &&
session->token.session_id == token->session_id &&
session->token.slot_generation == token->slot_generation;
}
@@ -357,12 +392,14 @@ esp_err_t admin_ssh_console_dispatch_defer(
if (!admin_ssh_console_dispatch_is_remote() || action == ADMIN_SSH_DEFER_NONE) {
return ESP_ERR_INVALID_STATE;
}
admin_ssh_console_frontend_ops_t frontend_ops = {0};
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;
frontend_ops = session->frontend_ops;
}
taskEXIT_CRITICAL(&s_lock);
if (!valid) {
@@ -371,6 +408,7 @@ esp_err_t admin_ssh_console_dispatch_defer(
admin_control_request_t request = {
.action = action,
.token = s_dispatch_token,
.frontend_ops = frontend_ops,
.argument = argument,
};
if (xQueueSend(s_control_queue, &request, 0U) == pdTRUE) {
@@ -385,7 +423,7 @@ esp_err_t admin_ssh_console_dispatch_defer(
return ESP_ERR_TIMEOUT;
}
static int ssh_output_write(void *cookie, const char *buffer, int length)
static int remote_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) {
@@ -468,17 +506,18 @@ static int command_exit(int argc, char **argv)
return 1;
}
if (!admin_ssh_console_dispatch_is_remote()) {
printf("The exit command is available only from an administrative SSH session.\n");
printf("The exit command is available only from a remote administrative session.\n");
return 1;
}
esp_err_t error = admin_ssh_console_dispatch_defer(
ADMIN_SSH_DEFER_DISCONNECT, s_dispatch_token.session_id);
ADMIN_SSH_DEFER_FRONTEND_DISCONNECT, s_dispatch_token.session_id);
if (error != ESP_OK) {
printf("Could not schedule SSH session close: %s\n", esp_err_to_name(error));
printf("Could not schedule administrative session close: %s\n",
esp_err_to_name(error));
return 1;
}
printf("SSH session close scheduled after output drains.\n");
printf("Administrative session close scheduled after output drains.\n");
return 0;
}
@@ -487,8 +526,8 @@ 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 (request->origin == ADMIN_REQUEST_REMOTE) {
remote_stream = funopen(&request->token, NULL, remote_output_write, NULL, NULL);
if (remote_stream == NULL) {
(void)worker_write(&request->token, "Could not create command output stream.\r\n");
return;
@@ -538,8 +577,16 @@ static void worker_task(void *context)
continue;
}
bool current = false;
esp_err_t auth_error = user_database_principal_is_current(&request.principal, &current);
bool principal_current = false;
esp_err_t auth_error = user_database_principal_is_current(
&request.principal, &principal_current);
bool command_allowed = request.principal.role == USER_ROLE_ADMIN &&
remote_command_allowed(&request);
bool binding_current = frontend_ops_valid(&request.frontend_ops) &&
request.frontend_ops.binding_is_current(&request.token);
bool authorized = auth_error == ESP_OK && principal_current &&
command_allowed && binding_current;
bool active;
taskENTER_CRITICAL(&s_lock);
admin_session_t *session = &s_sessions[request.token.slot_index];
@@ -549,16 +596,18 @@ static void worker_task(void *context)
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) {
if (authorized && active) {
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");
const char *message = "Command is restricted to physical UART0.\r\n";
if (auth_error != ESP_OK || !principal_current ||
request.principal.role != USER_ROLE_ADMIN) {
message = "Administrative authorization is no longer current; closing session.\r\n";
} else if (!binding_current) {
message = "Administrative session binding is no longer current; closing session.\r\n";
}
(void)worker_write(&request.token, message);
}
bool prompt = false;
taskENTER_CRITICAL(&s_lock);
@@ -566,7 +615,7 @@ static void worker_task(void *context)
if (token_matches(session, &request.token)) {
session->executing = false;
session->command_pending = false;
prompt = auth_error == ESP_OK && current &&
prompt = auth_error == ESP_OK && principal_current && binding_current &&
request.principal.role == USER_ROLE_ADMIN &&
!session->deferred_action_pending;
} else if (!session->active && session->executing &&
@@ -588,13 +637,14 @@ static void finish_deferred_request(const admin_control_request_t *request,
char message[160];
if (cancelled) {
snprintf(message, sizeof(message),
"Deferred action cancelled before SSH output drained.\r\nadmin@serial-tool> ");
"Deferred action cancelled before administrative output drained.\r\n"
"admin@serial-tool> ");
} else if (result == ESP_OK) {
snprintf(message, sizeof(message),
"Deferred SSH action completed.\r\nadmin@serial-tool> ");
"Deferred administrative action completed.\r\nadmin@serial-tool> ");
} else {
snprintf(message, sizeof(message),
"Deferred SSH action failed: %s\r\nadmin@serial-tool> ",
"Deferred administrative action failed: %s\r\nadmin@serial-tool> ",
esp_err_to_name(result));
}
taskENTER_CRITICAL(&s_lock);
@@ -615,6 +665,9 @@ static void control_task(void *context)
continue;
}
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(10000U);
bool must_execute =
request.action == ADMIN_SSH_DEFER_WEB_TLS_REFRESH_STOPPED ||
request.action == ADMIN_SSH_DEFER_WEB_TLS_REFRESH_RUNNING;
bool drained = false;
while ((int32_t)(xTaskGetTickCount() - deadline) < 0) {
taskENTER_CRITICAL(&s_lock);
@@ -624,27 +677,18 @@ static void control_task(void *context)
session->output_length == 0U;
taskEXIT_CRITICAL(&s_lock);
if (!current) {
drained = must_execute;
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;
}
}
}
bool transport_drained = console_drained &&
request.frontend_ops.transport_output_is_drained(&request.token);
if (console_drained && transport_drained) {
drained = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(10U));
}
if (!drained) {
if (!drained && !must_execute) {
finish_deferred_request(&request, ESP_ERR_TIMEOUT, true);
secure_wipe(&request, sizeof(request));
continue;
@@ -658,7 +702,11 @@ static void control_task(void *context)
case ADMIN_SSH_DEFER_STOP:
result = ssh_transport_stop();
break;
case ADMIN_SSH_DEFER_DISCONNECT:
case ADMIN_SSH_DEFER_FRONTEND_DISCONNECT:
result = request.frontend_ops.request_disconnect(
&request.token, request.argument);
break;
case ADMIN_SSH_DEFER_SSH_DISCONNECT:
result = ssh_transport_disconnect(request.argument);
break;
case ADMIN_SSH_DEFER_HOST_KEY_ROTATE:
@@ -667,6 +715,15 @@ static void control_task(void *context)
case ADMIN_SSH_DEFER_HOST_KEY_RESET:
result = ssh_transport_replace_host_key(true);
break;
case ADMIN_SSH_DEFER_WEB_STOP:
result = web_server_stop_if_generation(request.argument);
break;
case ADMIN_SSH_DEFER_WEB_TLS_REFRESH_STOPPED:
result = web_server_refresh_tls(request.argument, false);
break;
case ADMIN_SSH_DEFER_WEB_TLS_REFRESH_RUNNING:
result = web_server_refresh_tls(request.argument, true);
break;
default:
result = ESP_ERR_NOT_SUPPORTED;
break;
@@ -725,7 +782,9 @@ esp_err_t admin_ssh_console_init(void)
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) {
s_feed_mutex = xSemaphoreCreateMutexStatic(&s_feed_mutex_storage);
if (s_request_queue == NULL || s_control_queue == NULL || s_prompt_done == NULL ||
s_feed_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
if (xTaskCreate(worker_task, "admin_ssh_console", ADMIN_SSH_CONSOLE_TASK_STACK_SIZE,
@@ -750,7 +809,7 @@ esp_err_t admin_ssh_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "exit",
.help = "Close the current administrative SSH session",
.help = "Close the current remote administrative session",
.hint = NULL,
.func = &command_exit,
.argtable = NULL,
@@ -782,16 +841,16 @@ esp_err_t admin_ssh_console_start_uart_frontend(void)
return ESP_OK;
}
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
const user_principal_t *principal)
esp_err_t admin_ssh_console_open(
const admin_ssh_console_token_t *token,
const user_principal_t *principal,
const admin_ssh_console_frontend_ops_t *frontend_ops)
{
if (!token_valid(token) || principal == NULL || principal->role != USER_ROLE_ADMIN) {
if (!token_valid(token) || principal == NULL || principal->role != USER_ROLE_ADMIN ||
!frontend_ops_valid(frontend_ops)) {
return ESP_ERR_INVALID_ARG;
}
taskENTER_CRITICAL(&s_lock);
bool ready = s_initialized && s_dispatch_ready;
taskEXIT_CRITICAL(&s_lock);
if (!ready) {
if (!admin_ssh_console_is_ready()) {
return ESP_ERR_INVALID_STATE;
}
bool current = false;
@@ -809,8 +868,9 @@ esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
session->history_position = -1;
session->token = *token;
session->principal = *principal;
session->frontend_ops = *frontend_ops;
static const char banner[] =
"ESP32 Serial Swiss Army Knife administrative SSH shell\r\n";
"ESP32 Serial Swiss Army Knife administrative 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);
@@ -861,7 +921,7 @@ bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token)
return accepts;
}
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
static bool feed_input_serialized(const admin_ssh_console_token_t *token,
const uint8_t *data, size_t length,
size_t *consumed)
{
@@ -938,6 +998,23 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
continue;
}
session->discard_next_lf = false;
if (session->discard_overlong_line) {
if (value == '\r' || value == '\n' || value == 0x03U) {
session->discard_overlong_line = false;
session->discard_next_lf = value == '\r';
(void)append_output_locked(
session,
(const uint8_t *)(value == 0x03U
? "^C\r\nadmin@serial-tool> "
: "admin@serial-tool> "),
value == 0x03U
? sizeof("^C\r\nadmin@serial-tool> ") - 1U
: sizeof("admin@serial-tool> ") - 1U);
}
++*consumed;
taskEXIT_CRITICAL(&s_lock);
continue;
}
if (session->escape_state != 0U) {
if (session->escape_state == 1U) {
if (value == '[') {
@@ -1046,9 +1123,10 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
history_commit_locked(session);
memcpy(request.line, session->input, session->input_length);
}
request.origin = ADMIN_REQUEST_SSH;
request.origin = ADMIN_REQUEST_REMOTE;
request.token = *token;
request.principal = session->principal;
request.frontend_ops = session->frontend_ops;
secure_wipe(session->input, sizeof(session->input));
session->input_length = 0U;
session->input_cursor = 0U;
@@ -1080,11 +1158,17 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
}
} else if (value >= 0x20U && value <= 0x7eU) {
if (session->input_length >= ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY) {
secure_wipe(session->input, sizeof(session->input));
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);
session->history_position = -1;
session->escape_state = 0U;
session->discard_overlong_line = true;
(void)append_output_locked(
session,
(const uint8_t *)
"\r\nCommand too long; discarding until end of line.\r\n",
sizeof("\r\nCommand too long; discarding until end of line.\r\n") - 1U);
} else {
memmove(session->input + session->input_cursor + 1U,
session->input + session->input_cursor,
@@ -1117,6 +1201,23 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
return true;
}
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) {
return false;
}
*consumed = 0U;
if (!token_valid(token) || (data == NULL && length != 0U) || s_feed_mutex == NULL ||
xSemaphoreTake(s_feed_mutex, 0U) != pdTRUE) {
return false;
}
bool accepted = feed_input_serialized(token, data, length, consumed);
xSemaphoreGive(s_feed_mutex);
return accepted;
}
esp_err_t admin_ssh_console_read_output(const admin_ssh_console_token_t *token,
uint8_t *data, size_t capacity,
size_t *received)
+35 -6
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded, transport-neutral administrative command worker for SSH sessions. */
/* Bounded, transport-neutral administrative command worker for remote sessions. */
#pragma once
@@ -17,19 +17,42 @@ extern "C" {
/* Fits the longest supported ECDSA P-256 OpenSSH key import command. */
#define ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY 256U
/* SSH retains global slots 0 and 1; the web administration frontend owns slot 2. */
#define ADMIN_SSH_CONSOLE_SSH_SLOT_COUNT 2U
#define ADMIN_SSH_CONSOLE_WEB_SLOT_INDEX ADMIN_SSH_CONSOLE_SSH_SLOT_COUNT
#define ADMIN_SSH_CONSOLE_SLOT_COUNT (ADMIN_SSH_CONSOLE_WEB_SLOT_INDEX + 1U)
typedef enum {
ADMIN_SSH_CONSOLE_FRONTEND_NONE = 0,
ADMIN_SSH_CONSOLE_FRONTEND_SSH,
ADMIN_SSH_CONSOLE_FRONTEND_WEB,
} admin_ssh_console_frontend_t;
typedef struct {
admin_ssh_console_frontend_t frontend;
uint8_t slot_index;
uint32_t session_id;
uint32_t slot_generation;
} admin_ssh_console_token_t;
typedef struct {
bool (*binding_is_current)(const admin_ssh_console_token_t *token);
bool (*transport_output_is_drained)(const admin_ssh_console_token_t *token);
esp_err_t (*request_disconnect)(const admin_ssh_console_token_t *token,
uint32_t argument);
} admin_ssh_console_frontend_ops_t;
typedef enum {
ADMIN_SSH_DEFER_NONE = 0,
ADMIN_SSH_DEFER_REBOOT,
ADMIN_SSH_DEFER_STOP,
ADMIN_SSH_DEFER_DISCONNECT,
ADMIN_SSH_DEFER_FRONTEND_DISCONNECT,
ADMIN_SSH_DEFER_SSH_DISCONNECT,
ADMIN_SSH_DEFER_HOST_KEY_ROTATE,
ADMIN_SSH_DEFER_HOST_KEY_RESET,
ADMIN_SSH_DEFER_WEB_STOP,
ADMIN_SSH_DEFER_WEB_TLS_REFRESH_STOPPED,
ADMIN_SSH_DEFER_WEB_TLS_REFRESH_RUNNING,
} admin_ssh_deferred_action_type_t;
typedef struct {
@@ -47,9 +70,11 @@ esp_err_t admin_ssh_console_init(void);
esp_err_t admin_ssh_console_register_commands(void);
/* Called after all ESP-IDF commands are registered; starts the UART0 frontend. */
esp_err_t admin_ssh_console_start_uart_frontend(void);
bool admin_ssh_console_is_ready(void);
/* Valid only while a registered command callback runs on the dispatcher task. */
bool admin_ssh_console_dispatch_is_remote(void);
admin_ssh_console_frontend_t admin_ssh_console_dispatch_frontend(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,
@@ -57,18 +82,22 @@ esp_err_t admin_ssh_console_dispatch_read_input(
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, principal, and ops table are copied. Callback code and any state it
* references must have static lifetime; the console lock is not held during callbacks.
*/
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
const user_principal_t *principal);
const user_principal_t *principal,
const admin_ssh_console_frontend_ops_t *frontend_ops);
void admin_ssh_console_close(const admin_ssh_console_token_t *token);
/* Called only by the SSH owner task. Returns false when input must be backpressured. */
/* Called only by a frontend owner task. Returns false when input must be retried. */
bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token);
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
const uint8_t *data, size_t length,
size_t *consumed);
/* Called only by the SSH owner task; copies already-produced output without blocking. */
/* Called only by a frontend owner task; copies produced output without blocking. */
esp_err_t admin_ssh_console_read_output(const admin_ssh_console_token_t *token,
uint8_t *data, size_t capacity,
size_t *received);
+15 -1
View File
@@ -8,6 +8,7 @@
#include <stdlib.h>
#include <string.h>
#include "admin_command_gate.h"
#include "esp_console.h"
#include "local_display.h"
#include "local_status_ui.h"
@@ -113,7 +114,7 @@ static int apply_parameter(const char *parameter, const char *text)
return 0;
}
static int command_display(int argc, char **argv)
static int command_display_inner(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
return show_status();
@@ -189,6 +190,19 @@ static int command_display(int argc, char **argv)
return 1;
}
static int command_display(int argc, char **argv)
{
esp_err_t error = admin_command_gate_take();
if (error != ESP_OK) {
printf("Display administration unavailable: %s\n",
esp_err_to_name(error));
return 1;
}
int result = command_display_inner(argc, argv);
admin_command_gate_give();
return result;
}
esp_err_t local_ui_console_register_commands(void)
{
const esp_console_cmd_t command = {
+35 -1
View File
@@ -6,6 +6,7 @@
#include <stdlib.h>
#include <string.h>
#include "admin_command_gate.h"
#include "esp_console.h"
#include "esp_err.h"
#include "rs232_port_owner.h"
@@ -191,7 +192,7 @@ static int set_parameter(const char *parameter, const char *value)
return 0;
}
static int command_serial(int argc, char **argv)
static int command_serial_impl(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0)) {
return show_status();
@@ -290,6 +291,39 @@ static int command_serial(int argc, char **argv)
return 1;
}
static bool command_serial_requires_gate(int argc, char **argv)
{
if (argc == 4 && strcmp(argv[1], "set") == 0) {
return true;
}
if (argc != 2) {
return false;
}
return strcmp(argv[1], "start") == 0 ||
strcmp(argv[1], "stop") == 0 ||
strcmp(argv[1], "save") == 0 ||
strcmp(argv[1], "load") == 0 ||
strcmp(argv[1], "defaults") == 0 ||
strcmp(argv[1], "reset") == 0;
}
static int command_serial(int argc, char **argv)
{
if (!command_serial_requires_gate(argc, argv)) {
return command_serial_impl(argc, argv);
}
esp_err_t error = admin_command_gate_take();
if (error != ESP_OK) {
printf("Could not serialize serial administration: %s\n",
esp_err_to_name(error));
return 1;
}
int result = command_serial_impl(argc, argv);
admin_command_gate_give();
return result;
}
esp_err_t serial_console_register_commands(void)
{
const esp_console_cmd_t command = {
+54
View File
@@ -518,6 +518,60 @@ esp_err_t session_broker_force_writer(session_broker_client_id_t client_id)
return ESP_OK;
}
esp_err_t session_broker_compare_exchange_writer(
session_broker_client_id_t expected_writer_id,
session_broker_client_id_t target_client_id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (target_client_id == SESSION_BROKER_NO_CLIENT) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_mutex, portMAX_DELAY);
if (s_writer_id != expected_writer_id) {
xSemaphoreGive(s_mutex);
return ESP_ERR_INVALID_STATE;
}
session_broker_slot_t *new_writer = find_slot_locked(target_client_id);
if (new_writer == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_NOT_FOUND;
}
if (expected_writer_id == target_client_id) {
xSemaphoreGive(s_mutex);
return ESP_OK;
}
session_broker_slot_t *old_writer =
find_slot_locked(expected_writer_id);
if (expected_writer_id != SESSION_BROKER_NO_CLIENT && old_writer == NULL) {
xSemaphoreGive(s_mutex);
return ESP_ERR_INVALID_STATE;
}
s_writer_id = target_client_id;
if (old_writer != NULL) {
++old_writer->counters.writer_revocations;
++old_writer->counters.writer_changes;
++s_counters.writer_revocations;
}
++new_writer->counters.writer_grants;
++new_writer->counters.writer_changes;
++s_counters.writer_grants;
++s_counters.writer_changes;
if (old_writer != NULL) {
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_REVOKED,
expected_writer_id, s_writer_id);
}
broadcast_event_locked(SESSION_BROKER_EVENT_WRITER_GRANTED,
target_client_id, s_writer_id);
xSemaphoreGive(s_mutex);
return ESP_OK;
}
esp_err_t session_broker_force_release_writer(
session_broker_client_id_t expected_writer_id)
{
+8
View File
@@ -130,6 +130,14 @@ esp_err_t session_broker_disconnect(session_broker_client_id_t client_id);
esp_err_t session_broker_request_writer(session_broker_client_id_t client_id);
esp_err_t session_broker_release_writer(session_broker_client_id_t client_id);
esp_err_t session_broker_force_writer(session_broker_client_id_t client_id);
/*
* Atomically replace exactly the expected current writer with a connected target.
* expected_writer_id may be zero; target_client_id must identify a live client.
* A changed current writer returns ESP_ERR_INVALID_STATE without altering the lease.
*/
esp_err_t session_broker_compare_exchange_writer(
session_broker_client_id_t expected_writer_id,
session_broker_client_id_t target_client_id);
/* Revoke only if the expected client still owns the writer lease. */
esp_err_t session_broker_force_release_writer(session_broker_client_id_t expected_writer_id);
session_broker_client_id_t session_broker_get_writer_id(void);
+1 -1
View File
@@ -324,7 +324,7 @@ static int command_ssh(int argc, char **argv)
}
if (error == ESP_OK) {
error = admin_ssh_console_dispatch_defer(
ADMIN_SSH_DEFER_DISCONNECT, session_id);
ADMIN_SSH_DEFER_SSH_DISCONNECT, session_id);
}
} else {
error = ssh_transport_disconnect(session_id);
+70 -1
View File
@@ -49,6 +49,9 @@
#define SSH_TRANSPORT_GENERATION_MAX 0x3fffffffU
#define SSH_TRANSPORT_WOLFSSH_READ_BUDGET 2048U
_Static_assert(SSH_TRANSPORT_MAX_SESSIONS == ADMIN_SSH_CONSOLE_SSH_SLOT_COUNT,
"SSH admin slots must retain global indexes 0 and 1");
static const char *TAG = "ssh_transport";
typedef struct {
@@ -132,12 +135,77 @@ static admin_ssh_console_token_t admin_console_token(const ssh_slot_t *slot,
size_t slot_index)
{
return (admin_ssh_console_token_t){
.frontend = ADMIN_SSH_CONSOLE_FRONTEND_SSH,
.slot_index = (uint8_t)slot_index,
.session_id = slot->session_id,
.slot_generation = slot->generation,
};
}
static bool admin_console_token_is_ssh(const admin_ssh_console_token_t *token)
{
return token != NULL && token->frontend == ADMIN_SSH_CONSOLE_FRONTEND_SSH &&
token->slot_index < SSH_TRANSPORT_MAX_SESSIONS &&
token->session_id != 0U && token->slot_generation != 0U;
}
static bool admin_console_snapshot_matches_locked(
const admin_ssh_console_token_t *token)
{
const ssh_transport_session_snapshot_t *snapshot =
&s_session_snapshots[token->slot_index];
return s_initialized && snapshot->active &&
snapshot->session_id == token->session_id &&
snapshot->generation == token->slot_generation &&
snapshot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;
}
static bool admin_console_binding_is_current(
const admin_ssh_console_token_t *token)
{
if (!admin_console_token_is_ssh(token)) {
return false;
}
taskENTER_CRITICAL(&s_lock);
const ssh_transport_session_snapshot_t *snapshot =
&s_session_snapshots[token->slot_index];
bool current = admin_console_snapshot_matches_locked(token) &&
snapshot->state == SSH_TRANSPORT_SESSION_ACTIVE &&
snapshot->authenticated && snapshot->principal_valid &&
!snapshot->close_requested &&
s_external_close_id[token->slot_index] != token->session_id;
taskEXIT_CRITICAL(&s_lock);
return current;
}
static bool admin_console_transport_output_is_drained(
const admin_ssh_console_token_t *token)
{
if (!admin_console_token_is_ssh(token)) {
return false;
}
taskENTER_CRITICAL(&s_lock);
bool drained = admin_console_snapshot_matches_locked(token) &&
!s_session_snapshots[token->slot_index].tx_pending;
taskEXIT_CRITICAL(&s_lock);
return drained;
}
static esp_err_t admin_console_request_disconnect(
const admin_ssh_console_token_t *token, uint32_t session_id)
{
if (!admin_console_token_is_ssh(token)) {
return ESP_ERR_INVALID_ARG;
}
return ssh_transport_disconnect(session_id);
}
static const admin_ssh_console_frontend_ops_t s_admin_console_frontend_ops = {
.binding_is_current = admin_console_binding_is_current,
.transport_output_is_drained = admin_console_transport_output_is_drained,
.request_disconnect = admin_console_request_disconnect,
};
static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
{
ssh_transport_session_snapshot_t snapshot = {
@@ -924,7 +992,8 @@ static void process_handshake(ssh_slot_t *slot, size_t slot_index)
slot->route = SSH_TRANSPORT_ROUTE_BROKER;
} else if (slot->principal.role == USER_ROLE_ADMIN) {
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
error = admin_ssh_console_open(&token, &slot->principal);
error = admin_ssh_console_open(
&token, &slot->principal, &s_admin_console_frontend_ops);
if (error != ESP_OK) {
add_counter(&s_counters.admin_console_admission_failures, 1U);
request_slot_close(slot, false);
+431
View File
@@ -0,0 +1,431 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Typed, serialized user-administration mutations with transport revocation. */
#include "user_admin_service.h"
#include <string.h>
#include "admin_command_gate.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "web_server.h"
typedef enum {
USER_ADMIN_OPERATION_CREATE = 0,
USER_ADMIN_OPERATION_CREATE_GENERATED,
USER_ADMIN_OPERATION_DELETE,
USER_ADMIN_OPERATION_SET_ROLE,
USER_ADMIN_OPERATION_SET_PASSWORD,
USER_ADMIN_OPERATION_GENERATE_PASSWORD,
USER_ADMIN_OPERATION_ADD_SSH_KEY,
USER_ADMIN_OPERATION_REMOVE_SSH_KEY,
USER_ADMIN_OPERATION_CLEAR_SSH_KEYS,
} user_admin_operation_type_t;
typedef struct {
user_admin_operation_type_t type;
const uint8_t *username;
size_t username_length;
user_role_t role;
const uint8_t *password;
size_t password_length;
const uint8_t *key_type;
size_t key_type_length;
const uint8_t *key_blob;
size_t key_blob_length;
uint8_t key_index;
uint8_t *added_key_index;
user_database_generated_password_t *generated_password;
} user_admin_operation_t;
/* Every access is protected by admin_command_gate. Keep this large snapshot off task stacks. */
static user_database_snapshot_t s_snapshot;
static const user_database_user_snapshot_t *find_snapshot_user(
const user_admin_operation_t *operation)
{
for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) {
const user_database_user_snapshot_t *user = &s_snapshot.users[index];
if (user->active && user->username_length == operation->username_length &&
memcmp(user->username, operation->username, operation->username_length) == 0) {
return user;
}
}
return NULL;
}
static bool role_valid(user_role_t role)
{
return role == USER_ROLE_USER || role == USER_ROLE_ADMIN;
}
static bool operation_arguments_valid(const user_admin_operation_t *operation)
{
if (operation == NULL ||
!user_database_username_valid(operation->username, operation->username_length)) {
return false;
}
switch (operation->type) {
case USER_ADMIN_OPERATION_CREATE:
return role_valid(operation->role) &&
user_database_password_valid(operation->password,
operation->password_length);
case USER_ADMIN_OPERATION_CREATE_GENERATED:
return role_valid(operation->role) && operation->generated_password != NULL;
case USER_ADMIN_OPERATION_DELETE:
return true;
case USER_ADMIN_OPERATION_SET_ROLE:
return role_valid(operation->role);
case USER_ADMIN_OPERATION_SET_PASSWORD:
return user_database_password_valid(operation->password,
operation->password_length);
case USER_ADMIN_OPERATION_GENERATE_PASSWORD:
return operation->generated_password != NULL;
case USER_ADMIN_OPERATION_ADD_SSH_KEY:
return operation->added_key_index != NULL;
case USER_ADMIN_OPERATION_REMOVE_SSH_KEY:
return operation->key_index < USER_DATABASE_MAX_SSH_KEYS_PER_USER;
case USER_ADMIN_OPERATION_CLEAR_SSH_KEYS:
return true;
default:
return false;
}
}
static bool operation_expected_to_commit(
const user_admin_operation_t *operation,
const user_database_user_snapshot_t *user)
{
if (operation->type == USER_ADMIN_OPERATION_SET_ROLE && user != NULL) {
return user->role != operation->role;
}
if (operation->type == USER_ADMIN_OPERATION_CLEAR_SSH_KEYS && user != NULL) {
return user->public_key_count != 0U;
}
return true;
}
static esp_err_t apply_database_operation(const user_admin_operation_t *operation)
{
switch (operation->type) {
case USER_ADMIN_OPERATION_CREATE:
return user_database_create(
operation->username, operation->username_length, operation->role,
operation->password, operation->password_length);
case USER_ADMIN_OPERATION_CREATE_GENERATED:
return user_database_create_generated(
operation->username, operation->username_length, operation->role,
operation->generated_password);
case USER_ADMIN_OPERATION_DELETE:
return user_database_delete(operation->username, operation->username_length);
case USER_ADMIN_OPERATION_SET_ROLE:
return user_database_set_role(operation->username, operation->username_length,
operation->role);
case USER_ADMIN_OPERATION_SET_PASSWORD:
return user_database_set_password(
operation->username, operation->username_length,
operation->password, operation->password_length);
case USER_ADMIN_OPERATION_GENERATE_PASSWORD:
return user_database_generate_password(
operation->username, operation->username_length,
operation->generated_password);
case USER_ADMIN_OPERATION_ADD_SSH_KEY:
return user_database_add_ssh_key(
operation->username, operation->username_length,
operation->key_type, operation->key_type_length,
operation->key_blob, operation->key_blob_length,
operation->added_key_index);
case USER_ADMIN_OPERATION_REMOVE_SSH_KEY:
return user_database_remove_ssh_key(
operation->username, operation->username_length, operation->key_index);
case USER_ADMIN_OPERATION_CLEAR_SSH_KEYS:
return user_database_clear_ssh_keys(
operation->username, operation->username_length);
default:
return ESP_ERR_INVALID_ARG;
}
}
static void revoke_committed_user(const user_admin_operation_t *operation,
user_admin_service_result_t *result)
{
result->revocation.attempted = true;
result->revocation.web_error = web_server_revoke_user(
operation->username, operation->username_length);
result->revocation.ssh_error = ssh_transport_revoke_user(
operation->username, operation->username_length);
}
static bool operation_is_create(const user_admin_operation_t *operation)
{
return operation->type == USER_ADMIN_OPERATION_CREATE ||
operation->type == USER_ADMIN_OPERATION_CREATE_GENERATED;
}
static bool operation_advances_auth_generation(
const user_admin_operation_t *operation)
{
return operation->type == USER_ADMIN_OPERATION_SET_ROLE ||
operation->type == USER_ADMIN_OPERATION_SET_PASSWORD ||
operation->type == USER_ADMIN_OPERATION_GENERATE_PASSWORD ||
operation->type == USER_ADMIN_OPERATION_ADD_SSH_KEY ||
operation->type == USER_ADMIN_OPERATION_REMOVE_SSH_KEY ||
operation->type == USER_ADMIN_OPERATION_CLEAR_SSH_KEYS;
}
static user_admin_service_failure_t classify_failure(
const user_admin_operation_t *operation,
const user_database_user_snapshot_t *user, esp_err_t error)
{
if (error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY) {
return USER_ADMIN_SERVICE_FAILURE_DUPLICATE_KEY;
}
if (error == ESP_ERR_NO_MEM) {
return USER_ADMIN_SERVICE_FAILURE_CAPACITY;
}
if (error != ESP_ERR_INVALID_STATE) {
return USER_ADMIN_SERVICE_FAILURE_NONE;
}
if (operation_is_create(operation) && user != NULL) {
return USER_ADMIN_SERVICE_FAILURE_DUPLICATE_USERNAME;
}
if (user != NULL && user->role == USER_ROLE_ADMIN &&
s_snapshot.admin_count <= 1U &&
(operation->type == USER_ADMIN_OPERATION_DELETE ||
(operation->type == USER_ADMIN_OPERATION_SET_ROLE &&
operation->role != USER_ROLE_ADMIN))) {
return USER_ADMIN_SERVICE_FAILURE_FINAL_ADMIN;
}
if (s_snapshot.generation == UINT32_MAX ||
(user != NULL && operation_advances_auth_generation(operation) &&
user->auth_generation == UINT32_MAX)) {
return USER_ADMIN_SERVICE_FAILURE_GENERATION_EXHAUSTED;
}
return USER_ADMIN_SERVICE_FAILURE_STATE;
}
static esp_err_t execute_operation(
const user_admin_operation_t *operation,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
if (result == NULL) {
return ESP_ERR_INVALID_ARG;
}
memset(result, 0, sizeof(*result));
if (!operation_arguments_valid(operation) ||
(operation_is_create(operation) && expectation != NULL &&
expectation->user_id != 0U)) {
return ESP_ERR_INVALID_ARG;
}
if (operation->added_key_index != NULL) {
*operation->added_key_index = 0U;
}
esp_err_t error = admin_command_gate_take();
if (error != ESP_OK) {
return error;
}
error = user_database_get_snapshot(&s_snapshot);
if (error != ESP_OK) {
admin_command_gate_give();
return error;
}
const uint32_t before_generation = s_snapshot.generation;
const user_database_user_snapshot_t *user = find_snapshot_user(operation);
result->database_generation = before_generation;
if (user != NULL) {
result->user_id = user->user_id;
}
if (expectation != NULL && expectation->database_generation != 0U &&
expectation->database_generation != before_generation) {
result->conflict = USER_ADMIN_SERVICE_CONFLICT_DATABASE_GENERATION;
error = ESP_ERR_INVALID_STATE;
} else if (!operation_is_create(operation) && expectation != NULL &&
expectation->user_id != 0U &&
(user == NULL || expectation->user_id != user->user_id)) {
result->conflict = USER_ADMIN_SERVICE_CONFLICT_USER_ID;
error = ESP_ERR_INVALID_STATE;
} else {
const bool expected_commit = operation_expected_to_commit(operation, user);
error = apply_database_operation(operation);
if (error != ESP_OK) {
result->failure = classify_failure(operation, user, error);
} else {
esp_err_t snapshot_error = user_database_get_snapshot(&s_snapshot);
if (snapshot_error == ESP_OK) {
result->database_generation = s_snapshot.generation;
result->mutation_committed =
s_snapshot.generation != before_generation;
const user_database_user_snapshot_t *updated_user =
find_snapshot_user(operation);
if (updated_user != NULL) {
result->user_id = updated_user->user_id;
}
} else {
result->mutation_committed = expected_commit;
if (expected_commit) {
result->database_generation = before_generation + 1U;
}
}
if (result->mutation_committed) {
revoke_committed_user(operation, result);
}
}
}
admin_command_gate_give();
if (error != ESP_OK && operation->generated_password != NULL) {
secure_wipe(operation->generated_password,
sizeof(*operation->generated_password));
}
return error;
}
esp_err_t user_admin_service_create(
const uint8_t *username, size_t username_length, user_role_t role,
const uint8_t *password, size_t password_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_CREATE,
.username = username,
.username_length = username_length,
.role = role,
.password = password,
.password_length = password_length,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_create_generated(
const uint8_t *username, size_t username_length, user_role_t role,
const user_admin_service_expectation_t *expectation,
user_database_generated_password_t *generated_password,
user_admin_service_result_t *result)
{
if (generated_password != NULL) {
secure_wipe(generated_password, sizeof(*generated_password));
}
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_CREATE_GENERATED,
.username = username,
.username_length = username_length,
.role = role,
.generated_password = generated_password,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_delete(
const uint8_t *username, size_t username_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_DELETE,
.username = username,
.username_length = username_length,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_set_role(
const uint8_t *username, size_t username_length, user_role_t role,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_SET_ROLE,
.username = username,
.username_length = username_length,
.role = role,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_set_password(
const uint8_t *username, size_t username_length,
const uint8_t *password, size_t password_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_SET_PASSWORD,
.username = username,
.username_length = username_length,
.password = password,
.password_length = password_length,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_generate_password(
const uint8_t *username, size_t username_length,
const user_admin_service_expectation_t *expectation,
user_database_generated_password_t *generated_password,
user_admin_service_result_t *result)
{
if (generated_password != NULL) {
secure_wipe(generated_password, sizeof(*generated_password));
}
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_GENERATE_PASSWORD,
.username = username,
.username_length = username_length,
.generated_password = generated_password,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_add_ssh_key(
const uint8_t *username, size_t username_length,
const uint8_t *key_type, size_t key_type_length,
const uint8_t *key_blob, size_t key_blob_length,
const user_admin_service_expectation_t *expectation,
uint8_t *key_index, user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_ADD_SSH_KEY,
.username = username,
.username_length = username_length,
.key_type = key_type,
.key_type_length = key_type_length,
.key_blob = key_blob,
.key_blob_length = key_blob_length,
.added_key_index = key_index,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_remove_ssh_key(
const uint8_t *username, size_t username_length, uint8_t key_index,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_REMOVE_SSH_KEY,
.username = username,
.username_length = username_length,
.key_index = key_index,
};
return execute_operation(&operation, expectation, result);
}
esp_err_t user_admin_service_clear_ssh_keys(
const uint8_t *username, size_t username_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result)
{
const user_admin_operation_t operation = {
.type = USER_ADMIN_OPERATION_CLEAR_SSH_KEYS,
.username = username,
.username_length = username_length,
};
return execute_operation(&operation, expectation, result);
}
+127
View File
@@ -0,0 +1,127 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Typed, serialized user-administration mutations with transport revocation. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "user_database.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
/* Zero disables the corresponding optimistic check. */
uint32_t database_generation;
uint32_t user_id;
} user_admin_service_expectation_t;
typedef enum {
USER_ADMIN_SERVICE_CONFLICT_NONE = 0,
USER_ADMIN_SERVICE_CONFLICT_DATABASE_GENERATION,
USER_ADMIN_SERVICE_CONFLICT_USER_ID,
} user_admin_service_conflict_t;
typedef struct {
bool attempted;
esp_err_t web_error;
esp_err_t ssh_error;
} user_admin_service_revocation_t;
typedef enum {
USER_ADMIN_SERVICE_FAILURE_NONE = 0,
USER_ADMIN_SERVICE_FAILURE_DUPLICATE_USERNAME,
USER_ADMIN_SERVICE_FAILURE_DUPLICATE_KEY,
USER_ADMIN_SERVICE_FAILURE_CAPACITY,
USER_ADMIN_SERVICE_FAILURE_FINAL_ADMIN,
USER_ADMIN_SERVICE_FAILURE_GENERATION_EXHAUSTED,
USER_ADMIN_SERVICE_FAILURE_STATE,
} user_admin_service_failure_t;
typedef struct {
/* True only when the database generation advanced for this operation. */
bool mutation_committed;
/* Current generation observed before, or immediately after, the operation. */
uint32_t database_generation;
/* Target account ID observed before the operation, or after a successful create. */
uint32_t user_id;
user_admin_service_conflict_t conflict;
user_admin_service_failure_t failure;
user_admin_service_revocation_t revocation;
} user_admin_service_result_t;
/*
* Every operation serializes its snapshot check and database mutation with
* admin_command_gate. A generation or user-ID mismatch returns
* ESP_ERR_INVALID_STATE and identifies the mismatch in result->conflict.
*
* A NULL expectation disables both optimistic checks. For existing-account
* operations, zero fields also disable their individual checks. Create
* operations require expectation->user_id to be zero because no prior account
* identity can be targeted.
*
* After a committed mutation, both transport revocation hooks are attempted.
* Their exact outcomes are returned separately and never replace ESP_OK from a
* successful database mutation. No-op role and key-clear requests do not
* advance the generation and do not trigger revocation.
*/
esp_err_t user_admin_service_create(
const uint8_t *username, size_t username_length, user_role_t role,
const uint8_t *password, size_t password_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result);
/* On success, the caller owns generated_password and must securely wipe it. */
esp_err_t user_admin_service_create_generated(
const uint8_t *username, size_t username_length, user_role_t role,
const user_admin_service_expectation_t *expectation,
user_database_generated_password_t *generated_password,
user_admin_service_result_t *result);
esp_err_t user_admin_service_delete(
const uint8_t *username, size_t username_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result);
esp_err_t user_admin_service_set_role(
const uint8_t *username, size_t username_length, user_role_t role,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result);
esp_err_t user_admin_service_set_password(
const uint8_t *username, size_t username_length,
const uint8_t *password, size_t password_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result);
/* On success, the caller owns generated_password and must securely wipe it. */
esp_err_t user_admin_service_generate_password(
const uint8_t *username, size_t username_length,
const user_admin_service_expectation_t *expectation,
user_database_generated_password_t *generated_password,
user_admin_service_result_t *result);
esp_err_t user_admin_service_add_ssh_key(
const uint8_t *username, size_t username_length,
const uint8_t *key_type, size_t key_type_length,
const uint8_t *key_blob, size_t key_blob_length,
const user_admin_service_expectation_t *expectation,
uint8_t *key_index, user_admin_service_result_t *result);
esp_err_t user_admin_service_remove_ssh_key(
const uint8_t *username, size_t username_length, uint8_t key_index,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result);
esp_err_t user_admin_service_clear_ssh_keys(
const uint8_t *username, size_t username_length,
const user_admin_service_expectation_t *expectation,
user_admin_service_result_t *result);
#ifdef __cplusplus
}
#endif
+58 -33
View File
@@ -14,9 +14,10 @@
#include "mbedtls/base64.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "user_admin_service.h"
#include "user_database.h"
#include "web_security.h"
#include "web_serial_transport.h"
#include "web_server.h"
#define USER_CONSOLE_KEY_LINE_CAPACITY 256U
@@ -40,15 +41,10 @@ static void print_usage(void)
printf(" user key clear <username> --force\n");
}
static void revoke_user_network_sessions(const char *username)
static void print_revocation_warnings(esp_err_t web_error, esp_err_t ssh_error)
{
size_t username_length = strlen(username);
esp_err_t web_error = web_serial_transport_revoke_user(
(const uint8_t *)username, username_length);
esp_err_t ssh_error = ssh_transport_revoke_user(
(const uint8_t *)username, username_length);
if (web_error != ESP_OK && web_error != ESP_ERR_INVALID_STATE) {
printf("Warning: WebSocket revocation failed: %s\n",
printf("Warning: Web session revocation failed: %s\n",
esp_err_to_name(web_error));
}
if (ssh_error != ESP_OK && ssh_error != ESP_ERR_INVALID_STATE) {
@@ -56,6 +52,25 @@ static void revoke_user_network_sessions(const char *username)
}
}
static void print_service_revocation_warnings(
const user_admin_service_result_t *result)
{
if (result->revocation.attempted) {
print_revocation_warnings(result->revocation.web_error,
result->revocation.ssh_error);
}
}
static void revoke_user_network_sessions(const char *username)
{
size_t username_length = strlen(username);
esp_err_t web_error = web_server_revoke_user(
(const uint8_t *)username, username_length);
esp_err_t ssh_error = ssh_transport_revoke_user(
(const uint8_t *)username, username_length);
print_revocation_warnings(web_error, ssh_error);
}
static void print_fingerprint(const uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH])
{
uint8_t encoded[48] = {0};
@@ -228,10 +243,12 @@ static int add_user(const char *username, const char *role_text, bool generated)
return 1;
}
esp_err_t error;
user_admin_service_result_t result;
if (generated) {
user_database_generated_password_t password;
error = user_database_create_generated((const uint8_t *)username,
strlen(username), role, &password);
error = user_admin_service_create_generated(
(const uint8_t *)username, strlen(username), role, NULL,
&password, &result);
if (error == ESP_OK) {
show_generated_password(username, &password);
}
@@ -240,8 +257,9 @@ static int add_user(const char *username, const char *role_text, bool generated)
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_create((const uint8_t *)username, strlen(username),
role, password, password_length);
error = user_admin_service_create(
(const uint8_t *)username, strlen(username), role,
password, password_length, NULL, &result);
}
secure_wipe(password, sizeof(password));
}
@@ -249,7 +267,7 @@ static int add_user(const char *username, const char *role_text, bool generated)
printf("Could not add user: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(username);
print_service_revocation_warnings(&result);
printf("User '%s' added with role %s.\n", username, user_role_to_string(role));
return 0;
}
@@ -257,10 +275,11 @@ static int add_user(const char *username, const char *role_text, bool generated)
static int change_password(const char *username, bool generated)
{
esp_err_t error;
user_admin_service_result_t result;
if (generated) {
user_database_generated_password_t password;
error = user_database_generate_password((const uint8_t *)username,
strlen(username), &password);
error = user_admin_service_generate_password(
(const uint8_t *)username, strlen(username), NULL, &password, &result);
if (error == ESP_OK) {
show_generated_password(username, &password);
}
@@ -269,9 +288,9 @@ static int change_password(const char *username, bool generated)
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_set_password((const uint8_t *)username,
strlen(username),
password, password_length);
error = user_admin_service_set_password(
(const uint8_t *)username, strlen(username),
password, password_length, NULL, &result);
}
secure_wipe(password, sizeof(password));
}
@@ -279,7 +298,7 @@ static int change_password(const char *username, bool generated)
printf("Could not change password: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(username);
print_service_revocation_warnings(&result);
printf("Password changed; affected network sessions are now stale and will be revoked.\n");
return 0;
}
@@ -317,9 +336,10 @@ static int add_key_parts(const char *username,
}
uint8_t key_index = 0U;
esp_err_t error = user_database_add_ssh_key(
user_admin_service_result_t result;
esp_err_t error = user_admin_service_add_ssh_key(
(const uint8_t *)username, strlen(username), type, type_length,
blob, blob_length, &key_index);
blob, blob_length, NULL, &key_index, &result);
secure_wipe(blob, sizeof(blob));
if (error != ESP_OK) {
if (error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY) {
@@ -332,7 +352,7 @@ static int add_key_parts(const char *username,
}
return 1;
}
revoke_user_network_sessions(username);
print_service_revocation_warnings(&result);
printf("SSH public key added at index %u. Public-key login is active.\n",
(unsigned int)key_index);
return 0;
@@ -425,13 +445,15 @@ static int command_user_inner(int argc, char **argv)
}
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
strcmp(argv[3], "--force") == 0) {
esp_err_t error = user_database_delete((const uint8_t *)argv[2], strlen(argv[2]));
user_admin_service_result_t result;
esp_err_t error = user_admin_service_delete(
(const uint8_t *)argv[2], strlen(argv[2]), NULL, &result);
if (error != ESP_OK) {
printf("Could not delete user (the migrated or final admin is protected): %s\n",
esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[2]);
print_service_revocation_warnings(&result);
printf("User '%s' deleted.\n", argv[2]);
return 0;
}
@@ -443,14 +465,15 @@ static int command_user_inner(int argc, char **argv)
printf("Role must be user or admin.\n");
return 1;
}
esp_err_t error = user_database_set_role((const uint8_t *)argv[2],
strlen(argv[2]), role);
user_admin_service_result_t result;
esp_err_t error = user_admin_service_set_role(
(const uint8_t *)argv[2], strlen(argv[2]), role, NULL, &result);
if (error != ESP_OK) {
printf("Could not change role (the final admin is protected): %s\n",
esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[2]);
print_service_revocation_warnings(&result);
printf("User '%s' role changed to %s.\n", argv[2], user_role_to_string(role));
return 0;
}
@@ -484,25 +507,27 @@ static int command_user_inner(int argc, char **argv)
printf("Key index must be 0..2.\n");
return 1;
}
esp_err_t error = user_database_remove_ssh_key(
(const uint8_t *)argv[3], strlen(argv[3]), index);
user_admin_service_result_t result;
esp_err_t error = user_admin_service_remove_ssh_key(
(const uint8_t *)argv[3], strlen(argv[3]), index, NULL, &result);
if (error != ESP_OK) {
printf("Could not delete SSH key: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[3]);
print_service_revocation_warnings(&result);
printf("SSH key %u deleted for '%s'.\n", (unsigned int)index, argv[3]);
return 0;
}
if (argc == 5 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "clear") == 0 && strcmp(argv[4], "--force") == 0) {
esp_err_t error = user_database_clear_ssh_keys(
(const uint8_t *)argv[3], strlen(argv[3]));
user_admin_service_result_t result;
esp_err_t error = user_admin_service_clear_ssh_keys(
(const uint8_t *)argv[3], strlen(argv[3]), NULL, &result);
if (error != ESP_OK) {
printf("Could not clear SSH keys: %s\n", esp_err_to_name(error));
return 1;
}
revoke_user_network_sessions(argv[3]);
print_service_revocation_warnings(&result);
printf("SSH keys cleared for '%s'.\n", argv[3]);
return 0;
}
File diff suppressed because it is too large Load Diff
+136
View File
@@ -0,0 +1,136 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Authenticated, bounded WebSocket frontend for the canonical admin console. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "esp_http_server.h"
#include "user_database.h"
#include "web_session.h"
#ifdef __cplusplus
extern "C" {
#endif
#define WEB_ADMIN_TRANSPORT_MAX_SESSIONS 1U
#define WEB_ADMIN_TRANSPORT_MAX_TICKETS 2U
#define WEB_ADMIN_TRANSPORT_TICKET_LENGTH 32U
#define WEB_ADMIN_TRANSPORT_TICKET_CAPACITY \
(WEB_ADMIN_TRANSPORT_TICKET_LENGTH + 1U)
#define WEB_ADMIN_TRANSPORT_TICKET_LIFETIME_SECONDS 30U
#define WEB_ADMIN_TRANSPORT_MAX_RX_PAYLOAD 1024U
#define WEB_ADMIN_TRANSPORT_PENDING_INPUT_CAPACITY 1024U
#define WEB_ADMIN_TRANSPORT_TX_PAYLOAD_SIZE 512U
#define WEB_ADMIN_TRANSPORT_TICKET_URI "/api/admin/ws-ticket"
#define WEB_ADMIN_TRANSPORT_WS_URI "/ws/admin"
#define WEB_ADMIN_TRANSPORT_TICKET_QUERY_KEY "ticket"
typedef struct {
uint64_t tickets_issued;
uint64_t tickets_consumed;
uint64_t tickets_rejected;
uint64_t tickets_expired;
uint64_t connections;
uint64_t connection_failures;
uint64_t console_admission_failures;
uint64_t disconnections;
uint64_t session_revocations;
uint64_t currentness_failures;
uint64_t rx_ws_frames_accepted;
uint64_t rx_ws_frames_rejected;
uint64_t rx_ws_bytes_accepted;
uint64_t rx_ws_bytes_rejected;
uint64_t input_bytes_fed;
uint64_t input_feed_retries;
uint64_t input_overflow_closes;
uint64_t tx_binary_frames;
uint64_t tx_binary_bytes;
uint64_t send_failures;
uint64_t queue_failures;
uint64_t protocol_errors;
uint64_t close_requests;
} web_admin_transport_counters_t;
typedef struct {
bool active;
bool principal_valid;
bool input_pending;
bool tx_pending;
bool close_requested;
int socket_fd;
uint32_t session_id;
uint32_t generation;
size_t pending_input_bytes;
user_role_t user_role;
user_auth_method_t auth_method;
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
} web_admin_transport_session_snapshot_t;
typedef struct {
bool initialized;
bool server_attached;
bool accepting_connections;
uint32_t active_sessions;
uint32_t active_tickets;
web_admin_transport_session_snapshot_t
sessions[WEB_ADMIN_TRANSPORT_MAX_SESSIONS];
web_admin_transport_counters_t counters;
} web_admin_transport_snapshot_t;
/*
* Allocate no heap objects and start the permanent static transport task.
* CONFIG_HTTPD_WS_SUPPORT must be enabled. CONFIG_HTTPD_QUEUE_WORK_BLOCKING must
* be disabled because that IDF mode can wait forever inside httpd_queue_work().
*/
esp_err_t web_admin_transport_init(void);
/* Attach after HTTPD start; detach before stopping that exact server. */
esp_err_t web_admin_transport_attach_server(httpd_handle_t server);
esp_err_t web_admin_transport_detach_server(httpd_handle_t server);
/*
* Complete a timed-out detach only after httpd_ssl_stop() has successfully
* destroyed that exact server, so discarded queued work can be retired safely.
*/
esp_err_t web_admin_transport_finalize_stopped_server(httpd_handle_t server);
/*
* Convenience POST response helper for /api/admin/ws-ticket. Authentication and
* CSRF validation remain outside this module: pass the principal and exact session
* reference produced by the authenticated request. Register it as HTTP_POST.
*/
esp_err_t web_admin_transport_handle_authenticated_ticket_request(
httpd_req_t *request, const user_principal_t *principal,
const web_session_ref_t *session_reference);
/*
* Handler for /ws/admin. Register as HTTP_GET with is_websocket=true and
* handle_ws_control_frames=false. Only complete binary terminal frames are valid.
*/
esp_err_t web_admin_transport_ws_handler(httpd_req_t *request);
/* Snapshot and counters contain no ticket, digest, browser-session reference, or data. */
esp_err_t web_admin_transport_get_snapshot(
web_admin_transport_snapshot_t *snapshot);
esp_err_t web_admin_transport_clear_counters(void);
/* Invalidate tickets and request closure for one exact browser login session. */
esp_err_t web_admin_transport_revoke_session(
const web_session_ref_t *session_reference);
/* Invalidate tickets/sessions for one account, or all admin web sessions. */
esp_err_t web_admin_transport_revoke_user(const uint8_t *username,
size_t username_length);
esp_err_t web_admin_transport_revoke_sessions(void);
#ifdef __cplusplus
}
#endif
+162 -29
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* UART0 HTTPS lifecycle, legacy recovery credential, and certificate commands. */
/* Shared HTTPS lifecycle, legacy recovery credential, and certificate commands. */
#include "web_console.h"
@@ -7,12 +7,15 @@
#include <stdio.h>
#include <string.h>
#include "admin_ssh_console.h"
#include "esp_console.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "user_database.h"
#include "web_admin_transport.h"
#include "web_security.h"
#include "web_serial_transport.h"
#include "web_session.h"
#include "web_server.h"
static void print_usage(void)
@@ -52,13 +55,45 @@ static int show_status(void)
(unsigned int)snapshot.port,
esp_err_to_name(snapshot.last_error));
if (users_error == ESP_OK) {
printf("Authentication: HTTP Basic over TLS via user database, users=%u admins=%u\n",
printf("Authentication: HTTPS login sessions via user database, active=%" PRIu32 "/%u users=%u admins=%u\n",
snapshot.active_sessions, WEB_SESSION_MAX_SESSIONS,
(unsigned int)users.user_count, (unsigned int)users.admin_count);
} else {
printf("Authentication database unavailable: %s; use 'user recover --force'.\n",
esp_err_to_name(users_error));
}
printf("Endpoints: GET /, GET /api/status, POST /api/ws-ticket, WSS /ws/serial\n");
printf("Endpoints: login/logout/session/status, serial WSS, admin WSS, and typed admin Serial/Wi-Fi/broker APIs\n");
web_admin_transport_snapshot_t admin_transport;
esp_err_t admin_transport_error =
web_admin_transport_get_snapshot(&admin_transport);
if (admin_transport_error == ESP_OK) {
printf("WebSocket admin: attached=%s accepting=%s sessions=%" PRIu32
"/%u tickets=%" PRIu32 "\n",
admin_transport.server_attached ? "yes" : "no",
admin_transport.accepting_connections ? "yes" : "no",
admin_transport.active_sessions,
WEB_ADMIN_TRANSPORT_MAX_SESSIONS,
admin_transport.active_tickets);
for (size_t index = 0U;
index < WEB_ADMIN_TRANSPORT_MAX_SESSIONS; ++index) {
const web_admin_transport_session_snapshot_t *session =
&admin_transport.sessions[index];
if (!session->active) {
continue;
}
printf(" admin-web session=%" PRIu32 " generation=%" PRIu32
" account=%s input-pending=%u tx-pending=%s closing=%s\n",
session->session_id, session->generation,
session->principal_valid ? session->username : "-",
(unsigned int)session->pending_input_bytes,
session->tx_pending ? "yes" : "no",
session->close_requested ? "yes" : "no");
}
} else {
printf("WebSocket admin transport unavailable: %s\n",
esp_err_to_name(snapshot.admin_transport_error));
}
web_serial_transport_snapshot_t transport;
esp_err_t transport_error = web_serial_transport_get_snapshot(&transport);
@@ -122,6 +157,82 @@ static int show_counters(void)
counter->authentication_failures, counter->root_requests,
counter->status_requests, counter->ticket_requests,
counter->asset_requests, counter->response_errors);
printf("Login sessions: active=%" PRIu32 "/%u login=%" PRIu64
" success=%" PRIu64 " failure=%" PRIu64 " throttled=%" PRIu64
" logout=%" PRIu64 " session-info=%" PRIu64 "\n",
snapshot.active_sessions, WEB_SESSION_MAX_SESSIONS,
counter->login_requests, counter->login_successes,
counter->login_failures, counter->login_throttled,
counter->logout_requests, counter->session_requests);
const web_session_counters_t *sessions = &snapshot.session_counters;
printf("Session table: created=%" PRIu64 " create-failures=%" PRIu64
" capacity=%" PRIu64 " authenticated=%" PRIu64
" rejected=%" PRIu64 " expired=%" PRIu64 "\n",
sessions->created, sessions->create_failures,
sessions->capacity_failures, sessions->authenticated,
sessions->rejected, sessions->expired);
printf("Session lifecycle: stale-principal=%" PRIu64
" destroyed=%" PRIu64 " revocations=%" PRIu64
" csrf-accepted=%" PRIu64 " csrf-rejected=%" PRIu64 "\n",
sessions->stale_principal, sessions->destroyed,
sessions->revocations, sessions->csrf_accepted,
sessions->csrf_rejected);
printf("Request rejection: cookie=%" PRIu64 " origin=%" PRIu64
" csrf=%" PRIu64 "\n",
counter->cookie_rejections, counter->origin_rejections,
counter->csrf_rejections);
printf("Admin API: tickets=%" PRIu64 " auth-denied=%" PRIu64
" requests=%" PRIu64 " rejected=%" PRIu64
" operation-failures=%" PRIu64 "\n",
counter->admin_ticket_requests,
counter->admin_authorization_failures,
counter->admin_api_requests,
counter->admin_request_rejections,
counter->admin_operation_failures);
printf("Writer transfer: attempts=%" PRIu64 " success=%" PRIu64
" conflicts=%" PRIu64 "\n",
counter->writer_transfer_attempts,
counter->writer_transfer_successes,
counter->writer_transfer_conflicts);
web_admin_transport_snapshot_t admin_transport;
esp_err_t admin_error =
web_admin_transport_get_snapshot(&admin_transport);
if (admin_error == ESP_OK) {
const web_admin_transport_counters_t *admin =
&admin_transport.counters;
printf("Admin WebSocket tickets: issued=%" PRIu64
" consumed=%" PRIu64 " rejected=%" PRIu64
" expired=%" PRIu64 "\n",
admin->tickets_issued, admin->tickets_consumed,
admin->tickets_rejected, admin->tickets_expired);
printf("Admin WebSocket sessions: connect=%" PRIu64
" failures=%" PRIu64 " admission-failures=%" PRIu64
" disconnect=%" PRIu64 " revocations=%" PRIu64
" stale=%" PRIu64 "\n",
admin->connections, admin->connection_failures,
admin->console_admission_failures, admin->disconnections,
admin->session_revocations, admin->currentness_failures);
printf("Admin WebSocket I/O: rx-frames=%" PRIu64
" rx-rejected=%" PRIu64 " rx-bytes=%" PRIu64
" rx-bytes-rejected=%" PRIu64 " fed=%" PRIu64
" retries=%" PRIu64 " overflow-close=%" PRIu64
" tx-frames=%" PRIu64 " tx-bytes=%" PRIu64 "\n",
admin->rx_ws_frames_accepted,
admin->rx_ws_frames_rejected,
admin->rx_ws_bytes_accepted,
admin->rx_ws_bytes_rejected, admin->input_bytes_fed,
admin->input_feed_retries, admin->input_overflow_closes,
admin->tx_binary_frames, admin->tx_binary_bytes);
printf("Admin WebSocket failures: send=%" PRIu64
" queue=%" PRIu64 " protocol=%" PRIu64
" closes=%" PRIu64 "\n",
admin->send_failures, admin->queue_failures,
admin->protocol_errors, admin->close_requests);
} else {
printf("Admin WebSocket counters unavailable: %s\n",
esp_err_to_name(admin_error));
}
web_serial_transport_snapshot_t transport;
error = web_serial_transport_get_snapshot(&transport);
@@ -212,20 +323,29 @@ static bool force_is_present(int argc, char **argv, int expected_argc)
return argc == expected_argc && strcmp(argv[expected_argc - 1], "--force") == 0;
}
static int restart_if_running(bool was_running)
static int refresh_tls_after_material_change(
const web_server_snapshot_t *before, bool ensure_running)
{
if (!was_running) {
return 0;
}
esp_err_t error = web_server_stop();
if (error != ESP_OK) {
printf("Material changed, but the old TLS server could not stop: %s\n",
bool start_if_unchanged = ensure_running || before->desired_running;
if (admin_ssh_console_dispatch_frontend() ==
ADMIN_SSH_CONSOLE_FRONTEND_WEB) {
admin_ssh_deferred_action_type_t action = start_if_unchanged
? ADMIN_SSH_DEFER_WEB_TLS_REFRESH_RUNNING
: ADMIN_SSH_DEFER_WEB_TLS_REFRESH_STOPPED;
esp_err_t error = admin_ssh_console_dispatch_defer(
action, before->lifecycle_generation);
if (error == ESP_OK) {
printf("HTTPS TLS refresh scheduled after administrative output drains.\n");
return 0;
}
printf("Could not schedule deferred HTTPS TLS refresh: %s; applying it now.\n",
esp_err_to_name(error));
return 1;
}
error = web_server_start();
esp_err_t error = web_server_refresh_tls(
before->lifecycle_generation, start_if_unchanged);
if (error != ESP_OK) {
printf("Material changed, but HTTPS could not restart: %s\n",
printf("Security material changed, but HTTPS could not apply it: %s\n",
esp_err_to_name(error));
return 1;
}
@@ -249,7 +369,7 @@ static void synchronize_migrated_user(
return;
}
if (synchronized) {
(void)web_serial_transport_revoke_user(
(void)web_server_revoke_user(
(const uint8_t *)credentials->username,
credentials->username_length);
(void)ssh_transport_revoke_user(
@@ -300,15 +420,19 @@ static int rotate_certificate(void)
return 1;
}
printf("Web certificate and private key rotated and persisted.\n");
return restart_if_running(snapshot.running);
return refresh_tls_after_material_change(&snapshot, false);
}
static int reset_material(void)
{
web_server_snapshot_t snapshot;
bool was_running = web_server_get_snapshot(&snapshot) == ESP_OK && snapshot.running;
esp_err_t error = web_server_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("Could not inspect HTTPS runtime: %s\n", esp_err_to_name(error));
return 1;
}
web_security_credentials_t credentials;
esp_err_t error = web_security_reset_all(&credentials);
error = web_security_reset_all(&credentials);
if (error != ESP_OK) {
printf("Could not reset web security material: %s\n", esp_err_to_name(error));
return 1;
@@ -320,18 +444,7 @@ static int reset_material(void)
(int)credentials.username_length, credentials.username,
(int)credentials.password_length, credentials.password);
secure_wipe(&credentials, sizeof(credentials));
if (was_running) {
return restart_if_running(true);
}
error = web_server_start();
if (error != ESP_OK) {
printf("Security material recovered, but HTTPS could not start: %s\n",
esp_err_to_name(error));
return 1;
}
printf("HTTPS started with the recovered security material.\n");
return 0;
return refresh_tls_after_material_change(&snapshot, true);
}
static int command_web(int argc, char **argv)
@@ -353,6 +466,23 @@ static int command_web(int argc, char **argv)
return 0;
}
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
if (admin_ssh_console_dispatch_frontend() ==
ADMIN_SSH_CONSOLE_FRONTEND_WEB) {
web_server_snapshot_t snapshot;
esp_err_t error = web_server_get_snapshot(&snapshot);
if (error == ESP_OK) {
error = admin_ssh_console_dispatch_defer(
ADMIN_SSH_DEFER_WEB_STOP,
snapshot.lifecycle_generation);
}
if (error != ESP_OK) {
printf("Could not schedule HTTPS stop: %s\n",
esp_err_to_name(error));
return 1;
}
printf("HTTPS stop scheduled after administrative output drains.\n");
return 0;
}
esp_err_t error = web_server_stop();
if (error != ESP_OK) {
printf("Could not stop HTTPS: %s\n", esp_err_to_name(error));
@@ -369,6 +499,9 @@ static int command_web(int argc, char **argv)
if (error == ESP_OK) {
error = web_serial_transport_clear_counters();
}
if (error == ESP_OK) {
error = web_admin_transport_clear_counters();
}
if (error != ESP_OK) {
printf("Could not clear web counters: %s\n", esp_err_to_name(error));
return 1;
+226 -60
View File
@@ -49,6 +49,7 @@ typedef struct {
uint8_t digest[WEB_SERIAL_SHA256_BYTES];
int64_t expires_at_us;
user_principal_t principal;
web_session_ref_t session_reference;
bool active;
} web_serial_ticket_t;
@@ -68,6 +69,7 @@ typedef struct web_serial_slot {
uint32_t generation;
session_broker_client_id_t broker_client_id;
user_principal_t principal;
web_session_ref_t session_reference;
int64_t next_currentness_check_us;
bool writer;
bool hello_pending;
@@ -145,6 +147,8 @@ static void clear_ticket_locked(web_serial_ticket_t *ticket)
{
secure_wipe(ticket->digest, sizeof(ticket->digest));
secure_wipe(&ticket->principal, sizeof(ticket->principal));
secure_wipe(&ticket->session_reference,
sizeof(ticket->session_reference));
ticket->expires_at_us = 0;
ticket->active = false;
}
@@ -177,6 +181,40 @@ static bool constant_time_equal(const uint8_t *left, const uint8_t *right,
return difference == 0U;
}
static bool principal_equal(const user_principal_t *left,
const user_principal_t *right)
{
if (left->username_length > USER_DATABASE_USERNAME_CAPACITY ||
right->username_length > USER_DATABASE_USERNAME_CAPACITY) {
return false;
}
return left->user_id == right->user_id &&
left->auth_generation == right->auth_generation &&
left->role == right->role && left->method == right->method &&
left->username_length == right->username_length &&
memcmp(left->username, right->username, left->username_length) == 0;
}
static bool session_reference_equal(const web_session_ref_t *left,
const web_session_ref_t *right)
{
return web_session_ref_valid(left) && web_session_ref_valid(right) &&
left->slot_index == right->slot_index &&
left->generation == right->generation;
}
static esp_err_t authentication_binding_is_current(
const user_principal_t *principal,
const web_session_ref_t *session_reference, bool *current)
{
if (principal == NULL || !web_session_ref_valid(session_reference) ||
current == NULL) {
return ESP_ERR_INVALID_ARG;
}
*current = false;
return web_session_ref_is_current(session_reference, principal, current);
}
static void encode_base64url_24(const uint8_t input[WEB_SERIAL_RANDOM_BYTES],
char output[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY])
{
@@ -257,9 +295,6 @@ static esp_err_t validate_origin(httpd_req_t *request)
esp_err_t result = httpd_req_get_hdr_value_str(
request, "Origin", origin, sizeof(origin));
if (result == ESP_ERR_NOT_FOUND) {
return ESP_OK;
}
if (result != ESP_OK) {
return ESP_ERR_INVALID_ARG;
}
@@ -280,14 +315,18 @@ static esp_err_t validate_origin(httpd_req_t *request)
return matches ? ESP_OK : ESP_ERR_INVALID_ARG;
}
static esp_err_t consume_ticket(const char *ticket,
user_principal_t *principal, bool *consumed)
static esp_err_t consume_ticket(
const char *ticket, user_principal_t *principal,
web_session_ref_t *session_reference, bool *consumed)
{
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
user_principal_t candidate = {0};
web_session_ref_t candidate_reference = {0};
bool ticket_found = false;
*consumed = false;
memset(principal, 0, sizeof(*principal));
memset(session_reference, 0, sizeof(*session_reference));
esp_err_t result = sha256_ticket(ticket, digest);
if (result != ESP_OK) {
@@ -317,6 +356,7 @@ static esp_err_t consume_ticket(const char *ticket,
web_serial_ticket_t *entry = &s_tickets[matching_index];
if (entry->active && entry->expires_at_us > now_us) {
candidate = entry->principal;
candidate_reference = entry->session_reference;
clear_ticket_locked(entry);
ticket_found = true;
}
@@ -336,9 +376,11 @@ static esp_err_t consume_ticket(const char *ticket,
if (ticket_found) {
bool current = false;
result = user_database_principal_is_current(&candidate, &current);
result = authentication_binding_is_current(
&candidate, &candidate_reference, &current);
if (result == ESP_OK && current) {
*principal = candidate;
*session_reference = candidate_reference;
*consumed = true;
add_counter(&s_counters.tickets_consumed, 1U);
}
@@ -348,6 +390,7 @@ static esp_err_t consume_ticket(const char *ticket,
}
secure_wipe(&candidate, sizeof(candidate));
secure_wipe(&candidate_reference, sizeof(candidate_reference));
secure_wipe(digest, sizeof(digest));
return result;
}
@@ -371,6 +414,8 @@ static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
slot->socket_fd = socket_fd;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
secure_wipe(&slot->session_reference,
sizeof(slot->session_reference));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -396,6 +441,7 @@ static void make_slot_free_locked(web_serial_slot_t *slot)
slot->socket_fd = -1;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
secure_wipe(&slot->principal, sizeof(slot->principal));
secure_wipe(&slot->session_reference, sizeof(slot->session_reference));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -431,6 +477,9 @@ static void close_unpublished_broker_session(web_serial_slot_t *slot,
} else {
slot->state = WEB_SERIAL_SLOT_CLOSING;
slot->broker_client_id = client_id;
secure_wipe(&slot->principal, sizeof(slot->principal));
secure_wipe(&slot->session_reference,
sizeof(slot->session_reference));
++s_counters.broker_failures;
}
}
@@ -480,6 +529,8 @@ static void web_serial_session_free(void *context)
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
secure_wipe(&slot->session_reference,
sizeof(slot->session_reference));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -518,6 +569,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
uint32_t slot_generation = 0U;
web_serial_slot_t *slot = NULL;
user_principal_t principal = {0};
web_session_ref_t session_reference = {0};
bool consumed = false;
esp_err_t result;
@@ -535,21 +587,22 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
result = consume_ticket(ticket, &principal, &consumed);
result = consume_ticket(ticket, &principal, &session_reference, &consumed);
if (result != ESP_OK || !consumed) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
taskENTER_CRITICAL(&s_lock);
bool principal_staged = slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
!slot->close_requested;
if (principal_staged) {
bool binding_staged = slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
!slot->close_requested;
if (binding_staged) {
slot->principal = principal;
slot->session_reference = session_reference;
}
taskEXIT_CRITICAL(&s_lock);
if (!principal_staged) {
if (!binding_staged) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
@@ -570,9 +623,10 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
}
}
bool principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
bool binding_current = false;
result = authentication_binding_is_current(
&principal, &session_reference, &binding_current);
if (result != ESP_OK || !binding_current) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
@@ -601,9 +655,10 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
binding_current = false;
result = authentication_binding_is_current(
&principal, &session_reference, &binding_current);
if (result != ESP_OK || !binding_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
@@ -624,9 +679,34 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
goto cleanup;
}
principal_current = false;
result = user_database_principal_is_current(&principal, &principal_current);
if (result != ESP_OK || !principal_current) {
binding_current = false;
result = authentication_binding_is_current(
&principal, &session_reference, &binding_current);
if (result != ESP_OK || !binding_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
bool staged_current;
taskENTER_CRITICAL(&s_lock);
staged_current = slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
!slot->close_requested &&
principal_equal(&slot->principal, &principal) &&
session_reference_equal(&slot->session_reference,
&session_reference);
taskEXIT_CRITICAL(&s_lock);
if (!staged_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
binding_current = false;
result = authentication_binding_is_current(
&principal, &session_reference, &binding_current);
if (result != ESP_OK || !binding_current) {
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
@@ -642,6 +722,7 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
slot->state = WEB_SERIAL_SLOT_ACTIVE;
slot->broker_client_id = client_id;
slot->principal = principal;
slot->session_reference = session_reference;
slot->next_currentness_check_us = next_currentness_check_us;
slot->writer = writer;
slot->hello_pending = true;
@@ -665,14 +746,15 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
cleanup:
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
secure_wipe(ticket, sizeof(ticket));
return result;
}
static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slot_out,
uint32_t *generation,
session_broker_client_id_t *client_id,
user_principal_t *principal)
static bool capture_active_session(
httpd_req_t *request, web_serial_slot_t **slot_out, uint32_t *generation,
session_broker_client_id_t *client_id, user_principal_t *principal,
web_session_ref_t *session_reference)
{
web_serial_slot_t *slot = request->sess_ctx;
int socket_fd = httpd_req_to_sockfd(request);
@@ -692,6 +774,7 @@ static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slo
*generation = slot->generation;
*client_id = slot->broker_client_id;
*principal = slot->principal;
*session_reference = slot->session_reference;
}
taskEXIT_CRITICAL(&s_lock);
return valid;
@@ -803,8 +886,9 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
uint32_t generation = 0U;
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
user_principal_t principal = {0};
web_session_ref_t session_reference = {0};
if (!capture_active_session(request, &slot, &generation, &client_id,
&principal)) {
&principal, &session_reference)) {
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
@@ -813,6 +897,7 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
if (httpd_ws_get_fd_info(request->handle, socket_fd) !=
HTTPD_WS_CLIENT_WEBSOCKET) {
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
return reject_protocol_frame(slot, generation, 0U);
}
@@ -820,6 +905,7 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
esp_err_t result = httpd_ws_recv_frame(request, &frame, 0U);
if (result != ESP_OK) {
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
return reject_protocol_frame(slot, generation, frame.len);
}
if (!frame.final || frame.type == HTTPD_WS_TYPE_CONTINUE ||
@@ -827,6 +913,7 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
(frame.type != HTTPD_WS_TYPE_BINARY &&
frame.type != HTTPD_WS_TYPE_TEXT)) {
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
return reject_protocol_frame(slot, generation, frame.len);
}
@@ -834,14 +921,16 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
result = httpd_ws_recv_frame(request, &frame, sizeof(slot->rx_data));
if (result != ESP_OK) {
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
return reject_protocol_frame(slot, generation, frame.len);
}
bool current = false;
esp_err_t currentness_result =
user_database_principal_is_current(&principal, &current);
secure_wipe(&principal, sizeof(principal));
esp_err_t currentness_result = authentication_binding_is_current(
&principal, &session_reference, &current);
if (currentness_result != ESP_OK || !current) {
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
request_handler_close(slot, generation);
return ESP_FAIL;
}
@@ -852,8 +941,13 @@ static esp_err_t process_websocket_frame(httpd_req_t *request)
slot->generation == generation &&
slot->broker_client_id == client_id &&
slot->server == request->handle &&
slot->server == s_server;
slot->server == s_server &&
principal_equal(&slot->principal, &principal) &&
session_reference_equal(&slot->session_reference,
&session_reference);
taskEXIT_CRITICAL(&s_lock);
secure_wipe(&principal, sizeof(principal));
secure_wipe(&session_reference, sizeof(session_reference));
if (!still_active) {
return ESP_FAIL;
}
@@ -1198,6 +1292,8 @@ static void process_close_request(web_serial_slot_t *slot)
if (session_gone) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
secure_wipe(&slot->session_reference,
sizeof(slot->session_reference));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -1250,9 +1346,10 @@ static void process_broker_disconnect(web_serial_slot_t *slot)
taskEXIT_CRITICAL(&s_lock);
}
static void process_principal_currentness(web_serial_slot_t *slot)
static void process_authentication_currentness(web_serial_slot_t *slot)
{
user_principal_t principal = {0};
web_session_ref_t session_reference = {0};
uint32_t generation = 0U;
bool check = false;
int64_t now_us = monotonic_time_us();
@@ -1262,6 +1359,7 @@ static void process_principal_currentness(web_serial_slot_t *slot)
slot->next_currentness_check_us <= now_us) {
generation = slot->generation;
principal = slot->principal;
session_reference = slot->session_reference;
slot->next_currentness_check_us =
now_us + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
check = true;
@@ -1272,18 +1370,20 @@ static void process_principal_currentness(web_serial_slot_t *slot)
}
bool current = false;
esp_err_t result = user_database_principal_is_current(&principal, &current);
esp_err_t result = authentication_binding_is_current(
&principal, &session_reference, &current);
if (result != ESP_OK || !current) {
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE &&
slot->generation == generation &&
session_reference_equal(&slot->session_reference,
&session_reference)) {
slot->close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
}
secure_wipe(&principal, sizeof(principal));
if (result == ESP_OK && current) {
return;
}
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE &&
slot->generation == generation) {
slot->close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
secure_wipe(&session_reference, sizeof(session_reference));
}
static void process_active_output(web_serial_slot_t *slot)
@@ -1343,7 +1443,7 @@ static void transport_task(void *context)
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
++index) {
web_serial_slot_t *slot = &s_slots[index];
process_principal_currentness(slot);
process_authentication_currentness(slot);
process_close_request(slot);
process_broker_disconnect(slot);
process_active_output(slot);
@@ -1456,6 +1556,8 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
secure_wipe(&slot->principal, sizeof(slot->principal));
secure_wipe(&slot->session_reference,
sizeof(slot->session_reference));
slot->next_currentness_check_us = 0;
slot->writer = false;
slot->hello_pending = false;
@@ -1495,7 +1597,7 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
if (quiescent) {
break;
}
if (operations_done && monotonic_time_us() >= detach_deadline) {
if (monotonic_time_us() >= detach_deadline) {
session_broker_client_id_t writer_id =
session_broker_get_writer_id();
bool web_writer = false;
@@ -1509,11 +1611,14 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
break;
}
}
bool httpd_close_idle = s_httpd_close_operations == 0U;
taskEXIT_CRITICAL(&s_lock);
if (web_writer) {
(void)session_broker_force_writer(SESSION_BROKER_NO_CLIENT);
(void)session_broker_force_release_writer(writer_id);
}
result = ESP_ERR_TIMEOUT;
/* HTTPD must remain alive around permanent-task API calls. */
result = httpd_close_idle ? ESP_ERR_TIMEOUT
: ESP_ERR_INVALID_STATE;
break;
}
notify_transport_task();
@@ -1523,17 +1628,19 @@ esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
return result;
}
esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
char *ticket, size_t capacity)
esp_err_t web_serial_transport_mint_ticket(
const user_principal_t *principal,
const web_session_ref_t *session_reference, char *ticket, size_t capacity)
{
if (principal == NULL || ticket == NULL ||
capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
if (principal == NULL || !web_session_ref_valid(session_reference) ||
ticket == NULL || capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
return ESP_ERR_INVALID_ARG;
}
ticket[0] = '\0';
bool current = false;
esp_err_t result = user_database_principal_is_current(principal, &current);
esp_err_t result = authentication_binding_is_current(
principal, session_reference, &current);
if (result != ESP_OK) {
return result;
}
@@ -1553,25 +1660,41 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
goto cleanup;
}
current = false;
result = authentication_binding_is_current(
principal, session_reference, &current);
if (result != ESP_OK || !current) {
ticket[0] = '\0';
if (result == ESP_OK) {
result = ESP_ERR_INVALID_STATE;
}
goto cleanup;
}
int64_t now_us = monotonic_time_us();
bool stored = false;
taskENTER_CRITICAL(&s_lock);
if (s_initialized && s_server != NULL) {
purge_tickets_locked(now_us);
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
int64_t oldest_expiry = INT64_MAX;
size_t free_slot = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS;
++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (!entry->active) {
if (entry->active && session_reference_equal(
&entry->session_reference,
session_reference)) {
selected = index;
break;
}
if (entry->expires_at_us < oldest_expiry) {
oldest_expiry = entry->expires_at_us;
selected = index;
if (!entry->active &&
free_slot == WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
free_slot = index;
}
}
if (selected == WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
selected = free_slot;
}
if (selected < WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
web_serial_ticket_t *entry = &s_tickets[selected];
clear_ticket_locked(entry);
@@ -1580,6 +1703,7 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
now_us + (int64_t)WEB_SERIAL_TRANSPORT_TICKET_LIFETIME_SECONDS *
1000000LL;
entry->principal = *principal;
entry->session_reference = *session_reference;
entry->active = true;
++s_counters.tickets_issued;
stored = true;
@@ -1599,9 +1723,11 @@ cleanup:
}
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request, const user_principal_t *principal)
httpd_req_t *request, const user_principal_t *principal,
const web_session_ref_t *session_reference)
{
if (request == NULL || principal == NULL) {
if (request == NULL || principal == NULL ||
!web_session_ref_valid(session_reference)) {
return ESP_ERR_INVALID_ARG;
}
if (request->method != HTTP_POST || request->content_len != 0U ||
@@ -1620,7 +1746,7 @@ esp_err_t web_serial_transport_handle_authenticated_ticket_request(
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
char response[WEB_SERIAL_TICKET_RESPONSE_CAPACITY];
esp_err_t result = web_serial_transport_mint_ticket(
principal, ticket, sizeof(ticket));
principal, session_reference, ticket, sizeof(ticket));
if (result != ESP_OK) {
secure_wipe(ticket, sizeof(ticket));
return result;
@@ -1651,6 +1777,7 @@ esp_err_t web_serial_transport_handle_authenticated_ticket_request(
return result;
}
esp_err_t web_serial_transport_ws_handler(httpd_req_t *request)
{
if (request == NULL || request->handle == NULL) {
@@ -1762,6 +1889,44 @@ esp_err_t web_serial_transport_clear_counters(void)
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_session(
const web_session_ref_t *session_reference)
{
if (!web_session_ref_valid(session_reference)) {
return ESP_ERR_INVALID_ARG;
}
bool notify = false;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *ticket = &s_tickets[index];
if (ticket->active && session_reference_equal(
&ticket->session_reference,
session_reference)) {
clear_ticket_locked(ticket);
}
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
web_serial_slot_t *slot = &s_slots[index];
if ((slot->state == WEB_SERIAL_SLOT_RESERVED ||
slot->state == WEB_SERIAL_SLOT_ACTIVE) &&
session_reference_equal(&slot->session_reference,
session_reference)) {
slot->close_requested = true;
notify = true;
}
}
taskEXIT_CRITICAL(&s_lock);
if (notify) {
notify_transport_task();
}
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
size_t username_length)
{
@@ -1810,7 +1975,8 @@ esp_err_t web_serial_transport_revoke_sessions(void)
clear_all_tickets_locked();
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
web_serial_slot_t *slot = &s_slots[index];
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
if (slot->state == WEB_SERIAL_SLOT_RESERVED ||
slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->close_requested = true;
}
}
+18 -8
View File
@@ -11,6 +11,7 @@
#include "esp_http_server.h"
#include "session_broker.h"
#include "user_database.h"
#include "web_session.h"
#ifdef __cplusplus
extern "C" {
@@ -98,20 +99,25 @@ esp_err_t web_serial_transport_attach_server(httpd_handle_t server);
esp_err_t web_serial_transport_detach_server(httpd_handle_t server);
/*
* Mint a one-time bearer ticket bound to a current authenticated principal. The
* principal is copied; the output is exactly 32 Base64URL characters plus a
* terminator and expires after 30 monotonic seconds. Never log or persist it.
* Mint a one-time bearer ticket bound to one exact current browser login session.
* The principal and session reference are copied; the output is exactly 32
* Base64URL characters plus a terminator and expires after 30 monotonic seconds.
* Never log or persist the ticket.
*/
esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
char *ticket, size_t capacity);
esp_err_t web_serial_transport_mint_ticket(
const user_principal_t *principal, const web_session_ref_t *session_reference,
char *ticket, size_t capacity);
/*
* Convenience POST response helper for /api/ws-ticket. Authentication is
* intentionally outside this module: pass the principal returned by successful
* Basic authentication. Register it as HTTP_POST, not as a public handler.
* intentionally outside this module: pass the principal and exact session
* reference returned by successful cookie-session authentication. Register it as
* HTTP_POST, not as a public handler.
*/
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request, const user_principal_t *principal);
httpd_req_t *request, const user_principal_t *principal,
const web_session_ref_t *session_reference);
/*
* Handler for /ws/serial. Register as HTTP_GET with is_websocket=true and
@@ -126,6 +132,10 @@ esp_err_t web_serial_transport_get_snapshot(
/* Clearing counters does not alter tickets, sessions, ownership, or queued data. */
esp_err_t web_serial_transport_clear_counters(void);
/* Invalidate tickets and request closure for one exact browser login session. */
esp_err_t web_serial_transport_revoke_session(
const web_session_ref_t *session_reference);
/* Invalidate tickets/sessions for one account, or all authenticated sessions. */
esp_err_t web_serial_transport_revoke_user(const uint8_t *username,
size_t username_length);
+3471 -199
View File
File diff suppressed because it is too large Load Diff
+40 -1
View File
@@ -1,12 +1,14 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Authenticated HTTPS administration foundation. */
/* Session-authenticated HTTPS administration foundation. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "web_session.h"
#ifdef __cplusplus
extern "C" {
@@ -22,17 +24,39 @@ typedef struct {
uint64_t root_requests;
uint64_t status_requests;
uint64_t ticket_requests;
uint64_t admin_ticket_requests;
uint64_t admin_authorization_failures;
uint64_t admin_api_requests;
uint64_t admin_request_rejections;
uint64_t admin_operation_failures;
uint64_t writer_transfer_attempts;
uint64_t writer_transfer_successes;
uint64_t writer_transfer_conflicts;
uint64_t asset_requests;
uint64_t response_errors;
uint64_t login_requests;
uint64_t login_successes;
uint64_t login_failures;
uint64_t login_throttled;
uint64_t logout_requests;
uint64_t session_requests;
uint64_t cookie_rejections;
uint64_t origin_rejections;
uint64_t csrf_rejections;
} web_server_counters_t;
typedef struct {
bool initialized;
bool running;
bool transitioning;
bool desired_running;
uint32_t lifecycle_generation;
uint16_t port;
esp_err_t last_error;
esp_err_t serial_transport_error;
esp_err_t admin_transport_error;
uint32_t active_sessions;
web_session_counters_t session_counters;
web_server_counters_t counters;
} web_server_snapshot_t;
@@ -42,10 +66,25 @@ esp_err_t web_server_init(void);
/* Start one TLS-only server on all active network interfaces. */
esp_err_t web_server_start(void);
esp_err_t web_server_stop(void);
/*
* Stop only while expected_lifecycle_generation still names the latest
* explicit HTTPS intent. A newer start/stop/refresh wins and returns
* ESP_ERR_INVALID_STATE without changing server state.
*/
esp_err_t web_server_stop_if_generation(uint32_t expected_lifecycle_generation);
/*
* Apply current TLS material to a live server. If it is stopped, start only
* when requested and no newer explicit lifecycle intent superseded the caller.
*/
esp_err_t web_server_refresh_tls(uint32_t expected_lifecycle_generation,
bool start_if_unchanged);
esp_err_t web_server_get_snapshot(web_server_snapshot_t *snapshot);
esp_err_t web_server_clear_counters(void);
/* Revoke one account's browser sessions and both WebSocket transports. */
esp_err_t web_server_revoke_user(const uint8_t *username, size_t username_length);
#ifdef __cplusplus
}
#endif
+1097
View File
File diff suppressed because it is too large Load Diff
+156
View File
@@ -0,0 +1,156 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded opaque browser sessions with principal and CSRF validation. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "user_database.h"
#ifdef __cplusplus
extern "C" {
#endif
#define WEB_SESSION_MAX_SESSIONS 8U
#define WEB_SESSION_MAX_SESSIONS_PER_ACCOUNT 2U
#define WEB_SESSION_BASE64URL_SOURCE_LENGTH 24U
#define WEB_SESSION_TOKEN_RANDOM_LENGTH WEB_SESSION_BASE64URL_SOURCE_LENGTH
#define WEB_SESSION_TOKEN_LENGTH 32U
#define WEB_SESSION_TOKEN_CAPACITY (WEB_SESSION_TOKEN_LENGTH + 1U)
#define WEB_SESSION_TOKEN_DIGEST_LENGTH 32U
#define WEB_SESSION_CSRF_KEY_LENGTH 32U
#define WEB_SESSION_HMAC_BLOCK_LENGTH 64U
#define WEB_SESSION_HMAC_DIGEST_LENGTH 32U
#define WEB_SESSION_CSRF_SOURCE_LENGTH WEB_SESSION_BASE64URL_SOURCE_LENGTH
#define WEB_SESSION_CSRF_TOKEN_LENGTH 32U
#define WEB_SESSION_CSRF_TOKEN_CAPACITY (WEB_SESSION_CSRF_TOKEN_LENGTH + 1U)
#define WEB_SESSION_LIFETIME_SECONDS (8U * 60U * 60U)
#define WEB_SESSION_LIFETIME_US \
((int64_t)WEB_SESSION_LIFETIME_SECONDS * 1000000LL)
#define WEB_SESSION_ERR_CAPACITY ESP_ERR_NO_MEM
typedef struct {
uint8_t slot_index;
uint32_t generation;
} web_session_ref_t;
typedef struct {
uint64_t created;
uint64_t create_failures;
uint64_t capacity_failures;
uint64_t authenticated;
uint64_t rejected;
uint64_t expired;
uint64_t stale_principal;
uint64_t destroyed;
uint64_t revocations;
uint64_t csrf_accepted;
uint64_t csrf_rejected;
} web_session_counters_t;
typedef struct {
bool initialized;
uint32_t active_sessions;
web_session_counters_t counters;
} web_session_snapshot_t;
/*
* Generate the boot-local CSRF key and initialize the fixed session table.
* secure_random_init() and user_database_init() must already have succeeded.
* Repeated calls after successful initialization return ESP_OK without changing
* sessions, the boot-local key, or counters.
*/
esp_err_t web_session_init(void);
/*
* Authenticate bounded username/password input and create a fixed eight-hour
* session. On success, session_token contains exactly 32 Base64URL characters
* plus a terminator and principal is a copied secret-free value. The caller must send
* the session token only through a suitably protected host-only cookie and must
* never log either token. Invalid credentials return ESP_OK with created=false;
* a full table of current, unexpired sessions returns WEB_SESSION_ERR_CAPACITY.
* At most two sessions are retained per account; a later login replaces that
* account's oldest session. All secret-bearing outputs are cleared on failure.
*/
esp_err_t web_session_create(
const uint8_t *username, size_t username_length,
const uint8_t *password, size_t password_length,
char *session_token, size_t session_token_capacity,
user_principal_t *principal, bool *created);
/*
* Authenticate an exact length-delimited session token. Missing, malformed,
* expired, destroyed, or unknown tokens return ESP_OK with authenticated=false.
* Database/currentness failures fail closed and are returned to the caller.
*/
esp_err_t web_session_authenticate(const char *session_token,
size_t session_token_length,
user_principal_t *principal,
bool *authenticated);
/* A syntactically valid reference names a bounded slot and nonzero generation. */
bool web_session_ref_valid(const web_session_ref_t *reference);
/*
* Authenticate exactly as web_session_authenticate() and additionally return the
* reference of that exact live browser login session. reference is cleared unless
* authenticated is true. No raw session token is retained.
*/
esp_err_t web_session_get_reference(
const char *session_token, size_t session_token_length,
user_principal_t *principal, web_session_ref_t *reference,
bool *authenticated);
/*
* Check that reference still names the same live session carrying principal and
* that the copied principal remains current in the user database. The database
* check is performed without holding the browser-session mutex, followed by
* locked reference revalidation. Database failures fail closed.
*/
esp_err_t web_session_ref_is_current(const web_session_ref_t *reference,
const user_principal_t *principal,
bool *current);
/*
* Authenticate a session and reproduce its deterministic boot-local CSRF token.
* This supports rendering a fresh page without storing separate per-session CSRF
* material. csrf_token is cleared unless authenticated is true.
*/
esp_err_t web_session_copy_csrf_token(
const char *session_token, size_t session_token_length,
char *csrf_token, size_t csrf_token_capacity,
user_principal_t *principal, bool *authenticated);
/*
* Authenticate the session and compare an exact length-delimited CSRF token in
* constant time. accepted is true only when both the current session and its
* session-bound CSRF token are valid. The copied principal is cleared otherwise.
*/
esp_err_t web_session_authenticate_csrf(
const char *session_token, size_t session_token_length,
const char *csrf_token, size_t csrf_token_length,
user_principal_t *principal, bool *accepted);
/* Explicitly destroy every entry matching one opaque token digest. */
esp_err_t web_session_destroy(const char *session_token,
size_t session_token_length,
bool *destroyed);
/* Revoke sessions for one bounded username, or every browser session. */
esp_err_t web_session_revoke_username(const uint8_t *username,
size_t username_length);
esp_err_t web_session_revoke_all(void);
/* The snapshot contains no token, digest, CSRF key, expiry, or principal data. */
esp_err_t web_session_get_snapshot(web_session_snapshot_t *snapshot);
/* Clearing counters never changes live sessions or the boot-local CSRF key. */
esp_err_t web_session_clear_counters(void);
#ifdef __cplusplus
}
#endif
+1169 -59
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Offline browser UI response helpers for authenticated HTTPS routes. */
/* Offline browser UI response helpers for public and session-authenticated HTTPS routes. */
#pragma once
@@ -12,6 +12,8 @@ extern "C" {
typedef enum {
WEB_UI_RESOURCE_INDEX = 0,
WEB_UI_RESOURCE_LOGIN,
WEB_UI_RESOURCE_LOGIN_ERROR,
WEB_UI_RESOURCE_XTERM_JS,
WEB_UI_RESOURCE_XTERM_CSS,
WEB_UI_RESOURCE_ADDON_FIT_JS,
@@ -20,8 +22,9 @@ typedef enum {
} web_ui_resource_t;
/*
* Send one UI resource after the caller has authenticated the request.
* This module deliberately performs no authentication or URI dispatch.
* Send one UI resource after the caller has applied the route's public or
* session-authenticated access policy. This module deliberately performs no
* authentication, authorization, or URI dispatch.
*/
esp_err_t web_ui_send_response(httpd_req_t *request,
web_ui_resource_t resource);
-13
View File
@@ -352,17 +352,4 @@ esp_err_t wifi_config_save(const wifi_app_config_t *config)
return err;
}
esp_err_t wifi_config_reset_storage(const wifi_app_config_t *defaults)
{
if (defaults != NULL) {
return wifi_config_save(defaults);
}
wifi_app_config_t generated_defaults;
esp_err_t err = wifi_config_defaults(&generated_defaults);
if (err == ESP_OK) {
err = wifi_config_save(&generated_defaults);
}
wifi_config_secure_wipe(&generated_defaults, sizeof(generated_defaults));
return err;
}
-2
View File
@@ -104,8 +104,6 @@ esp_err_t wifi_config_load(wifi_app_config_t *config,
wifi_config_load_source_t *source);
esp_err_t wifi_config_save(const wifi_app_config_t *config);
/* Pass NULL to generate fresh defaults, or supply validated defaults to save. */
esp_err_t wifi_config_reset_storage(const wifi_app_config_t *defaults);
/* Compatibility wrapper around the shared secure_wipe() implementation. */
void wifi_config_secure_wipe(void *data, size_t size);
+84 -40
View File
@@ -120,13 +120,16 @@ static void print_ipv4(uint32_t address)
static int show_status(void)
{
wifi_manager_snapshot_t snapshot;
wifi_app_config_t config;
wifi_manager_snapshot_t snapshot = {0};
wifi_app_config_t config = {0};
uint32_t config_generation = 0U;
esp_err_t error = wifi_manager_get_snapshot(&snapshot);
if (error == ESP_OK) {
error = wifi_manager_get_working_config(&config);
error = wifi_manager_get_working_config_versioned(
&config, &config_generation);
}
if (error != ESP_OK) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Wi-Fi manager unavailable: %s\n", esp_err_to_name(error));
return 1;
}
@@ -136,7 +139,7 @@ static int show_status(void)
config.enabled_at_boot ? "yes" : "no",
snapshot.started ? "yes" : "no",
wifi_manager_state_to_string(snapshot.state),
snapshot.config_generation);
config_generation);
if (snapshot.active_profile >= 0) {
printf("STA: profile=%d SSID=", snapshot.active_profile);
@@ -177,7 +180,7 @@ static int show_status(void)
static int show_profiles(void)
{
wifi_app_config_t config;
wifi_app_config_t config = {0};
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read Wi-Fi profiles: %s\n", esp_err_to_name(error));
@@ -231,9 +234,12 @@ static int show_counters(void)
return 0;
}
static esp_err_t apply_candidate(wifi_app_config_t *candidate)
static esp_err_t apply_candidate(wifi_app_config_t *candidate,
uint32_t expected_generation,
uint32_t *resulting_generation)
{
esp_err_t error = wifi_manager_apply_working_config(candidate);
esp_err_t error = wifi_manager_compare_exchange_working_config(
candidate, expected_generation, resulting_generation);
wifi_config_secure_wipe(candidate, sizeof(*candidate));
return error;
}
@@ -270,9 +276,12 @@ static int set_profile(char **argv)
return 1;
}
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&config, &expected_generation);
if (error != ESP_OK) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
@@ -283,7 +292,7 @@ static int set_profile(char **argv)
profile->ssid_len = (uint8_t)ssid_len;
profile->priority = (uint8_t)priority;
profile->security = security;
error = apply_candidate(&config);
error = apply_candidate(&config, expected_generation, NULL);
if (error != ESP_OK) {
printf("Could not apply profile: %s\n", esp_err_to_name(error));
return 1;
@@ -302,9 +311,12 @@ static int set_profile_secret(const char *slot_text)
return 1;
}
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&config, &expected_generation);
if (error != ESP_OK) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
@@ -317,7 +329,7 @@ static int set_profile_secret(const char *slot_text)
error = read_secret_no_echo(config.profiles[slot].psk,
&config.profiles[slot].psk_len);
if (error == ESP_OK) {
error = apply_candidate(&config);
error = apply_candidate(&config, expected_generation, NULL);
} else {
wifi_config_secure_wipe(&config, sizeof(config));
}
@@ -341,9 +353,12 @@ static int change_profile_state(const char *operation, const char *slot_text)
return 1;
}
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&config, &expected_generation);
if (error != ESP_OK) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
@@ -355,7 +370,7 @@ static int change_profile_state(const char *operation, const char *slot_text)
profile->enabled = strcmp(operation, "enable") == 0 ? 1U : 0U;
}
error = apply_candidate(&config);
error = apply_candidate(&config, expected_generation, NULL);
if (error != ESP_OK) {
printf("Could not %s profile %u: %s\n", operation,
(unsigned int)slot, esp_err_to_name(error));
@@ -368,9 +383,12 @@ static int change_profile_state(const char *operation, const char *slot_text)
static int set_ap_parameter(const char *parameter, const char *value)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&config, &expected_generation);
if (error != ESP_OK) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
@@ -403,7 +421,7 @@ static int set_ap_parameter(const char *parameter, const char *value)
config.ap_channel = (uint8_t)channel;
}
error = apply_candidate(&config);
error = apply_candidate(&config, expected_generation, NULL);
if (error != ESP_OK) {
printf("Could not apply AP configuration: %s\n", esp_err_to_name(error));
return 1;
@@ -414,16 +432,19 @@ static int set_ap_parameter(const char *parameter, const char *value)
static int set_ap_secret(void)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&config, &expected_generation);
if (error != ESP_OK) {
wifi_config_secure_wipe(&config, sizeof(config));
printf("Could not read working configuration: %s\n", esp_err_to_name(error));
return 1;
}
error = read_secret_no_echo(config.ap_psk, &config.ap_psk_len);
if (error == ESP_OK) {
error = apply_candidate(&config);
error = apply_candidate(&config, expected_generation, NULL);
} else {
wifi_config_secure_wipe(&config, sizeof(config));
}
@@ -439,7 +460,7 @@ static int set_ap_secret(void)
static int show_ap_secret(void)
{
wifi_app_config_t config;
wifi_app_config_t config = {0};
esp_err_t error = wifi_manager_get_working_config(&config);
if (error != ESP_OK) {
printf("Could not read AP secret: %s\n", esp_err_to_name(error));
@@ -456,12 +477,15 @@ static int show_ap_secret(void)
static int save_config(void)
{
wifi_app_config_t config;
esp_err_t error = wifi_manager_get_working_config(&config);
if (error == ESP_OK) {
error = wifi_config_save(&config);
}
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&config, &expected_generation);
wifi_config_secure_wipe(&config, sizeof(config));
if (error == ESP_OK) {
error = wifi_manager_save_working_config_if_generation(
expected_generation);
}
if (error != ESP_OK) {
printf("Could not save Wi-Fi configuration: %s\n", esp_err_to_name(error));
return 1;
@@ -472,11 +496,19 @@ static int save_config(void)
static int load_config(void)
{
wifi_app_config_t config;
wifi_config_load_source_t source;
esp_err_t error = wifi_config_load(&config, &source);
wifi_app_config_t current = {0};
wifi_app_config_t config = {0};
uint32_t expected_generation = 0U;
wifi_config_load_source_t source = WIFI_CONFIG_LOAD_GENERATED_MISSING;
esp_err_t error = wifi_manager_get_working_config_versioned(
&current, &expected_generation);
wifi_config_secure_wipe(&current, sizeof(current));
if (error == ESP_OK) {
error = wifi_manager_apply_working_config(&config);
error = wifi_config_load(&config, &source);
}
if (error == ESP_OK) {
error = wifi_manager_compare_exchange_working_config(
&config, expected_generation, NULL);
}
wifi_config_secure_wipe(&config, sizeof(config));
if (error != ESP_OK) {
@@ -494,20 +526,32 @@ static int load_config(void)
static int apply_defaults(bool persist)
{
wifi_app_config_t previous;
wifi_app_config_t defaults;
esp_err_t error = wifi_manager_get_working_config(&previous);
wifi_app_config_t previous = {0};
wifi_app_config_t defaults = {0};
uint32_t expected_generation = 0U;
uint32_t defaults_generation = 0U;
esp_err_t error = wifi_manager_get_working_config_versioned(
&previous, &expected_generation);
if (error == ESP_OK) {
error = wifi_config_defaults(&defaults);
}
if (error == ESP_OK) {
error = wifi_manager_apply_working_config(&defaults);
error = wifi_manager_compare_exchange_working_config(
&defaults, expected_generation, &defaults_generation);
}
if (error == ESP_OK && persist) {
error = wifi_config_reset_storage(&defaults);
if (error != ESP_OK) {
/* Restore RAM behavior if persistence failed. */
(void)wifi_manager_apply_working_config(&previous);
error = wifi_manager_save_working_config_if_generation(
defaults_generation);
if (error != ESP_OK && error != WIFI_MANAGER_ERR_CONFIG_CONFLICT) {
/* Restore RAM behavior only if no newer writer has won. */
esp_err_t rollback_error =
wifi_manager_compare_exchange_working_config(
&previous, defaults_generation, NULL);
if (rollback_error != ESP_OK &&
rollback_error != WIFI_MANAGER_ERR_CONFIG_CONFLICT) {
printf("Warning: could not restore the prior RAM configuration: %s\n",
esp_err_to_name(rollback_error));
}
}
}
+137 -19
View File
@@ -94,6 +94,8 @@ typedef struct {
} manager_runtime_t;
static SemaphoreHandle_t s_mutex;
/* Serializes every mutation of s_shared.config, including persistence. */
static SemaphoreHandle_t s_config_writer_mutex;
static QueueHandle_t s_queue;
static TaskHandle_t s_task;
static esp_netif_t *s_sta_netif;
@@ -130,6 +132,16 @@ static void unlock_shared(void)
(void)xSemaphoreGive(s_mutex);
}
static void lock_config_writer(void)
{
(void)xSemaphoreTake(s_config_writer_mutex, portMAX_DELAY);
}
static void unlock_config_writer(void)
{
(void)xSemaphoreGive(s_config_writer_mutex);
}
static bool manager_is_started(void)
{
bool started;
@@ -1277,10 +1289,15 @@ static void cleanup_failed_init(bool wifi_initialized, bool wifi_handler_registe
vQueueDelete(s_queue);
s_queue = NULL;
}
if (s_config_writer_mutex != NULL) {
vSemaphoreDelete(s_config_writer_mutex);
s_config_writer_mutex = NULL;
}
if (s_mutex != NULL) {
vSemaphoreDelete(s_mutex);
s_mutex = NULL;
}
wifi_config_secure_wipe(&s_shared, sizeof(s_shared));
s_task = NULL;
}
@@ -1290,7 +1307,7 @@ esp_err_t wifi_manager_init(const wifi_app_config_t *config)
if (error != ESP_OK) {
return error;
}
if (s_mutex != NULL) {
if (s_mutex != NULL || s_config_writer_mutex != NULL) {
return ESP_ERR_INVALID_STATE;
}
@@ -1298,6 +1315,11 @@ esp_err_t wifi_manager_init(const wifi_app_config_t *config)
if (s_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
s_config_writer_mutex = xSemaphoreCreateMutex();
if (s_config_writer_mutex == NULL) {
cleanup_failed_init(false, false, false, false);
return ESP_ERR_NO_MEM;
}
s_queue = xQueueCreate(WIFI_MANAGER_QUEUE_LENGTH, sizeof(manager_message_t));
if (s_queue == NULL) {
cleanup_failed_init(false, false, false, false);
@@ -1401,30 +1423,51 @@ esp_err_t wifi_manager_init(const wifi_app_config_t *config)
return ESP_OK;
}
esp_err_t wifi_manager_get_working_config(wifi_app_config_t *config)
esp_err_t wifi_manager_get_working_config_versioned(wifi_app_config_t *config,
uint32_t *generation)
{
if (config == NULL) {
if (config != NULL) {
wifi_config_secure_wipe(config, sizeof(*config));
}
if (generation != NULL) {
*generation = 0U;
}
if (config == NULL || generation == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
copy_working_config(config);
lock_shared();
*config = s_shared.config;
*generation = s_shared.snapshot.config_generation;
unlock_shared();
return ESP_OK;
}
esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config)
esp_err_t wifi_manager_get_working_config(wifi_app_config_t *config)
{
esp_err_t error = wifi_config_validate(config);
if (error != ESP_OK) {
return error;
uint32_t generation = 0U;
return wifi_manager_get_working_config_versioned(config, &generation);
}
/* s_config_writer_mutex must be held by the caller. */
static esp_err_t apply_working_config_serialized(
const wifi_app_config_t *config, bool compare_generation,
uint32_t expected_generation, uint32_t *resulting_generation)
{
lock_shared();
if (compare_generation &&
s_shared.snapshot.config_generation != expected_generation) {
unlock_shared();
return WIFI_MANAGER_ERR_CONFIG_CONFLICT;
}
if (s_mutex == NULL) {
if (s_shared.snapshot.config_generation == UINT32_MAX) {
unlock_shared();
return ESP_ERR_INVALID_STATE;
}
lock_shared();
bool restart_radio = config_requires_radio_restart(&s_shared.config, config);
if (restart_radio) {
manager_message_t message = {.type = MESSAGE_COMMAND_APPLY};
@@ -1433,40 +1476,115 @@ esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config)
return ESP_ERR_TIMEOUT;
}
}
s_shared.config = *config;
++s_shared.snapshot.config_generation;
if (s_shared.snapshot.config_generation == 0U) {
s_shared.snapshot.config_generation = 1U;
}
s_shared.snapshot.ap_policy = config->ap_policy;
++s_shared.snapshot.counters.applies;
if (resulting_generation != NULL) {
*resulting_generation = s_shared.snapshot.config_generation;
}
unlock_shared();
return ESP_OK;
}
esp_err_t wifi_manager_compare_exchange_working_config(
const wifi_app_config_t *config, uint32_t expected_generation,
uint32_t *resulting_generation)
{
if (resulting_generation != NULL) {
*resulting_generation = 0U;
}
if (expected_generation == 0U) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t error = wifi_config_validate(config);
if (error != ESP_OK) {
return error;
}
if (s_mutex == NULL || s_config_writer_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
lock_config_writer();
error = apply_working_config_serialized(
config, true, expected_generation, resulting_generation);
unlock_config_writer();
return error;
}
esp_err_t wifi_manager_save_working_config_if_generation(
uint32_t expected_generation)
{
if (expected_generation == 0U) {
return ESP_ERR_INVALID_ARG;
}
if (expected_generation == UINT32_MAX) {
return ESP_ERR_INVALID_STATE;
}
if (s_mutex == NULL || s_config_writer_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
wifi_app_config_t config = {0};
lock_config_writer();
lock_shared();
if (s_shared.snapshot.config_generation != expected_generation) {
unlock_shared();
unlock_config_writer();
wifi_config_secure_wipe(&config, sizeof(config));
return WIFI_MANAGER_ERR_CONFIG_CONFLICT;
}
config = s_shared.config;
unlock_shared();
esp_err_t error = wifi_config_save(&config);
wifi_config_secure_wipe(&config, sizeof(config));
unlock_config_writer();
return error;
}
static esp_err_t enqueue_lifecycle_command(manager_message_type_t type,
int enabled_at_boot)
{
if (s_mutex == NULL) {
if (s_mutex == NULL || s_config_writer_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
bool writes_config = enabled_at_boot >= 0;
if (writes_config) {
lock_config_writer();
}
manager_message_t message = {.type = type};
lock_shared();
bool changes_config =
writes_config &&
s_shared.config.enabled_at_boot != (uint8_t)enabled_at_boot;
if (changes_config && s_shared.snapshot.config_generation == UINT32_MAX) {
unlock_shared();
if (writes_config) {
unlock_config_writer();
}
return ESP_ERR_INVALID_STATE;
}
if (!enqueue_message(&message)) {
unlock_shared();
if (writes_config) {
unlock_config_writer();
}
return ESP_ERR_TIMEOUT;
}
if (enabled_at_boot >= 0 &&
s_shared.config.enabled_at_boot != (uint8_t)enabled_at_boot) {
if (changes_config) {
s_shared.config.enabled_at_boot = (uint8_t)enabled_at_boot;
++s_shared.snapshot.config_generation;
if (s_shared.snapshot.config_generation == 0U) {
s_shared.snapshot.config_generation = 1U;
}
}
unlock_shared();
if (writes_config) {
unlock_config_writer();
}
return ESP_OK;
}
+34 -5
View File
@@ -72,6 +72,9 @@ typedef struct {
wifi_manager_counters_t counters;
} wifi_manager_snapshot_t;
/* A stale expected working-config generation maps cleanly to HTTP 409. */
#define WIFI_MANAGER_ERR_CONFIG_CONFLICT ESP_ERR_INVALID_VERSION
/*
* Initializes ESP-NETIF, the default event loop, both default Wi-Fi netifs,
* Wi-Fi itself, and the permanent policy task. The manager never aborts the
@@ -79,15 +82,41 @@ typedef struct {
*/
esp_err_t wifi_manager_init(const wifi_app_config_t *config);
/* Returns a copy of the RAM working configuration, including credentials. */
/*
* Returns a copy of the RAM working configuration, including credentials.
* The caller owns the returned copy and must securely wipe it after use.
*/
esp_err_t wifi_manager_get_working_config(wifi_app_config_t *config);
/*
* Replaces the RAM working configuration. Disabled-profile-only edits do not
* interrupt a running radio; changes to effective station/AP policy are
* applied asynchronously by restarting with the newest generation.
* Atomically copies the credential-bearing working configuration and the exact
* nonzero generation that identified it. Both outputs are cleared on failure;
* the caller must securely wipe config after every successful call.
*/
esp_err_t wifi_manager_apply_working_config(const wifi_app_config_t *config);
esp_err_t wifi_manager_get_working_config_versioned(wifi_app_config_t *config,
uint32_t *generation);
/*
* Atomically replace the complete validated working configuration only when
* expected_generation still identifies the current configuration. A stale
* expectation returns WIFI_MANAGER_ERR_CONFIG_CONFLICT without queueing a
* restart or changing state. resulting_generation is optional and is set to
* zero on failure. Generation exhaustion returns ESP_ERR_INVALID_STATE.
*/
esp_err_t wifi_manager_compare_exchange_working_config(
const wifi_app_config_t *config, uint32_t expected_generation,
uint32_t *resulting_generation);
/*
* Persist exactly the working configuration identified by expected_generation.
* All config writers are excluded through the NVS operation. A stale
* expectation returns WIFI_MANAGER_ERR_CONFIG_CONFLICT. Generation exhaustion
* returns ESP_ERR_INVALID_STATE. The saved generation is not incremented because
* the RAM working configuration is unchanged.
*/
esp_err_t wifi_manager_save_working_config_if_generation(
uint32_t expected_generation);
/* Lifecycle requests are asynchronous and serialized by the manager task. */
esp_err_t wifi_manager_start(void);