- Require current admin cookie sessions, Origin checks and single-use tickets - Reuse the shared console with session-aware authorization and slot allocation - Add HTTPD-owned I/O, bounded buffering and revocation cleanup - Prevent LRU eviction of serial clients and stale admin socket closure - Reject unsupported web-shell mutations before side effects - Add host regressions, a smoke client and resource accounting Validated by user sign-off after a 15-minute full-client soak at 230400 baud, with a few broker drops under heavy output. Browser UI remains for Phase 8D.6; numeric memory reserves remain open.
1934 lines
65 KiB
C
1934 lines
65 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_auth_parse.h"
|
|
#include "web_httpd_adapter.h"
|
|
#include "web_admin_transport.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 4U
|
|
#define WEB_SERIAL_ACTIVE_BURST_LOOPS 8U
|
|
#define WEB_SERIAL_POLL_MS 5U
|
|
#define WEB_SERIAL_CURRENTNESS_INTERVAL_US 250000LL
|
|
#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;
|
|
user_principal_t principal;
|
|
web_session_id_t web_session_id;
|
|
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;
|
|
user_principal_t principal;
|
|
web_session_id_t web_session_id;
|
|
int64_t next_currentness_check_us;
|
|
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;
|
|
/* Cancels ticket publication across revocation and server detach/re-attach.
|
|
* Never wraps: exhaustion disables minting for the remainder of the boot. */
|
|
static uint64_t s_ticket_epoch;
|
|
|
|
static esp_err_t identity_is_current(const user_principal_t *principal,
|
|
web_session_id_t id, bool *current)
|
|
{
|
|
return web_session_store_check_principal(id, principal, current);
|
|
}
|
|
|
|
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));
|
|
secure_wipe(&ticket->principal, sizeof(ticket->principal));
|
|
ticket->expires_at_us = 0;
|
|
ticket->web_session_id = 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)
|
|
{
|
|
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
|
|
web_serial_ticket_t *ticket = &s_tickets[index];
|
|
if (ticket->active && ticket->expires_at_us <= now_us) {
|
|
clear_ticket_locked(ticket);
|
|
++s_counters.tickets_expired;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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_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;
|
|
}
|
|
|
|
bool matches = web_auth_parse_origin(host, strlen(host), origin, strlen(origin), expected);
|
|
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,
|
|
web_session_id_t web_session_id,
|
|
user_principal_t *principal, bool *consumed)
|
|
{
|
|
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
|
|
user_principal_t candidate = {0};
|
|
bool ticket_found = false;
|
|
*consumed = false;
|
|
memset(principal, 0, sizeof(*principal));
|
|
|
|
esp_err_t 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);
|
|
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->web_session_id == web_session_id) {
|
|
candidate = entry->principal;
|
|
clear_ticket_locked(entry);
|
|
ticket_found = true;
|
|
}
|
|
}
|
|
if (!ticket_found && matching_count > 1U) {
|
|
/* A defensive duplicate can never become a repeatedly usable ticket. */
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
|
|
if (ticket_found) {
|
|
bool current = false;
|
|
result = identity_is_current(&candidate, web_session_id, ¤t);
|
|
if (result == ESP_OK && current) {
|
|
*principal = candidate;
|
|
*consumed = true;
|
|
add_counter(&s_counters.tickets_consumed, 1U);
|
|
}
|
|
}
|
|
if (!*consumed) {
|
|
add_counter(&s_counters.tickets_rejected, 1U);
|
|
}
|
|
|
|
secure_wipe(&candidate, sizeof(candidate));
|
|
secure_wipe(digest, sizeof(digest));
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
secure_wipe(&slot->principal, sizeof(slot->principal));
|
|
slot->web_session_id = 0U;
|
|
slot->next_currentness_check_us = 0;
|
|
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;
|
|
secure_wipe(&slot->principal, sizeof(slot->principal));
|
|
slot->web_session_id = 0U;
|
|
slot->next_currentness_check_us = 0;
|
|
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;
|
|
secure_wipe(&slot->principal, sizeof(slot->principal));
|
|
slot->next_currentness_check_us = 0;
|
|
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,
|
|
web_session_id_t web_session_id)
|
|
{
|
|
char ticket[WEB_SERIAL_TRANSPORT_TICKET_CAPACITY] = {0};
|
|
uint32_t slot_generation = 0U;
|
|
web_serial_slot_t *slot = NULL;
|
|
user_principal_t principal = {0};
|
|
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, web_session_id, &principal, &consumed);
|
|
if (result != ESP_OK || !consumed) {
|
|
release_reserved_slot(slot, slot_generation);
|
|
result = ESP_FAIL;
|
|
goto cleanup;
|
|
}
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool principal_staged = slot->state == WEB_SERIAL_SLOT_RESERVED &&
|
|
slot->generation == slot_generation &&
|
|
!slot->close_requested;
|
|
if (principal_staged) {
|
|
slot->principal = principal;
|
|
slot->web_session_id = web_session_id;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!principal_staged) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
bool principal_current = false;
|
|
result = identity_is_current(&principal, web_session_id, &principal_current);
|
|
if (result != ESP_OK || !principal_current) {
|
|
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;
|
|
}
|
|
|
|
principal_current = false;
|
|
result = identity_is_current(&principal, web_session_id, &principal_current);
|
|
if (result != ESP_OK || !principal_current) {
|
|
close_unpublished_broker_session(slot, slot_generation, client_id);
|
|
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;
|
|
}
|
|
|
|
principal_current = false;
|
|
result = identity_is_current(&principal, web_session_id, &principal_current);
|
|
if (result != ESP_OK || !principal_current) {
|
|
close_unpublished_broker_session(slot, slot_generation, client_id);
|
|
result = ESP_FAIL;
|
|
goto cleanup;
|
|
}
|
|
|
|
bool activated = false;
|
|
/* Ticket and principal admission must succeed before HTTP 101. */
|
|
result = web_httpd_upgrade(request, web_serial_transport_ws_handler);
|
|
if (result != ESP_OK) {
|
|
close_unpublished_broker_session(slot, slot_generation, client_id);
|
|
goto cleanup;
|
|
}
|
|
int64_t next_currentness_check_us =
|
|
monotonic_time_us() + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (slot->state == WEB_SERIAL_SLOT_RESERVED &&
|
|
slot->generation == slot_generation && !slot->close_requested &&
|
|
s_server == request->handle) {
|
|
slot->state = WEB_SERIAL_SLOT_ACTIVE;
|
|
slot->broker_client_id = client_id;
|
|
slot->principal = principal;
|
|
slot->next_currentness_check_us = next_currentness_check_us;
|
|
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(&principal, sizeof(principal));
|
|
secure_wipe(ticket, sizeof(ticket));
|
|
return result;
|
|
}
|
|
|
|
static bool capture_active_session(httpd_req_t *request, web_serial_slot_t **slot_out,
|
|
uint32_t *generation,
|
|
session_broker_client_id_t *client_id,
|
|
user_principal_t *principal,
|
|
web_session_id_t *web_session_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;
|
|
*principal = slot->principal;
|
|
*web_session_id = slot->web_session_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;
|
|
user_principal_t principal = {0};
|
|
web_session_id_t web_session_id = 0U;
|
|
if (!capture_active_session(request, &slot, &generation, &client_id,
|
|
&principal, &web_session_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) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
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) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
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)) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
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) {
|
|
secure_wipe(&principal, sizeof(principal));
|
|
return reject_protocol_frame(slot, generation, frame.len);
|
|
}
|
|
|
|
bool current = false;
|
|
esp_err_t currentness_result =
|
|
identity_is_current(&principal, web_session_id, ¤t);
|
|
secure_wipe(&principal, sizeof(principal));
|
|
if (currentness_result != ESP_OK || !current) {
|
|
request_handler_close(slot, generation);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool still_active = slot->state == WEB_SERIAL_SLOT_ACTIVE &&
|
|
!slot->close_requested &&
|
|
slot->generation == generation &&
|
|
slot->broker_client_id == client_id &&
|
|
slot->server == request->handle &&
|
|
slot->server == s_server;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!still_active) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
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;
|
|
bool retired = false;
|
|
|
|
taskENTER_CRITICAL(&s_lock);
|
|
bool owned_work = slot->work_pending && work == &slot->work &&
|
|
work->generation == slot->generation &&
|
|
work->server == slot->server &&
|
|
work->socket_fd == slot->socket_fd;
|
|
valid = owned_work && slot->state == WEB_SERIAL_SLOT_ACTIVE &&
|
|
!slot->close_requested;
|
|
server = work->server;
|
|
socket_fd = work->socket_fd;
|
|
generation = work->generation;
|
|
type = slot->tx_type;
|
|
length = slot->tx_length;
|
|
if (owned_work && !valid) {
|
|
slot->work_pending = false;
|
|
slot->tx_length = 0U;
|
|
finish_closing_slot_locked(slot);
|
|
retired = true;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!valid) {
|
|
if (retired) {
|
|
notify_transport_task();
|
|
}
|
|
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;
|
|
secure_wipe(&slot->principal, sizeof(slot->principal));
|
|
slot->next_currentness_check_us = 0;
|
|
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_principal_currentness(web_serial_slot_t *slot)
|
|
{
|
|
user_principal_t principal = {0};
|
|
web_session_id_t web_session_id = 0U;
|
|
uint32_t generation = 0U;
|
|
bool check = false;
|
|
int64_t now_us = monotonic_time_us();
|
|
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (slot->state == WEB_SERIAL_SLOT_ACTIVE && !slot->close_requested &&
|
|
slot->next_currentness_check_us <= now_us) {
|
|
generation = slot->generation;
|
|
principal = slot->principal;
|
|
web_session_id = slot->web_session_id;
|
|
slot->next_currentness_check_us =
|
|
now_us + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
|
|
check = true;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (!check) {
|
|
return;
|
|
}
|
|
|
|
bool current = false;
|
|
esp_err_t result = identity_is_current(&principal, web_session_id, ¤t);
|
|
secure_wipe(&principal, sizeof(principal));
|
|
if (result == ESP_OK && current) {
|
|
return;
|
|
}
|
|
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (slot->state == WEB_SERIAL_SLOT_ACTIVE &&
|
|
slot->generation == generation) {
|
|
slot->close_requested = true;
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
|
|
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;
|
|
size_t active_burst_loops = 0U;
|
|
|
|
for (;;) {
|
|
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS;
|
|
++index) {
|
|
web_serial_slot_t *slot = &s_slots[index];
|
|
process_principal_currentness(slot);
|
|
process_close_request(slot);
|
|
process_broker_disconnect(slot);
|
|
process_active_output(slot);
|
|
}
|
|
|
|
uint32_t notifications = ulTaskNotifyTake(
|
|
pdTRUE, milliseconds_to_ticks(WEB_SERIAL_POLL_MS));
|
|
if (notifications == 0U) {
|
|
active_burst_loops = 0U;
|
|
} else if (++active_burst_loops >= WEB_SERIAL_ACTIVE_BURST_LOOPS) {
|
|
active_burst_loops = 0U;
|
|
/*
|
|
* Send completion normally wakes this task immediately. Bound that
|
|
* producer/HTTPD hand-off so sustained serial output cannot keep an
|
|
* application task runnable forever and starve an idle watchdog.
|
|
*/
|
|
vTaskDelay(1U);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (s_ticket_epoch != UINT64_MAX) {
|
|
++s_ticket_epoch;
|
|
}
|
|
}
|
|
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;
|
|
if (s_ticket_epoch != UINT64_MAX) {
|
|
++s_ticket_epoch;
|
|
}
|
|
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;
|
|
secure_wipe(&slot->principal, sizeof(slot->principal));
|
|
slot->next_currentness_check_us = 0;
|
|
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(const user_principal_t *principal,
|
|
web_session_id_t web_session_id,
|
|
char *ticket, size_t capacity)
|
|
{
|
|
if (principal == NULL || ticket == NULL ||
|
|
capacity < WEB_SERIAL_TRANSPORT_TICKET_CAPACITY) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
ticket[0] = '\0';
|
|
|
|
taskENTER_CRITICAL(&s_lock);
|
|
uint64_t epoch = s_ticket_epoch;
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
|
|
bool current = false;
|
|
esp_err_t result = identity_is_current(principal, web_session_id, ¤t);
|
|
if (result != ESP_OK) {
|
|
return result;
|
|
}
|
|
if (!current) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
/* Reclaim stale identities without database calls under the transport lock.
|
|
* A late result must not clear a ticket published into the same array slot. */
|
|
for (size_t i = 0; i < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++i) {
|
|
taskENTER_CRITICAL(&s_lock);
|
|
web_serial_ticket_t candidate = s_tickets[i];
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
bool live = false;
|
|
if (candidate.active &&
|
|
(identity_is_current(&candidate.principal, candidate.web_session_id, &live) != ESP_OK || !live)) {
|
|
taskENTER_CRITICAL(&s_lock);
|
|
web_serial_ticket_t *entry = &s_tickets[i];
|
|
if (entry->active && entry->web_session_id == candidate.web_session_id &&
|
|
entry->expires_at_us == candidate.expires_at_us &&
|
|
constant_time_equal(entry->digest, candidate.digest, sizeof(entry->digest)))
|
|
clear_ticket_locked(entry);
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
}
|
|
secure_wipe(&candidate, sizeof(candidate));
|
|
}
|
|
|
|
uint8_t random_bytes[WEB_SERIAL_RANDOM_BYTES] = {0};
|
|
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
|
|
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;
|
|
}
|
|
|
|
current = false;
|
|
result = identity_is_current(principal, web_session_id, ¤t);
|
|
if (result != ESP_OK || !current) {
|
|
result = ESP_ERR_INVALID_STATE;
|
|
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 && epoch == s_ticket_epoch &&
|
|
epoch != UINT64_MAX) {
|
|
purge_tickets_locked(now_us);
|
|
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
|
|
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS;
|
|
++index) {
|
|
web_serial_ticket_t *entry = &s_tickets[index];
|
|
if (!entry->active) {
|
|
selected = index;
|
|
break;
|
|
}
|
|
}
|
|
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->principal = *principal;
|
|
entry->web_session_id = web_session_id;
|
|
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, const user_principal_t *principal,
|
|
web_session_id_t web_session_id)
|
|
{
|
|
if (request == NULL || principal == 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(
|
|
principal, web_session_id, 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_set_hdr(request, "Referrer-Policy", "no-referrer");
|
|
}
|
|
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)
|
|
{
|
|
return web_serial_transport_session_ws_handler(request, 0U);
|
|
}
|
|
|
|
esp_err_t web_serial_transport_session_ws_handler(httpd_req_t *request,
|
|
web_session_id_t web_session_id)
|
|
{
|
|
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);
|
|
bool opening = request->sess_ctx == NULL && web_session_id != 0U &&
|
|
request->method == HTTP_GET && web_httpd_upgrade_requested(request);
|
|
if (info == HTTPD_WS_CLIENT_HTTP && !opening) {
|
|
(void)send_plain_bad_request(request);
|
|
add_counter(&s_counters.protocol_errors, 1U);
|
|
return ESP_FAIL;
|
|
}
|
|
if (info != HTTPD_WS_CLIENT_WEBSOCKET && !opening) {
|
|
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) {
|
|
/* The registered HTTP route defers 101 until admission. */
|
|
result = connect_websocket(request, socket_fd, web_session_id);
|
|
} 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;
|
|
}
|
|
|
|
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);
|
|
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->principal_valid = 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;
|
|
session->user_role = slot->principal.role;
|
|
session->auth_method = slot->principal.method;
|
|
memcpy(session->username, slot->principal.username,
|
|
slot->principal.username_length);
|
|
++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_user(const uint8_t *username,
|
|
size_t username_length)
|
|
{
|
|
if (!user_database_username_valid(username, username_length)) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
web_session_store_invalidate_username(username, username_length);
|
|
web_admin_transport_revoke(0, username, username_length);
|
|
bool notify = false;
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_ticket_epoch != UINT64_MAX) {
|
|
++s_ticket_epoch;
|
|
}
|
|
if (!s_initialized) {
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++index) {
|
|
web_serial_ticket_t *ticket = &s_tickets[index];
|
|
if (ticket->active && ticket->principal.username_length == username_length &&
|
|
memcmp(ticket->principal.username, username, username_length) == 0) {
|
|
clear_ticket_locked(ticket);
|
|
}
|
|
}
|
|
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
|
|
web_serial_slot_t *slot = &s_slots[index];
|
|
if ((slot->state == WEB_SERIAL_SLOT_RESERVED ||
|
|
slot->state == WEB_SERIAL_SLOT_ACTIVE) &&
|
|
slot->principal.username_length == username_length &&
|
|
memcmp(slot->principal.username, username, username_length) == 0) {
|
|
slot->close_requested = true;
|
|
notify = true;
|
|
}
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
if (notify) {
|
|
notify_transport_task();
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t web_serial_transport_revoke_sessions(void)
|
|
{
|
|
web_session_store_invalidate_username(NULL, 0U);
|
|
web_admin_transport_revoke(0, NULL, 0);
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_ticket_epoch != UINT64_MAX) {
|
|
++s_ticket_epoch;
|
|
}
|
|
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->state == WEB_SERIAL_SLOT_RESERVED) {
|
|
slot->close_requested = true;
|
|
}
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
notify_transport_task();
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t web_serial_transport_revoke_web_session(web_session_id_t id)
|
|
{
|
|
if (id == 0U) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
web_session_store_invalidate(id);
|
|
web_admin_transport_revoke(id, NULL, 0);
|
|
taskENTER_CRITICAL(&s_lock);
|
|
if (s_ticket_epoch != UINT64_MAX) {
|
|
++s_ticket_epoch;
|
|
}
|
|
for (size_t i = 0U; i < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++i) {
|
|
if (s_tickets[i].active && s_tickets[i].web_session_id == id) {
|
|
clear_ticket_locked(&s_tickets[i]);
|
|
}
|
|
}
|
|
for (size_t i = 0U; i < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++i) {
|
|
web_serial_slot_t *slot = &s_slots[i];
|
|
if ((slot->state == WEB_SERIAL_SLOT_RESERVED ||
|
|
slot->state == WEB_SERIAL_SLOT_ACTIVE) && slot->web_session_id == id) {
|
|
slot->close_requested = true;
|
|
}
|
|
}
|
|
taskEXIT_CRITICAL(&s_lock);
|
|
notify_transport_task();
|
|
return ESP_OK;
|
|
}
|