Files
ESP32_Serial_Swiss_Army_Knife/src/web_serial_transport.c
T

1633 lines
53 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* Authenticated, bounded WebSocket transport for the serial session broker. */
#include "web_serial_transport.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/sha256.h"
#include "sdkconfig.h"
#include "secure_random.h"
#include "serial_service.h"
#include "web_security.h"
#if !defined(CONFIG_HTTPD_WS_SUPPORT) || !CONFIG_HTTPD_WS_SUPPORT
#error "web_serial_transport requires CONFIG_HTTPD_WS_SUPPORT"
#endif
#define WEB_SERIAL_RANDOM_BYTES 24U
#define WEB_SERIAL_SHA256_BYTES 32U
#define WEB_SERIAL_QUERY_CAPACITY 48U
#define WEB_SERIAL_HOST_CAPACITY 128U
#define WEB_SERIAL_ORIGIN_CAPACITY (sizeof("https://") + WEB_SERIAL_HOST_CAPACITY)
#define WEB_SERIAL_TICKET_RESPONSE_CAPACITY 96U
#define WEB_SERIAL_TASK_STACK_SIZE 6144U
#define WEB_SERIAL_TASK_PRIORITY 7U
#define WEB_SERIAL_POLL_MS 5U
#define WEB_SERIAL_DETACH_TIMEOUT_US 1000000LL
_Static_assert(WEB_SERIAL_TRANSPORT_TICKET_LENGTH ==
(WEB_SERIAL_RANDOM_BYTES * 4U) / 3U,
"24 random bytes must encode as 32 unpadded Base64URL bytes");
_Static_assert(WEB_SERIAL_TRANSPORT_TX_PAYLOAD_SIZE <= 512U,
"WebSocket TX buffers must remain bounded to 512 bytes");
typedef enum {
WEB_SERIAL_SLOT_FREE = 0,
WEB_SERIAL_SLOT_RESERVED,
WEB_SERIAL_SLOT_ACTIVE,
WEB_SERIAL_SLOT_CLOSING,
} web_serial_slot_state_t;
typedef struct {
uint8_t digest[WEB_SERIAL_SHA256_BYTES];
int64_t expires_at_us;
uint32_t material_generation;
bool active;
} web_serial_ticket_t;
struct web_serial_slot;
typedef struct {
struct web_serial_slot *slot;
httpd_handle_t server;
int socket_fd;
uint32_t generation;
} web_serial_work_t;
typedef struct web_serial_slot {
web_serial_slot_state_t state;
httpd_handle_t server;
int socket_fd;
uint32_t generation;
session_broker_client_id_t broker_client_id;
bool writer;
bool hello_pending;
bool work_pending;
bool close_requested;
bool close_triggered;
int64_t close_retry_at_us;
bool disconnect_busy;
httpd_ws_type_t tx_type;
size_t tx_length;
uint8_t rx_data[WEB_SERIAL_TRANSPORT_MAX_RX_PAYLOAD];
uint8_t tx_data[WEB_SERIAL_TRANSPORT_TX_PAYLOAD_SIZE];
web_serial_work_t work;
} web_serial_slot_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_initialized;
static httpd_handle_t s_server;
static TaskHandle_t s_transport_task;
static StaticTask_t s_transport_task_storage;
static StackType_t
s_transport_task_stack[WEB_SERIAL_TASK_STACK_SIZE / sizeof(StackType_t)];
static web_serial_ticket_t s_tickets[WEB_SERIAL_TRANSPORT_MAX_TICKETS];
static web_serial_slot_t s_slots[WEB_SERIAL_TRANSPORT_MAX_SESSIONS];
static web_serial_transport_counters_t s_counters;
static uint32_t s_httpd_close_operations;
static uint32_t s_inflight_handlers;
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
return (milliseconds > 0U && ticks == 0U) ? 1U : ticks;
}
static int64_t monotonic_time_us(void)
{
return esp_timer_get_time();
}
static void notify_transport_task(void)
{
TaskHandle_t task;
taskENTER_CRITICAL(&s_lock);
task = s_transport_task;
taskEXIT_CRITICAL(&s_lock);
if (task != NULL) {
xTaskNotifyGive(task);
}
}
static void add_counter(uint64_t *counter, uint64_t amount)
{
taskENTER_CRITICAL(&s_lock);
*counter += amount;
taskEXIT_CRITICAL(&s_lock);
}
static uint32_t next_generation(uint32_t generation)
{
++generation;
return generation == 0U ? 1U : generation;
}
static bool slot_pointer_valid(const web_serial_slot_t *slot)
{
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
if (slot == &s_slots[index]) {
return true;
}
}
return false;
}
static void clear_ticket_locked(web_serial_ticket_t *ticket)
{
secure_wipe(ticket->digest, sizeof(ticket->digest));
ticket->expires_at_us = 0;
ticket->material_generation = 0U;
ticket->active = false;
}
static void clear_all_tickets_locked(void)
{
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
clear_ticket_locked(&s_tickets[index]);
}
}
static void purge_tickets_locked(int64_t now_us, uint32_t material_generation,
bool check_generation)
{
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *ticket = &s_tickets[index];
if (!ticket->active) {
continue;
}
if (ticket->expires_at_us <= now_us) {
clear_ticket_locked(ticket);
++s_counters.tickets_expired;
} else if (check_generation &&
ticket->material_generation != material_generation) {
clear_ticket_locked(ticket);
}
}
}
static bool constant_time_equal(const uint8_t *left, const uint8_t *right,
size_t length)
{
uint8_t difference = 0U;
for (size_t index = 0U; index < length; ++index) {
difference |= left[index] ^ right[index];
}
return difference == 0U;
}
static void encode_base64url_24(const uint8_t input[WEB_SERIAL_RANDOM_BYTES],
char output[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY])
{
static const char alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
size_t output_offset = 0U;
for (size_t input_offset = 0U; input_offset < WEB_SERIAL_RANDOM_BYTES;
input_offset += 3U) {
uint32_t value = ((uint32_t)input[input_offset] << 16U) |
((uint32_t)input[input_offset + 1U] << 8U) |
input[input_offset + 2U];
output[output_offset++] = alphabet[(value >> 18U) & 0x3fU];
output[output_offset++] = alphabet[(value >> 12U) & 0x3fU];
output[output_offset++] = alphabet[(value >> 6U) & 0x3fU];
output[output_offset++] = alphabet[value & 0x3fU];
}
output[output_offset] = '\0';
}
static bool base64url_character(char value)
{
return (value >= 'A' && value <= 'Z') ||
(value >= 'a' && value <= 'z') ||
(value >= '0' && value <= '9') || value == '-' || value == '_';
}
static esp_err_t current_material_generation(uint32_t *generation)
{
web_security_certificate_metadata_t metadata;
esp_err_t result = web_security_get_certificate_metadata(&metadata);
if (result == ESP_OK) {
*generation = metadata.material_generation;
}
secure_wipe(&metadata, sizeof(metadata));
return result;
}
static esp_err_t sha256_ticket(const char *ticket,
uint8_t digest[WEB_SERIAL_SHA256_BYTES])
{
return mbedtls_sha256((const unsigned char *)ticket,
WEB_SERIAL_TRANSPORT_TICKET_LENGTH,
digest, 0) == 0
? ESP_OK
: ESP_FAIL;
}
static esp_err_t extract_ticket_query(httpd_req_t *request,
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY])
{
char query[WEB_SERIAL_QUERY_CAPACITY] = {0};
const char prefix[] = WEB_SERIAL_TRANSPORT_TICKET_QUERY_KEY "=";
size_t query_length = httpd_req_get_url_query_len(request);
esp_err_t result = ESP_ERR_INVALID_ARG;
if (query_length == 0U || query_length >= sizeof(query) ||
httpd_req_get_url_query_str(request, query, sizeof(query)) != ESP_OK) {
goto cleanup;
}
size_t prefix_length = sizeof(prefix) - 1U;
if (query_length != prefix_length + WEB_SERIAL_TRANSPORT_TICKET_LENGTH ||
memcmp(query, prefix, prefix_length) != 0) {
goto cleanup;
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_TICKET_LENGTH; ++index) {
char value = query[prefix_length + index];
if (!base64url_character(value)) {
goto cleanup;
}
ticket[index] = value;
}
ticket[WEB_SERIAL_TRANSPORT_TICKET_LENGTH] = '\0';
result = ESP_OK;
cleanup:
secure_wipe(query, sizeof(query));
return result;
}
static esp_err_t validate_origin(httpd_req_t *request)
{
char origin[WEB_SERIAL_ORIGIN_CAPACITY] = {0};
char host[WEB_SERIAL_HOST_CAPACITY] = {0};
char expected[WEB_SERIAL_ORIGIN_CAPACITY] = {0};
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;
}
size_t host_length = httpd_req_get_hdr_value_len(request, "Host");
if (host_length == 0U || host_length >= sizeof(host) ||
httpd_req_get_hdr_value_str(request, "Host", host, sizeof(host)) != ESP_OK) {
secure_wipe(origin, sizeof(origin));
return ESP_ERR_INVALID_ARG;
}
int written = snprintf(expected, sizeof(expected), "https://%s", host);
bool matches = written > 0 && (size_t)written < sizeof(expected) &&
strcmp(origin, expected) == 0;
secure_wipe(origin, sizeof(origin));
secure_wipe(host, sizeof(host));
secure_wipe(expected, sizeof(expected));
return matches ? ESP_OK : ESP_ERR_INVALID_ARG;
}
static esp_err_t consume_ticket(const char *ticket, bool *consumed)
{
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
uint32_t generation = 0U;
*consumed = false;
esp_err_t result = current_material_generation(&generation);
if (result == ESP_OK) {
result = sha256_ticket(ticket, digest);
}
if (result != ESP_OK) {
secure_wipe(digest, sizeof(digest));
add_counter(&s_counters.tickets_rejected, 1U);
return result;
}
int64_t now_us = monotonic_time_us();
size_t matching_index = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
size_t matching_count = 0U;
taskENTER_CRITICAL(&s_lock);
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *entry = &s_tickets[index];
bool digest_matches =
constant_time_equal(entry->digest, digest, sizeof(digest));
if (entry->active && digest_matches) {
matching_index = index;
++matching_count;
}
}
purge_tickets_locked(now_us, generation, true);
if (matching_count == 1U &&
matching_index < WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
web_serial_ticket_t *entry = &s_tickets[matching_index];
if (entry->active && entry->expires_at_us > now_us &&
entry->material_generation == generation) {
clear_ticket_locked(entry);
++s_counters.tickets_consumed;
*consumed = true;
}
}
if (!*consumed) {
/* A defensive duplicate can never become a repeatedly usable ticket. */
if (matching_count > 1U) {
for (size_t index = 0U;
index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (entry->active && constant_time_equal(
entry->digest, digest,
sizeof(digest))) {
clear_ticket_locked(entry);
}
}
}
++s_counters.tickets_rejected;
}
taskEXIT_CRITICAL(&s_lock);
secure_wipe(digest, sizeof(digest));
return ESP_OK;
}
static web_serial_slot_t *reserve_slot(httpd_handle_t server, int socket_fd,
uint32_t *generation)
{
web_serial_slot_t *selected = NULL;
taskENTER_CRITICAL(&s_lock);
if (s_initialized && s_server == server) {
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_FREE) {
continue;
}
slot->generation = next_generation(slot->generation);
slot->state = WEB_SERIAL_SLOT_RESERVED;
slot->server = server;
slot->socket_fd = socket_fd;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
slot->close_requested = false;
slot->close_triggered = false;
slot->close_retry_at_us = 0;
slot->disconnect_busy = false;
slot->tx_length = 0U;
selected = slot;
*generation = slot->generation;
break;
}
}
taskEXIT_CRITICAL(&s_lock);
return selected;
}
static void make_slot_free_locked(web_serial_slot_t *slot)
{
slot->state = WEB_SERIAL_SLOT_FREE;
slot->server = NULL;
slot->socket_fd = -1;
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
slot->close_requested = false;
slot->close_triggered = false;
slot->close_retry_at_us = 0;
slot->disconnect_busy = false;
slot->tx_length = 0U;
}
static void release_reserved_slot(web_serial_slot_t *slot, uint32_t generation)
{
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == generation) {
make_slot_free_locked(slot);
}
taskEXIT_CRITICAL(&s_lock);
}
static void close_unpublished_broker_session(web_serial_slot_t *slot,
uint32_t generation,
session_broker_client_id_t client_id)
{
esp_err_t result = session_broker_disconnect(client_id);
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == generation) {
if (result == ESP_OK || result == ESP_ERR_NOT_FOUND) {
make_slot_free_locked(slot);
} else {
slot->state = WEB_SERIAL_SLOT_CLOSING;
slot->broker_client_id = client_id;
++s_counters.broker_failures;
}
}
taskEXIT_CRITICAL(&s_lock);
if (result != ESP_OK && result != ESP_ERR_NOT_FOUND) {
notify_transport_task();
}
}
static void request_handler_close(web_serial_slot_t *slot, uint32_t generation)
{
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation &&
(slot->state == WEB_SERIAL_SLOT_ACTIVE ||
slot->state == WEB_SERIAL_SLOT_CLOSING)) {
slot->close_requested = true;
if (!slot->close_triggered) {
/* Returning ESP_FAIL closes this request; do not also queue a close. */
slot->close_triggered = true;
++s_counters.close_requests;
}
}
taskEXIT_CRITICAL(&s_lock);
}
static esp_err_t reject_protocol_frame(web_serial_slot_t *slot,
uint32_t generation, size_t payload_length)
{
taskENTER_CRITICAL(&s_lock);
++s_counters.rx_ws_frames_rejected;
s_counters.rx_ws_bytes_rejected += payload_length;
++s_counters.protocol_errors;
taskEXIT_CRITICAL(&s_lock);
request_handler_close(slot, generation);
return ESP_FAIL;
}
static void web_serial_session_free(void *context)
{
web_serial_slot_t *slot = context;
if (!slot_pointer_valid(slot)) {
return;
}
bool notify = false;
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
slot->writer = false;
slot->hello_pending = false;
slot->close_requested = false;
slot->close_triggered = true;
slot->close_retry_at_us = 0;
++s_counters.disconnections;
notify = true;
}
taskEXIT_CRITICAL(&s_lock);
if (notify) {
notify_transport_task();
}
}
static esp_err_t send_plain_bad_request(httpd_req_t *request)
{
static const char message[] = "WebSocket upgrade required.\n";
esp_err_t result = httpd_resp_set_status(request, "400 Bad Request");
if (result == ESP_OK) {
result = httpd_resp_set_type(request, "text/plain; charset=utf-8");
}
if (result == ESP_OK) {
result = httpd_resp_set_hdr(request, "Cache-Control", "no-store");
}
if (result == ESP_OK) {
result = httpd_resp_send(request, message, sizeof(message) - 1U);
}
return result;
}
static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd)
{
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
uint32_t slot_generation = 0U;
web_serial_slot_t *slot = NULL;
bool consumed = false;
esp_err_t result;
if (validate_origin(request) != ESP_OK ||
extract_ticket_query(request, ticket) != ESP_OK) {
add_counter(&s_counters.tickets_rejected, 1U);
result = ESP_FAIL;
goto cleanup;
}
slot = reserve_slot(request->handle, socket_fd, &slot_generation);
if (slot == NULL) {
add_counter(&s_counters.connection_failures, 1U);
result = ESP_FAIL;
goto cleanup;
}
result = consume_ticket(ticket, &consumed);
if (result != ESP_OK || !consumed) {
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
if (!serial_service_is_running()) {
result = serial_service_start();
/* Another permanent transport may have won the start race. */
if (result != ESP_OK && serial_service_is_running()) {
result = ESP_OK;
}
if (result != ESP_OK) {
add_counter(&s_counters.service_start_failures, 1U);
add_counter(&s_counters.connection_failures, 1U);
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
}
size_t slot_index = (size_t)(slot - s_slots);
char client_name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
int written = snprintf(client_name, sizeof(client_name),
"web-%u-%" PRIu32,
(unsigned int)slot_index, slot_generation);
if (written <= 0 || (size_t)written >= sizeof(client_name)) {
add_counter(&s_counters.connection_failures, 1U);
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
result = session_broker_connect(SESSION_BROKER_CLIENT_WEB,
client_name, &client_id);
if (result != ESP_OK) {
add_counter(&s_counters.connection_failures, 1U);
add_counter(&s_counters.broker_failures, 1U);
release_reserved_slot(slot, slot_generation);
result = ESP_FAIL;
goto cleanup;
}
add_counter(&s_counters.writer_requests, 1U);
result = session_broker_request_writer(client_id);
bool writer = result == ESP_OK;
if (writer) {
add_counter(&s_counters.writer_grants, 1U);
} else if (result == ESP_ERR_INVALID_STATE) {
add_counter(&s_counters.writer_denials, 1U);
} else {
add_counter(&s_counters.connection_failures, 1U);
add_counter(&s_counters.broker_failures, 1U);
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
bool activated = false;
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_RESERVED &&
slot->generation == slot_generation &&
s_server == request->handle) {
slot->state = WEB_SERIAL_SLOT_ACTIVE;
slot->broker_client_id = client_id;
slot->writer = writer;
slot->hello_pending = true;
++s_counters.connections;
activated = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!activated) {
add_counter(&s_counters.connection_failures, 1U);
close_unpublished_broker_session(slot, slot_generation, client_id);
result = ESP_FAIL;
goto cleanup;
}
/* Static context is published only after service and broker setup succeed. */
request->sess_ctx = slot;
request->free_ctx = web_serial_session_free;
notify_transport_task();
result = ESP_OK;
cleanup:
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)
{
web_serial_slot_t *slot = request->sess_ctx;
int socket_fd = httpd_req_to_sockfd(request);
if (!slot_pointer_valid(slot) || socket_fd < 0) {
return false;
}
bool valid;
taskENTER_CRITICAL(&s_lock);
valid = slot->state == WEB_SERIAL_SLOT_ACTIVE &&
s_server == request->handle &&
slot->server == request->handle &&
slot->socket_fd == socket_fd &&
slot->broker_client_id != SESSION_BROKER_NO_CLIENT;
if (valid) {
*slot_out = slot;
*generation = slot->generation;
*client_id = slot->broker_client_id;
}
taskEXIT_CRITICAL(&s_lock);
return valid;
}
static void account_accepted_frame(size_t accepted, size_t rejected,
bool complete)
{
taskENTER_CRITICAL(&s_lock);
s_counters.rx_ws_bytes_accepted += accepted;
s_counters.rx_ws_bytes_rejected += rejected;
if (complete) {
++s_counters.rx_ws_frames_accepted;
} else {
++s_counters.rx_ws_frames_rejected;
}
taskEXIT_CRITICAL(&s_lock);
}
static esp_err_t process_binary_frame(web_serial_slot_t *slot,
uint32_t generation,
session_broker_client_id_t client_id,
const uint8_t *payload, size_t length)
{
size_t accepted = 0U;
esp_err_t result = session_broker_write(client_id, payload, length, &accepted);
if (accepted > length) {
accepted = 0U;
result = ESP_FAIL;
}
account_accepted_frame(accepted, length - accepted,
result == ESP_OK && accepted == length);
if (result == ESP_ERR_NOT_FOUND) {
add_counter(&s_counters.broker_failures, 1U);
request_handler_close(slot, generation);
return ESP_FAIL;
}
if (result != ESP_OK && result != ESP_ERR_INVALID_STATE &&
result != ESP_ERR_TIMEOUT) {
add_counter(&s_counters.broker_failures, 1U);
}
return ESP_OK;
}
static esp_err_t process_text_frame(web_serial_slot_t *slot,
uint32_t generation,
session_broker_client_id_t client_id,
const uint8_t *payload, size_t length)
{
static const char request_writer[] = "request-writer";
static const char release_writer[] = "release-writer";
esp_err_t result;
if (length == sizeof(request_writer) - 1U &&
memcmp(payload, request_writer, length) == 0) {
bool was_writer;
taskENTER_CRITICAL(&s_lock);
was_writer = slot->generation == generation && slot->writer;
++s_counters.writer_requests;
taskEXIT_CRITICAL(&s_lock);
result = session_broker_request_writer(client_id);
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation && result == ESP_OK) {
slot->writer = true;
}
if (result == ESP_OK && !was_writer) {
++s_counters.writer_grants;
} else if (result == ESP_ERR_INVALID_STATE) {
++s_counters.writer_denials;
}
taskEXIT_CRITICAL(&s_lock);
account_accepted_frame(length, 0U, true);
if (result == ESP_ERR_NOT_FOUND) {
add_counter(&s_counters.broker_failures, 1U);
request_handler_close(slot, generation);
return ESP_FAIL;
}
return ESP_OK;
}
if (length == sizeof(release_writer) - 1U &&
memcmp(payload, release_writer, length) == 0) {
result = session_broker_release_writer(client_id);
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation && result == ESP_OK) {
slot->writer = false;
++s_counters.writer_releases;
}
taskEXIT_CRITICAL(&s_lock);
account_accepted_frame(length, 0U, true);
if (result == ESP_ERR_NOT_FOUND) {
add_counter(&s_counters.broker_failures, 1U);
request_handler_close(slot, generation);
return ESP_FAIL;
}
return ESP_OK;
}
return reject_protocol_frame(slot, generation, length);
}
static esp_err_t process_websocket_frame(httpd_req_t *request)
{
web_serial_slot_t *slot = NULL;
uint32_t generation = 0U;
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
if (!capture_active_session(request, &slot, &generation, &client_id)) {
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
int socket_fd = httpd_req_to_sockfd(request);
if (httpd_ws_get_fd_info(request->handle, socket_fd) !=
HTTPD_WS_CLIENT_WEBSOCKET) {
return reject_protocol_frame(slot, generation, 0U);
}
httpd_ws_frame_t frame = {0};
esp_err_t result = httpd_ws_recv_frame(request, &frame, 0U);
if (result != ESP_OK) {
return reject_protocol_frame(slot, generation, frame.len);
}
if (!frame.final || frame.type == HTTPD_WS_TYPE_CONTINUE ||
frame.len > WEB_SERIAL_TRANSPORT_MAX_RX_PAYLOAD ||
(frame.type != HTTPD_WS_TYPE_BINARY &&
frame.type != HTTPD_WS_TYPE_TEXT)) {
return reject_protocol_frame(slot, generation, frame.len);
}
frame.payload = slot->rx_data;
result = httpd_ws_recv_frame(request, &frame, sizeof(slot->rx_data));
if (result != ESP_OK) {
return reject_protocol_frame(slot, generation, frame.len);
}
if (frame.type == HTTPD_WS_TYPE_BINARY) {
return process_binary_frame(slot, generation, client_id,
slot->rx_data, frame.len);
}
return process_text_frame(slot, generation, client_id,
slot->rx_data, frame.len);
}
static void finish_closing_slot_locked(web_serial_slot_t *slot)
{
if (slot->state == WEB_SERIAL_SLOT_CLOSING &&
slot->broker_client_id == SESSION_BROKER_NO_CLIENT &&
!slot->work_pending) {
make_slot_free_locked(slot);
}
}
static void web_serial_send_work(void *argument)
{
web_serial_work_t *work = argument;
web_serial_slot_t *slot = work != NULL ? work->slot : NULL;
if (!slot_pointer_valid(slot)) {
return;
}
httpd_handle_t server;
int socket_fd;
uint32_t generation;
httpd_ws_type_t type;
size_t length;
bool valid;
taskENTER_CRITICAL(&s_lock);
valid = slot->work_pending && work == &slot->work &&
work->generation == slot->generation &&
work->server == slot->server &&
work->socket_fd == slot->socket_fd;
server = work->server;
socket_fd = work->socket_fd;
generation = work->generation;
type = slot->tx_type;
length = slot->tx_length;
taskEXIT_CRITICAL(&s_lock);
if (!valid) {
return;
}
esp_err_t result = ESP_FAIL;
void *current_context = httpd_sess_get_ctx(server, socket_fd);
if (current_context == slot &&
httpd_ws_get_fd_info(server, socket_fd) ==
HTTPD_WS_CLIENT_WEBSOCKET) {
httpd_ws_frame_t frame = {
.final = true,
.fragmented = false,
.type = type,
.payload = slot->tx_data,
.len = length,
};
result = httpd_ws_send_frame_async(server, socket_fd, &frame);
if (result == ESP_OK) {
(void)httpd_sess_update_lru_counter(server, socket_fd);
}
}
taskENTER_CRITICAL(&s_lock);
if (slot->work_pending && slot->generation == generation &&
work == &slot->work) {
slot->work_pending = false;
slot->tx_length = 0U;
if (result == ESP_OK) {
if (type == HTTPD_WS_TYPE_BINARY) {
++s_counters.tx_binary_frames;
s_counters.tx_binary_bytes += length;
} else {
++s_counters.tx_control_frames;
s_counters.tx_control_bytes += length;
}
} else {
++s_counters.send_failures;
slot->close_requested = true;
}
finish_closing_slot_locked(slot);
}
taskEXIT_CRITICAL(&s_lock);
notify_transport_task();
}
static esp_err_t queue_slot_frame(web_serial_slot_t *slot, uint32_t generation,
httpd_ws_type_t type, size_t length)
{
httpd_handle_t server;
bool prepared = false;
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE &&
slot->generation == generation && !slot->work_pending &&
!slot->close_requested && slot->server == s_server &&
length <= sizeof(slot->tx_data)) {
slot->tx_type = type;
slot->tx_length = length;
slot->work.slot = slot;
slot->work.server = slot->server;
slot->work.socket_fd = slot->socket_fd;
slot->work.generation = generation;
slot->work_pending = true;
server = slot->server;
prepared = true;
} else {
server = NULL;
}
taskEXIT_CRITICAL(&s_lock);
if (!prepared) {
return ESP_ERR_INVALID_STATE;
}
#if defined(CONFIG_HTTPD_QUEUE_WORK_BLOCKING) && CONFIG_HTTPD_QUEUE_WORK_BLOCKING
esp_err_t result = ESP_ERR_NOT_SUPPORTED;
#else
esp_err_t result = httpd_queue_work(server, web_serial_send_work,
&slot->work);
#endif
if (result != ESP_OK) {
taskENTER_CRITICAL(&s_lock);
if (slot->work_pending && slot->generation == generation) {
slot->work_pending = false;
slot->tx_length = 0U;
slot->close_requested = true;
++s_counters.queue_failures;
}
taskEXIT_CRITICAL(&s_lock);
notify_transport_task();
}
return result;
}
static const char *writer_event_name(session_broker_event_type_t type)
{
switch (type) {
case SESSION_BROKER_EVENT_WRITER_GRANTED:
return "granted";
case SESSION_BROKER_EVENT_WRITER_RELEASED:
return "released";
case SESSION_BROKER_EVENT_WRITER_REVOKED:
return "revoked";
case SESSION_BROKER_EVENT_WRITER_DENIED:
return "denied";
default:
return NULL;
}
}
static bool prepare_hello(web_serial_slot_t *slot, uint32_t generation,
session_broker_client_id_t client_id)
{
session_broker_client_id_t writer_id = session_broker_get_writer_id();
bool writer = writer_id == client_id;
int written = snprintf(
(char *)slot->tx_data, sizeof(slot->tx_data),
"{\"type\":\"hello\",\"clientId\":%" PRIu32
",\"role\":\"%s\",\"writerId\":%" PRIu32 "}",
client_id, writer ? "writer" : "observer", writer_id);
if (written <= 0 || (size_t)written >= sizeof(slot->tx_data)) {
return false;
}
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation &&
slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->writer = writer;
slot->hello_pending = false;
}
taskEXIT_CRITICAL(&s_lock);
return queue_slot_frame(slot, generation, HTTPD_WS_TYPE_TEXT,
(size_t)written) == ESP_OK;
}
static bool prepare_writer_event(web_serial_slot_t *slot, uint32_t generation,
session_broker_client_id_t client_id)
{
session_broker_event_t event;
for (size_t count = 0U; count < SESSION_BROKER_EVENT_QUEUE_LENGTH; ++count) {
esp_err_t result = session_broker_pop_event(client_id, &event);
if (result == ESP_ERR_TIMEOUT) {
return false;
}
if (result != ESP_OK) {
if (result == ESP_ERR_NOT_FOUND) {
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation) {
slot->close_requested = true;
++s_counters.broker_failures;
}
taskEXIT_CRITICAL(&s_lock);
}
return false;
}
session_broker_client_id_t writer_id = session_broker_get_writer_id();
bool writer = writer_id == client_id;
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation &&
slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->writer = writer;
if (event.type == SESSION_BROKER_EVENT_WRITER_REVOKED &&
event.client_id == client_id) {
++s_counters.writer_revocations;
}
}
taskEXIT_CRITICAL(&s_lock);
const char *event_name = writer_event_name(event.type);
if (event_name == NULL) {
continue;
}
int written = snprintf(
(char *)slot->tx_data, sizeof(slot->tx_data),
"{\"type\":\"writer\",\"event\":\"%s\","
"\"sequence\":%" PRIu64 ",\"clientId\":%" PRIu32
",\"role\":\"%s\",\"writerId\":%" PRIu32 "}",
event_name, event.sequence, event.client_id,
writer ? "writer" : "observer", writer_id);
if (written <= 0 || (size_t)written >= sizeof(slot->tx_data)) {
taskENTER_CRITICAL(&s_lock);
++s_counters.protocol_errors;
if (slot->generation == generation) {
slot->close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
return false;
}
return queue_slot_frame(slot, generation, HTTPD_WS_TYPE_TEXT,
(size_t)written) == ESP_OK;
}
return false;
}
static bool prepare_writer_sync(web_serial_slot_t *slot, uint32_t generation,
session_broker_client_id_t client_id)
{
session_broker_client_id_t writer_id = session_broker_get_writer_id();
bool writer = writer_id == client_id;
bool changed = false;
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation &&
slot->state == WEB_SERIAL_SLOT_ACTIVE && slot->writer != writer) {
slot->writer = writer;
changed = true;
}
taskEXIT_CRITICAL(&s_lock);
if (!changed) {
return false;
}
int written = snprintf(
(char *)slot->tx_data, sizeof(slot->tx_data),
"{\"type\":\"writer\",\"event\":\"sync\",\"sequence\":0,"
"\"clientId\":%" PRIu32 ",\"role\":\"%s\",\"writerId\":%" PRIu32 "}",
client_id, writer ? "writer" : "observer", writer_id);
if (written <= 0 || (size_t)written >= sizeof(slot->tx_data)) {
add_counter(&s_counters.protocol_errors, 1U);
return false;
}
return queue_slot_frame(slot, generation, HTTPD_WS_TYPE_TEXT,
(size_t)written) == ESP_OK;
}
static void drain_binary_output(web_serial_slot_t *slot, uint32_t generation,
session_broker_client_id_t client_id)
{
size_t received = 0U;
esp_err_t result = session_broker_read(client_id, slot->tx_data,
sizeof(slot->tx_data), &received);
if (result == ESP_OK && received > 0U) {
(void)queue_slot_frame(slot, generation, HTTPD_WS_TYPE_BINARY, received);
} else if (result == ESP_ERR_NOT_FOUND) {
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation) {
slot->close_requested = true;
++s_counters.broker_failures;
}
taskEXIT_CRITICAL(&s_lock);
} else if (result != ESP_OK && result != ESP_ERR_TIMEOUT) {
add_counter(&s_counters.broker_failures, 1U);
}
}
static void process_close_request(web_serial_slot_t *slot)
{
httpd_handle_t server = NULL;
int socket_fd = -1;
uint32_t generation = 0U;
int64_t now_us = monotonic_time_us();
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_ACTIVE && slot->close_requested &&
!slot->close_triggered && slot->close_retry_at_us <= now_us &&
slot->server != NULL && slot->server == s_server) {
slot->close_triggered = true;
server = slot->server;
socket_fd = slot->socket_fd;
generation = slot->generation;
++s_counters.close_requests;
++s_httpd_close_operations;
}
taskEXIT_CRITICAL(&s_lock);
if (server == NULL) {
return;
}
esp_err_t result = httpd_sess_trigger_close(server, socket_fd);
bool session_gone = false;
if (result != ESP_OK) {
session_gone = result == ESP_ERR_NOT_FOUND ||
httpd_sess_get_ctx(server, socket_fd) != slot;
}
taskENTER_CRITICAL(&s_lock);
if (result != ESP_OK && slot->generation == generation &&
slot->state == WEB_SERIAL_SLOT_ACTIVE) {
++s_counters.queue_failures;
if (session_gone) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
slot->writer = false;
slot->hello_pending = false;
slot->work_pending = false;
slot->tx_length = 0U;
++s_counters.disconnections;
} else {
/* The nonblocking HTTPD control queue was full; retry later. */
slot->close_triggered = false;
slot->close_retry_at_us = now_us + 100000LL;
}
}
if (s_httpd_close_operations > 0U) {
--s_httpd_close_operations;
}
taskEXIT_CRITICAL(&s_lock);
notify_transport_task();
}
static void process_broker_disconnect(web_serial_slot_t *slot)
{
session_broker_client_id_t client_id = SESSION_BROKER_NO_CLIENT;
uint32_t generation = 0U;
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_CLOSING && !slot->disconnect_busy &&
slot->broker_client_id != SESSION_BROKER_NO_CLIENT) {
slot->disconnect_busy = true;
client_id = slot->broker_client_id;
generation = slot->generation;
}
taskEXIT_CRITICAL(&s_lock);
if (client_id == SESSION_BROKER_NO_CLIENT) {
return;
}
esp_err_t result = session_broker_disconnect(client_id);
taskENTER_CRITICAL(&s_lock);
if (slot->state == WEB_SERIAL_SLOT_CLOSING &&
slot->generation == generation && slot->broker_client_id == client_id) {
slot->disconnect_busy = false;
if (result == ESP_OK || result == ESP_ERR_NOT_FOUND) {
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
} else {
++s_counters.broker_failures;
}
finish_closing_slot_locked(slot);
}
taskEXIT_CRITICAL(&s_lock);
}
static void process_active_output(web_serial_slot_t *slot)
{
uint32_t generation;
session_broker_client_id_t client_id;
bool active;
bool hello_pending;
taskENTER_CRITICAL(&s_lock);
active = slot->state == WEB_SERIAL_SLOT_ACTIVE &&
!slot->work_pending && !slot->close_requested &&
slot->server == s_server;
generation = slot->generation;
client_id = slot->broker_client_id;
hello_pending = slot->hello_pending;
taskEXIT_CRITICAL(&s_lock);
if (!active || client_id == SESSION_BROKER_NO_CLIENT) {
return;
}
if (hello_pending) {
if (!prepare_hello(slot, generation, client_id)) {
taskENTER_CRITICAL(&s_lock);
if (slot->generation == generation &&
slot->state == WEB_SERIAL_SLOT_ACTIVE &&
!slot->work_pending) {
slot->close_requested = true;
}
taskEXIT_CRITICAL(&s_lock);
}
return;
}
if (prepare_writer_event(slot, generation, client_id)) {
return;
}
if (prepare_writer_sync(slot, generation, client_id)) {
return;
}
taskENTER_CRITICAL(&s_lock);
active = slot->state == WEB_SERIAL_SLOT_ACTIVE &&
slot->generation == generation && !slot->work_pending &&
!slot->close_requested;
taskEXIT_CRITICAL(&s_lock);
if (active) {
drain_binary_output(slot, generation, client_id);
}
}
static void transport_task(void *context)
{
(void)context;
for (;;) {
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
++index) {
web_serial_slot_t *slot = &s_slots[index];
process_close_request(slot);
process_broker_disconnect(slot);
process_active_output(slot);
}
(void)ulTaskNotifyTake(pdTRUE,
milliseconds_to_ticks(WEB_SERIAL_POLL_MS));
}
}
esp_err_t web_serial_transport_init(void)
{
#if defined(CONFIG_HTTPD_QUEUE_WORK_BLOCKING) && CONFIG_HTTPD_QUEUE_WORK_BLOCKING
return ESP_ERR_NOT_SUPPORTED;
#else
taskENTER_CRITICAL(&s_lock);
if (s_initialized || s_transport_task != NULL) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
memset(s_tickets, 0, sizeof(s_tickets));
memset(s_slots, 0, sizeof(s_slots));
memset(&s_counters, 0, sizeof(s_counters));
s_server = NULL;
s_httpd_close_operations = 0U;
s_inflight_handlers = 0U;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
s_slots[index].socket_fd = -1;
}
taskEXIT_CRITICAL(&s_lock);
TaskHandle_t task = xTaskCreateStatic(
transport_task, "web_serial", WEB_SERIAL_TASK_STACK_SIZE, NULL,
WEB_SERIAL_TASK_PRIORITY, s_transport_task_stack,
&s_transport_task_storage);
if (task == NULL) {
return ESP_ERR_NO_MEM;
}
taskENTER_CRITICAL(&s_lock);
s_transport_task = task;
s_initialized = true;
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
#endif
}
esp_err_t web_serial_transport_attach_server(httpd_handle_t server)
{
if (server == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t result = ESP_OK;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
result = ESP_ERR_INVALID_STATE;
} else if (s_server != NULL) {
result = ESP_ERR_INVALID_STATE;
} else {
clear_all_tickets_locked();
s_server = server;
}
taskEXIT_CRITICAL(&s_lock);
if (result == ESP_OK) {
notify_transport_task();
}
return result;
}
esp_err_t web_serial_transport_detach_server(httpd_handle_t server)
{
if (server == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t result = ESP_OK;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized || s_server != server) {
result = ESP_ERR_INVALID_STATE;
} else {
/*
* Revoke broker access before touching HTTPD. This keeps a failed server
* stop from retaining a writer lease or accepting post-detach input.
* Reserved generations remain owned by their in-progress handler, which
* observes the cleared server and disconnects unpublished broker state.
*/
s_server = NULL;
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->server != server) {
continue;
}
/* Queued work validates this flag before touching the socket. */
slot->work_pending = false;
slot->tx_length = 0U;
if (slot->state == WEB_SERIAL_SLOT_ACTIVE) {
slot->state = WEB_SERIAL_SLOT_CLOSING;
slot->writer = false;
slot->hello_pending = false;
slot->close_requested = false;
slot->close_triggered = true;
slot->close_retry_at_us = 0;
++s_counters.disconnections;
}
}
}
taskEXIT_CRITICAL(&s_lock);
if (result == ESP_OK) {
notify_transport_task();
/*
* No admitted handler or broker writer may outlive detach. HTTPD sends
* can finish later, but their static buffers cannot be reused until the
* server itself has stopped.
*/
int64_t detach_deadline =
monotonic_time_us() + WEB_SERIAL_DETACH_TIMEOUT_US;
for (;;) {
bool broker_sessions_gone = true;
taskENTER_CRITICAL(&s_lock);
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
++index) {
const web_serial_slot_t *slot = &s_slots[index];
if (slot->server == server &&
slot->broker_client_id != SESSION_BROKER_NO_CLIENT) {
broker_sessions_gone = false;
break;
}
}
bool operations_done = s_httpd_close_operations == 0U &&
s_inflight_handlers == 0U;
bool quiescent = operations_done && broker_sessions_gone;
taskEXIT_CRITICAL(&s_lock);
if (quiescent) {
break;
}
if (operations_done && monotonic_time_us() >= detach_deadline) {
session_broker_client_id_t writer_id =
session_broker_get_writer_id();
bool web_writer = false;
taskENTER_CRITICAL(&s_lock);
for (size_t index = 0U;
index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
const web_serial_slot_t *slot = &s_slots[index];
if (slot->server == server &&
slot->broker_client_id == writer_id) {
web_writer = writer_id != SESSION_BROKER_NO_CLIENT;
break;
}
}
taskEXIT_CRITICAL(&s_lock);
if (web_writer) {
(void)session_broker_force_writer(SESSION_BROKER_NO_CLIENT);
}
result = ESP_ERR_TIMEOUT;
break;
}
notify_transport_task();
vTaskDelay(milliseconds_to_ticks(1U));
}
}
return result;
}
esp_err_t web_serial_transport_mint_ticket(char *ticket, size_t capacity)
{
if (ticket == NULL || capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
return ESP_ERR_INVALID_ARG;
}
ticket[0] = '\0';
uint8_t random_bytes[WEB_SERIAL_RANDOM_BYTES] = {0};
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
uint32_t generation = 0U;
esp_err_t result = current_material_generation(&generation);
if (result == ESP_OK) {
result = secure_random_fill(random_bytes, sizeof(random_bytes));
}
if (result == ESP_OK) {
encode_base64url_24(random_bytes, ticket);
result = sha256_ticket(ticket, digest);
}
if (result != ESP_OK) {
ticket[0] = '\0';
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, generation, true);
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
int64_t oldest_expiry = INT64_MAX;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS;
++index) {
web_serial_ticket_t *entry = &s_tickets[index];
if (!entry->active) {
selected = index;
break;
}
if (entry->expires_at_us < oldest_expiry) {
oldest_expiry = entry->expires_at_us;
selected = index;
}
}
if (selected < WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
web_serial_ticket_t *entry = &s_tickets[selected];
clear_ticket_locked(entry);
memcpy(entry->digest, digest, sizeof(entry->digest));
entry->expires_at_us =
now_us + (int64_t)WEB_SERIAL_TRANSPORT_TICKET_LIFETIME_SECONDS *
1000000LL;
entry->material_generation = generation;
entry->active = true;
++s_counters.tickets_issued;
stored = true;
}
}
taskEXIT_CRITICAL(&s_lock);
if (!stored) {
ticket[0] = '\0';
result = ESP_ERR_NO_MEM;
}
cleanup:
secure_wipe(random_bytes, sizeof(random_bytes));
secure_wipe(digest, sizeof(digest));
return result;
}
esp_err_t web_serial_transport_handle_authenticated_ticket_request(
httpd_req_t *request)
{
if (request == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (request->method != HTTP_POST || request->content_len != 0U ||
validate_origin(request) != ESP_OK) {
return ESP_ERR_INVALID_ARG;
}
bool attached_request;
taskENTER_CRITICAL(&s_lock);
attached_request = s_initialized && s_server == request->handle;
taskEXIT_CRITICAL(&s_lock);
if (!attached_request) {
return ESP_ERR_INVALID_STATE;
}
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
char response[WEB_SERIAL_TICKET_RESPONSE_CAPACITY];
esp_err_t result = web_serial_transport_mint_ticket(ticket, sizeof(ticket));
if (result != ESP_OK) {
secure_wipe(ticket, sizeof(ticket));
return result;
}
int written = snprintf(
response, sizeof(response),
"{\"ticket\":\"%s\",\"expiresIn\":%u}\n", ticket,
(unsigned int)WEB_SERIAL_TRANSPORT_TICKET_LIFETIME_SECONDS);
if (written <= 0 || (size_t)written >= sizeof(response)) {
secure_wipe(ticket, sizeof(ticket));
secure_wipe(response, sizeof(response));
return ESP_FAIL;
}
result = httpd_resp_set_type(request, "application/json; charset=utf-8");
if (result == ESP_OK) {
result = httpd_resp_set_hdr(request, "Cache-Control", "no-store");
}
if (result == ESP_OK) {
result = httpd_resp_set_hdr(request, "X-Content-Type-Options", "nosniff");
}
if (result == ESP_OK) {
result = httpd_resp_send(request, response, written);
}
secure_wipe(ticket, sizeof(ticket));
secure_wipe(response, sizeof(response));
return result;
}
esp_err_t web_serial_transport_ws_handler(httpd_req_t *request)
{
if (request == NULL || request->handle == NULL) {
return ESP_ERR_INVALID_ARG;
}
int socket_fd = httpd_req_to_sockfd(request);
if (socket_fd < 0) {
return ESP_FAIL;
}
httpd_ws_client_info_t info =
httpd_ws_get_fd_info(request->handle, socket_fd);
if (info == HTTPD_WS_CLIENT_HTTP) {
(void)send_plain_bad_request(request);
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
if (info != HTTPD_WS_CLIENT_WEBSOCKET) {
return ESP_FAIL;
}
taskENTER_CRITICAL(&s_lock);
bool admitted = s_initialized && s_server == request->handle;
if (admitted) {
++s_inflight_handlers;
}
taskEXIT_CRITICAL(&s_lock);
if (!admitted) {
return ESP_FAIL;
}
esp_err_t result;
if (request->sess_ctx == NULL) {
/* IDF has already sent 101; authentication failures must only close. */
result = connect_websocket(request, socket_fd);
} else {
result = process_websocket_frame(request);
}
taskENTER_CRITICAL(&s_lock);
if (s_inflight_handlers > 0U) {
--s_inflight_handlers;
}
taskEXIT_CRITICAL(&s_lock);
return result;
}
esp_err_t web_serial_transport_get_snapshot(
web_serial_transport_snapshot_t *snapshot)
{
if (snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
uint32_t generation = 0U;
bool have_generation = current_material_generation(&generation) == ESP_OK;
int64_t now_us = monotonic_time_us();
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
purge_tickets_locked(now_us, generation, have_generation);
memset(snapshot, 0, sizeof(*snapshot));
snapshot->initialized = true;
snapshot->server_attached = s_server != NULL;
snapshot->counters = s_counters;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
if (s_tickets[index].active) {
++snapshot->active_tickets;
}
}
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
const web_serial_slot_t *slot = &s_slots[index];
web_serial_transport_session_snapshot_t *session =
&snapshot->sessions[index];
if (slot->state != WEB_SERIAL_SLOT_ACTIVE) {
session->socket_fd = -1;
continue;
}
session->active = true;
session->writer = slot->writer;
session->tx_pending = slot->work_pending;
session->close_requested = slot->close_requested;
session->socket_fd = slot->socket_fd;
session->generation = slot->generation;
session->broker_client_id = slot->broker_client_id;
++snapshot->active_sessions;
}
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
esp_err_t web_serial_transport_clear_counters(void)
{
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
memset(&s_counters, 0, sizeof(s_counters));
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
esp_err_t web_serial_transport_revoke_sessions(void)
{
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
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) {
slot->close_requested = true;
}
}
taskEXIT_CRITICAL(&s_lock);
notify_transport_task();
return ESP_OK;
}