Files
ESP32_Serial_Swiss_Army_Knife/src/web_admin_transport.c
T

607 lines
24 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* One optional admin socket. HTTPD owns IO; the canonical dispatcher owns commands. */
#include "web_admin_transport.h"
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include "admin_ssh_console.h"
#include "esp_heap_caps.h"
#include "esp_timer.h"
#include "esp_system.h"
#include "web_server.h"
#include "web_security.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "secure_random.h"
#include "web_admin_tickets.h"
#include "web_cookie_auth.h"
#include "web_httpd_adapter.h"
#define ADMIN_POLL_US 20000ULL
#define ADMIN_INPUT_TIMEOUT_US 5000000LL
#define ADMIN_DETACH_TIMEOUT_US 2000000LL
typedef struct {
uint8_t rx[WEB_ADMIN_RX_CAPACITY];
uint8_t tx[WEB_ADMIN_TX_CAPACITY];
size_t rx_length, rx_offset;
int64_t input_deadline;
} admin_payload_t;
typedef struct {
bool occupied, active, console_open, close_requested, close_triggered, sending;
int fd;
web_session_id_t session;
user_principal_t principal;
admin_ssh_console_token_t token;
} admin_slot_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static admin_slot_t s_slot;
static admin_payload_t *s_payload; /* PSRAM; touched only by HTTPD while attached. */
static esp_timer_handle_t s_timer;
static httpd_handle_t s_server;
static bool s_initialized, s_accepting, s_queued;
static unsigned s_submitting;
static uint32_t s_generation; /* Never wrap/reuse within a boot. */
static web_admin_transport_snapshot_t s_counts;
static void count(uint32_t *value, uint32_t amount)
{
taskENTER_CRITICAL(&s_lock);
*value = UINT32_MAX - *value < amount ? UINT32_MAX : *value + amount;
taskEXIT_CRITICAL(&s_lock);
}
static bool token_matches(const admin_ssh_console_token_t *token)
{
return token && s_slot.occupied && s_slot.console_open &&
token->transport == ADMIN_CONSOLE_TRANSPORT_WEB &&
token->session_id == s_slot.token.session_id &&
token->slot_generation == s_slot.token.slot_generation &&
token->slot_index == s_slot.token.slot_index;
}
static bool owner_current(const admin_ssh_console_token_t *token,
const user_principal_t *principal)
{
taskENTER_CRITICAL(&s_lock);
bool valid = token_matches(token) && s_accepting && s_slot.active && !s_slot.close_requested;
web_session_id_t id = valid ? s_slot.session : 0;
taskEXIT_CRITICAL(&s_lock);
bool current = false;
if (!valid || !principal || principal->role != USER_ROLE_ADMIN ||
web_session_store_check_principal(id, principal, &current) != ESP_OK || !current)
return false;
taskENTER_CRITICAL(&s_lock);
valid = token_matches(token) && s_accepting && s_slot.active &&
!s_slot.close_requested && s_slot.session == id;
taskEXIT_CRITICAL(&s_lock);
return valid;
}
static bool owner_drained(const admin_ssh_console_token_t *token)
{
taskENTER_CRITICAL(&s_lock);
bool drained = token_matches(token) && s_slot.active &&
!s_slot.close_requested && !s_slot.sending;
taskEXIT_CRITICAL(&s_lock);
return drained;
}
static esp_err_t owner_perform(const admin_ssh_console_token_t *token,
admin_ssh_deferred_action_type_t action, uint32_t argument)
{
(void)argument;
if (action != ADMIN_CONSOLE_DEFER_SELF_CLOSE && action != ADMIN_SSH_DEFER_REBOOT &&
action != ADMIN_CONSOLE_DEFER_WEB_STOP &&
action != ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE) return ESP_ERR_NOT_SUPPORTED;
/* Drain is only a delivery heuristic, not an authorization lease. The
* execution task must recheck cookie/account binding after delay/queueing. */
taskENTER_CRITICAL(&s_lock);
user_principal_t principal = s_slot.principal;
taskEXIT_CRITICAL(&s_lock);
bool current = owner_current(token, &principal);
secure_wipe(&principal, sizeof(principal));
if (!current) return ESP_ERR_NOT_FOUND;
if (action == ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE) {
/* The owner mask routes this crypto/NVS path to the 12KiB dispatcher.
* Commit before restart; a failed stop retains HTTPD ownership and must
* not be followed by start. No socket IO or console handler calls here. */
bool committed = false;
return web_server_replace_identity(0, 0, false, &committed);
}
if (action == ADMIN_CONSOLE_DEFER_WEB_STOP) return web_server_stop();
if (action == ADMIN_SSH_DEFER_REBOOT) {
esp_restart();
return ESP_OK;
}
taskENTER_CRITICAL(&s_lock);
bool valid = token_matches(token) && s_slot.active && s_accepting && !s_slot.close_requested;
if (valid) s_slot.close_requested = true;
taskEXIT_CRITICAL(&s_lock);
if (valid) admin_ssh_console_close(token);
return valid ? ESP_OK : ESP_ERR_NOT_FOUND;
}
static const admin_console_owner_t s_owner = {
.supported_actions = (1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE) |
(1U << ADMIN_SSH_DEFER_REBOOT) | (1U << ADMIN_CONSOLE_DEFER_WEB_STOP) |
(1U << ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE),
.dispatcher_actions = 1U << ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE,
.is_current = owner_current, .drained = owner_drained, .perform = owner_perform,
};
/* No IO and no payload mutation: safe on console/revocation/lifecycle callers. */
static void request_close(void)
{
taskENTER_CRITICAL(&s_lock);
bool opened = s_slot.console_open;
admin_ssh_console_token_t token = s_slot.token;
if (s_slot.occupied) s_slot.close_requested = true;
taskEXIT_CRITICAL(&s_lock);
if (opened) admin_ssh_console_close(&token);
}
/* HTTPD callback, or lifecycle caller ONLY after HTTPD has successfully stopped. */
static void session_free(void *context)
{
if (context != &s_slot) return;
taskENTER_CRITICAL(&s_lock);
bool occupied = s_slot.occupied;
bool opened = s_slot.console_open;
bool active = s_slot.active;
admin_ssh_console_token_t token = s_slot.token;
secure_wipe(&s_slot, sizeof(s_slot));
taskEXIT_CRITICAL(&s_lock);
if (opened) admin_ssh_console_close(&token);
if (occupied && s_payload) secure_wipe(s_payload, sizeof(*s_payload));
if (active) count(&s_counts.disconnections, 1);
}
static bool capture(admin_ssh_console_token_t *token, user_principal_t *principal, int *fd)
{
taskENTER_CRITICAL(&s_lock);
bool active = s_slot.active;
*token = s_slot.token;
*principal = s_slot.principal;
*fd = s_slot.fd;
taskEXIT_CRITICAL(&s_lock);
return active;
}
static bool input_current(const admin_ssh_console_token_t *token,
const user_principal_t *principal)
{
if (owner_current(token, principal)) return true;
count(&s_counts.authorization_rejections, 1);
request_close();
return false;
}
static void discard_pending_input(void)
{
secure_wipe(s_payload->rx, sizeof(s_payload->rx));
s_payload->rx_offset = s_payload->rx_length = 0;
s_payload->input_deadline = 0;
count(&s_counts.input_backpressure, 1);
}
static bool feed_pending(const admin_ssh_console_token_t *token,
const user_principal_t *principal)
{
if (s_payload->rx_offset == s_payload->rx_length) return true;
if (!input_current(token, principal)) return false;
admin_ssh_console_session_snapshot_t console;
if (admin_ssh_console_get_session_snapshot(token, &console) != ESP_OK || !console.active) {
request_close();
return false;
}
if (console.deferred_action_pending) {
/* Never replay buffered keystrokes if a deferred action fails/cancels. */
discard_pending_input();
return true;
}
if (esp_timer_get_time() >= s_payload->input_deadline) {
count(&s_counts.input_backpressure, 1);
request_close();
return false;
}
size_t consumed = 0;
(void)admin_ssh_console_feed_input(token, s_payload->rx + s_payload->rx_offset,
s_payload->rx_length - s_payload->rx_offset, &consumed);
secure_wipe(s_payload->rx + s_payload->rx_offset, consumed);
s_payload->rx_offset += consumed;
if (s_payload->rx_offset == s_payload->rx_length) {
s_payload->rx_offset = s_payload->rx_length = 0;
s_payload->input_deadline = 0;
}
return true;
}
/* Only this HTTPD work callback sends console output or requests idle closure. */
static void poll_work(void *argument)
{
httpd_handle_t server = argument;
admin_ssh_console_token_t token;
user_principal_t principal;
int fd;
bool active = capture(&token, &principal, &fd);
taskENTER_CRITICAL(&s_lock);
bool attached = s_accepting && server == s_server;
taskEXIT_CRITICAL(&s_lock);
if (!active || !attached) goto done;
if (!input_current(&token, &principal)) goto closing;
if (httpd_sess_get_ctx(server, fd) != &s_slot ||
httpd_ws_get_fd_info(server, fd) != HTTPD_WS_CLIENT_WEBSOCKET) {
request_close();
goto closing;
}
admin_ssh_console_session_snapshot_t console;
if (admin_ssh_console_get_session_snapshot(&token, &console) != ESP_OK || !console.active) {
request_close();
goto closing;
}
if (!feed_pending(&token, &principal)) goto closing;
taskENTER_CRITICAL(&s_lock);
s_slot.sending = true; /* Covers the gap between ring consumption and socket send. */
taskEXIT_CRITICAL(&s_lock);
size_t length = 0;
esp_err_t error = admin_ssh_console_read_output(&token, s_payload->tx,
sizeof(s_payload->tx), &length);
if (error == ESP_OK && length && input_current(&token, &principal)) {
httpd_ws_frame_t frame = {.final = true, .type = HTTPD_WS_TYPE_BINARY,
.payload = s_payload->tx, .len = length};
error = httpd_ws_send_frame_async(server, fd, &frame);
if (error == ESP_OK) count(&s_counts.tx_bytes, (uint32_t)length);
}
secure_wipe(s_payload->tx, sizeof(s_payload->tx));
taskENTER_CRITICAL(&s_lock);
s_slot.sending = false;
taskEXIT_CRITICAL(&s_lock);
if (error != ESP_OK) {
count(&s_counts.send_failures, 1);
request_close();
}
closing:
taskENTER_CRITICAL(&s_lock);
bool close = s_slot.active && s_slot.close_requested && !s_slot.close_triggered;
taskEXIT_CRITICAL(&s_lock);
if (close && httpd_sess_get_ctx(server, fd) == &s_slot) {
/* IDF's queued close retains a reusable sock_db pointer. Shutdown on
* HTTPD instead: its next read owns deletion, with no late close that
* could evict a replacement (including a serial client). */
if (shutdown(fd, SHUT_RDWR) == 0) {
taskENTER_CRITICAL(&s_lock);
s_slot.close_triggered = true;
taskEXIT_CRITICAL(&s_lock);
} else count(&s_counts.send_failures, 1); /* Retry on the next bounded poll. */
}
done:
secure_wipe(&principal, sizeof(principal));
taskENTER_CRITICAL(&s_lock);
s_queued = false;
taskEXIT_CRITICAL(&s_lock);
}
/* ESP timer task: no database/console/socket calls, no waits, one queue entry max.
* Detach prevents new submissions and fences any submission already outside lock. */
static void poll_timer(void *argument)
{
(void)argument;
taskENTER_CRITICAL(&s_lock);
httpd_handle_t server = NULL;
uint32_t generation = 0;
if (s_accepting && s_slot.active && !s_queued) {
server = s_server;
generation = s_slot.token.slot_generation;
s_queued = true;
++s_submitting;
}
taskEXIT_CRITICAL(&s_lock);
if (!server) return;
esp_err_t error = httpd_queue_work(server, poll_work, server);
taskENTER_CRITICAL(&s_lock);
--s_submitting;
if (error != ESP_OK) {
s_queued = false;
if (s_slot.active && s_slot.token.slot_generation == generation)
s_slot.close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
if (error != ESP_OK) count(&s_counts.queue_failures, 1);
}
esp_err_t web_admin_transport_init(void)
{
#if defined(CONFIG_HTTPD_QUEUE_WORK_BLOCKING) && CONFIG_HTTPD_QUEUE_WORK_BLOCKING
return ESP_ERR_NOT_SUPPORTED;
#else
if (s_initialized) return ESP_OK; /* Lifecycle caller serializes initialization. */
admin_payload_t *payload = heap_caps_calloc(1, sizeof(*payload), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
esp_err_t error = payload ? ESP_OK : ESP_ERR_NO_MEM;
esp_timer_handle_t timer = NULL;
const esp_timer_create_args_t args = {
.callback = poll_timer, .name = "web_admin", .skip_unhandled_events = true,
};
if (error == ESP_OK) error = esp_timer_create(&args, &timer);
if (error == ESP_OK) error = esp_timer_start_periodic(timer, ADMIN_POLL_US);
if (error != ESP_OK) {
if (timer) (void)esp_timer_delete(timer);
if (payload) heap_caps_free(payload);
}
taskENTER_CRITICAL(&s_lock);
if (error == ESP_OK) {
s_payload = payload;
s_timer = timer;
s_initialized = true;
}
s_counts.last_error = error;
taskEXIT_CRITICAL(&s_lock);
return error;
#endif
}
esp_err_t web_admin_transport_attach(httpd_handle_t server)
{
if (!server) return ESP_ERR_INVALID_ARG;
taskENTER_CRITICAL(&s_lock);
bool allowed = s_initialized && !s_server && !s_queued && !s_submitting && !s_slot.occupied;
taskEXIT_CRITICAL(&s_lock);
if (!allowed) return ESP_ERR_INVALID_STATE;
web_admin_tickets_start();
taskENTER_CRITICAL(&s_lock);
s_server = server;
s_accepting = true;
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
esp_err_t web_admin_transport_detach(httpd_handle_t server)
{
taskENTER_CRITICAL(&s_lock);
bool owned = server && server == s_server;
if (owned) s_accepting = false;
taskEXIT_CRITICAL(&s_lock);
if (!owned) return ESP_ERR_INVALID_STATE;
web_admin_tickets_stop();
request_close();
int64_t deadline = esp_timer_get_time() + ADMIN_DETACH_TIMEOUT_US;
for (;;) {
taskENTER_CRITICAL(&s_lock);
bool submitting = s_submitting != 0;
taskEXIT_CRITICAL(&s_lock);
if (!submitting) return ESP_OK;
if (esp_timer_get_time() >= deadline) return ESP_ERR_TIMEOUT;
vTaskDelay(1);
}
}
void web_admin_transport_stopped(httpd_handle_t server)
{
taskENTER_CRITICAL(&s_lock);
bool owned = server && s_server == server && !s_accepting && !s_submitting;
taskEXIT_CRITICAL(&s_lock);
if (!owned) return;
session_free(&s_slot);
taskENTER_CRITICAL(&s_lock);
s_server = NULL;
s_queued = false; /* HTTPD is gone; its queued callbacks can no longer execute. */
taskEXIT_CRITICAL(&s_lock);
}
void web_admin_transport_revoke(web_session_id_t id, const uint8_t *username, size_t length)
{
web_admin_tickets_revoke(id, username, length);
taskENTER_CRITICAL(&s_lock);
bool match = s_slot.occupied && (id ? s_slot.session == id :
!username || (length == s_slot.principal.username_length &&
length <= USER_DATABASE_USERNAME_CAPACITY &&
memcmp(username, s_slot.principal.username, length) == 0));
admin_ssh_console_token_t token = s_slot.token;
bool opened = match && s_slot.console_open;
if (match) s_slot.close_requested = true;
taskEXIT_CRITICAL(&s_lock);
if (opened) admin_ssh_console_close(&token);
}
static esp_err_t response(httpd_req_t *request, const char *status, const char *body)
{
esp_err_t error = httpd_resp_set_status(request, status);
if (error == ESP_OK) error = httpd_resp_set_type(request, "application/json");
if (error == ESP_OK) error = httpd_resp_set_hdr(request, "Cache-Control", "no-store");
if (error == ESP_OK) error = httpd_resp_set_hdr(request, "Referrer-Policy", "no-referrer");
if (error == ESP_OK) error = httpd_resp_set_hdr(request, "X-Content-Type-Options", "nosniff");
if (error == ESP_OK) error = httpd_resp_sendstr(request, body);
return error;
}
static esp_err_t deny(httpd_req_t *request, const char *status, const char *body)
{
if (!strcmp(status, "503 Service Unavailable") &&
httpd_resp_set_hdr(request, "Retry-After", "5") != ESP_OK) return ESP_FAIL;
(void)response(request, status, body);
return ESP_FAIL; /* Close after rejection, never leave unread frames/body alive. */
}
esp_err_t web_admin_transport_ticket_handler(httpd_req_t *request)
{
web_session_view_t view = {0};
char ticket[WEB_ADMIN_TICKET_LENGTH + 1U] = {0}, body[128] = {0};
bool allowed = false;
esp_err_t error = web_cookie_auth_require(request, true, false, &view, &allowed);
if (error != ESP_OK || !allowed) goto cleanup;
if (view.principal.role != USER_ROLE_ADMIN) {
count(&s_counts.authorization_rejections, 1);
error = deny(request, "403 Forbidden", "{\"error\":\"admin_required\"}");
goto cleanup;
}
taskENTER_CRITICAL(&s_lock);
bool attached = s_accepting && s_server == request->handle;
taskEXIT_CRITICAL(&s_lock);
error = attached ? web_admin_tickets_issue(view.id, &view.principal, ticket) : ESP_ERR_INVALID_STATE;
if (error != ESP_OK) {
if (error == ESP_ERR_NO_MEM) count(&s_counts.capacity_rejections, 1);
error = deny(request, "503 Service Unavailable", "{\"error\":\"admin_unavailable_or_capacity\"}");
goto cleanup;
}
int n = snprintf(body, sizeof(body), "{\"ticket\":\"%s\",\"expires_in\":30}", ticket);
error = n > 0 && (size_t)n < sizeof(body) ? response(request, "200 OK", body) : ESP_FAIL;
cleanup:
secure_wipe(ticket, sizeof(ticket));
secure_wipe(body, sizeof(body));
secure_wipe(&view, sizeof(view));
web_httpd_wipe_request(request, web_httpd_unread_body(request));
return error;
}
static esp_err_t frame_handler(httpd_req_t *request)
{
admin_ssh_console_token_t token;
user_principal_t principal;
int fd;
bool active = capture(&token, &principal, &fd);
bool valid = active && request->sess_ctx == &s_slot &&
fd == httpd_req_to_sockfd(request) && input_current(&token, &principal);
if (!valid) goto failure;
httpd_ws_frame_t frame = {0};
if (httpd_ws_recv_frame(request, &frame, 0) != ESP_OK || !frame.final ||
frame.type != HTTPD_WS_TYPE_BINARY || frame.len > WEB_ADMIN_RX_CAPACITY) {
count(&s_counts.protocol_errors, 1);
goto failure;
}
admin_ssh_console_session_snapshot_t console;
if (admin_ssh_console_get_session_snapshot(&token, &console) != ESP_OK || !console.active)
goto failure;
/* Latch before the potentially blocking receive: cancellation during receive
* must not turn input observed during deferral into a new command. */
bool discard_frame = console.deferred_action_pending;
if (s_payload->rx_length != s_payload->rx_offset) {
if (!discard_frame) {
count(&s_counts.input_backpressure, 1);
goto failure;
}
/* Deferral may start before the next poll discards buffered trailing input. */
discard_pending_input();
}
frame.payload = s_payload->rx;
/* IDF treats len==0 as another header probe, not an empty payload read. */
if (frame.len && httpd_ws_recv_frame(request, &frame, sizeof(s_payload->rx)) != ESP_OK)
goto failure;
s_payload->rx_length = frame.len;
s_payload->rx_offset = 0;
s_payload->input_deadline = esp_timer_get_time() + ADMIN_INPUT_TIMEOUT_US;
if (discard_frame) discard_pending_input();
else if (!feed_pending(&token, &principal)) goto failure;
count(&s_counts.rx_bytes, (uint32_t)frame.len);
secure_wipe(&principal, sizeof(principal));
return ESP_OK;
failure:
secure_wipe(&principal, sizeof(principal));
request_close();
return ESP_FAIL;
}
esp_err_t web_admin_transport_upgrade_handler(httpd_req_t *request)
{
web_session_view_t view = {0};
char ticket[WEB_ADMIN_TICKET_LENGTH + 1U] = {0};
admin_ssh_console_token_t token = {0};
bool allowed = false, reserved = false, opened = false;
esp_err_t error = web_cookie_auth_require(request, false, true, &view, &allowed);
if (error != ESP_OK || !allowed) goto cleanup;
if (view.principal.role != USER_ROLE_ADMIN) {
count(&s_counts.authorization_rejections, 1);
error = deny(request, "403 Forbidden", "{\"error\":\"admin_required\"}");
goto cleanup;
}
static const char prefix[] = WEB_ADMIN_WS_URI "?ticket=";
if (!web_httpd_upgrade_requested(request) ||
strncmp(request->uri, prefix, sizeof(prefix) - 1U) ||
strlen(request->uri) != sizeof(prefix) - 1U + WEB_ADMIN_TICKET_LENGTH) {
error = deny(request, "400 Bad Request", "{\"error\":\"invalid_upgrade\"}");
goto cleanup;
}
memcpy(ticket, request->uri + sizeof(prefix) - 1U, WEB_ADMIN_TICKET_LENGTH);
if (web_admin_tickets_consume(ticket, view.id, &view.principal) != ESP_OK) {
count(&s_counts.authorization_rejections, 1);
error = deny(request, "403 Forbidden", "{\"error\":\"invalid_ticket\"}");
goto cleanup;
}
int socket_fd = httpd_req_to_sockfd(request);
taskENTER_CRITICAL(&s_lock);
if (socket_fd >= 0 && s_accepting && s_server == request->handle &&
!s_slot.occupied && s_generation != UINT32_MAX) {
++s_generation;
token = (admin_ssh_console_token_t){.transport = ADMIN_CONSOLE_TRANSPORT_WEB,
.session_id = s_generation, .slot_generation = s_generation};
s_slot.occupied = true;
s_slot.session = view.id;
s_slot.principal = view.principal;
s_slot.fd = socket_fd;
s_slot.token = token;
reserved = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!reserved) {
count(&s_counts.capacity_rejections, 1);
error = deny(request, "503 Service Unavailable", "{\"error\":\"admin_capacity\"}");
goto cleanup;
}
error = admin_ssh_console_open_available(&token, &view.principal, &s_owner);
if (error != ESP_OK) {
count(&s_counts.capacity_rejections, 1);
error = deny(request, "503 Service Unavailable", "{\"error\":\"console_capacity_or_unavailable\"}");
goto cleanup;
}
opened = true;
bool current = false;
error = web_session_store_check_principal(view.id, &view.principal, &current);
taskENTER_CRITICAL(&s_lock);
s_slot.token = token;
s_slot.console_open = true;
bool admitted = error == ESP_OK && current && s_accepting && !s_slot.close_requested;
taskEXIT_CRITICAL(&s_lock);
if (!admitted) {
error = deny(request, "403 Forbidden", "{\"error\":\"session_revoked\"}");
goto cleanup;
}
error = web_httpd_upgrade(request, frame_handler);
if (error != ESP_OK) goto cleanup;
taskENTER_CRITICAL(&s_lock);
admitted = s_accepting && !s_slot.close_requested;
if (admitted) s_slot.active = true;
taskEXIT_CRITICAL(&s_lock);
if (!admitted) { error = ESP_FAIL; goto cleanup; }
request->sess_ctx = &s_slot;
request->free_ctx = session_free;
count(&s_counts.connections, 1);
reserved = false; /* HTTPD context now owns cleanup. */
cleanup:
if (reserved) {
if (opened) admin_ssh_console_close(&token);
session_free(&s_slot);
}
secure_wipe(ticket, sizeof(ticket));
secure_wipe(&view, sizeof(view));
web_httpd_wipe_request(request, web_httpd_unread_body(request));
return error;
}
void web_admin_transport_get_snapshot(web_admin_transport_snapshot_t *snapshot)
{
if (!snapshot) return;
taskENTER_CRITICAL(&s_lock);
*snapshot = s_counts;
snapshot->initialized = s_initialized;
snapshot->attached = s_accepting;
snapshot->active = s_slot.active;
snapshot->closing = s_slot.close_requested;
snapshot->payload_bytes = s_payload ? sizeof(*s_payload) : 0;
snapshot->static_bytes = sizeof(s_lock) + sizeof(s_slot) + sizeof(s_payload) +
sizeof(s_timer) + sizeof(s_server) + sizeof(s_initialized) + sizeof(s_accepting) +
sizeof(s_queued) + sizeof(s_submitting) + sizeof(s_generation) + sizeof(s_counts);
taskEXIT_CRITICAL(&s_lock);
}