Phase 8D - massive refactor and admin functions / admin shell in
webinterface. Memory and cross-origin problems.
This commit is contained in:
@@ -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
@@ -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, ¤t);
|
||||
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
@@ -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
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, ¤t);
|
||||
result = authentication_binding_is_current(
|
||||
&candidate, &candidate_reference, ¤t);
|
||||
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, ¤t);
|
||||
secure_wipe(&principal, sizeof(principal));
|
||||
esp_err_t currentness_result = authentication_binding_is_current(
|
||||
&principal, &session_reference, ¤t);
|
||||
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, ¤t);
|
||||
esp_err_t result = authentication_binding_is_current(
|
||||
&principal, &session_reference, ¤t);
|
||||
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, ¤t);
|
||||
esp_err_t result = authentication_binding_is_current(
|
||||
principal, session_reference, ¤t);
|
||||
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, ¤t);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+40
-1
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(
|
||||
¤t, &expected_generation);
|
||||
wifi_config_secure_wipe(¤t, 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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user