/* SPDX-License-Identifier: GPL-3.0-only */ /* Authenticated, bounded WebSocket transport for the serial session broker. */ #include "web_serial_transport.h" #include #include #include #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; }