feat: add bounded admin WebSocket backend (Phase 8D.5)

- 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.
This commit is contained in:
2026-09-06 14:41:41 +02:00
parent e5dce12ed4
commit aeb2043396
37 changed files with 3651 additions and 91 deletions
+29
View File
@@ -15,6 +15,8 @@
#include "web_serial_transport.h"
#include "web_server.h"
#include "web_cookie_auth.h"
#include "web_admin_transport.h"
#include "web_admin_tickets.h"
static void print_usage(void)
{
@@ -35,6 +37,30 @@ static void print_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENG
}
}
static void show_admin_transport(void)
{
web_admin_transport_snapshot_t admin;
web_admin_tickets_snapshot_t tickets;
web_admin_transport_get_snapshot(&admin);
web_admin_tickets_get_snapshot(&tickets);
printf("WebSocket admin: initialized=%s attached=%s active=%s/1 closing=%s init-error=%s\n",
admin.initialized ? "yes" : "no", admin.attached ? "yes" : "no",
admin.active ? "yes" : "no", admin.closing ? "yes" : "no", esp_err_to_name(admin.last_error));
printf(" tickets=%" PRIu32 "/%u issued=%" PRIu32 " consumed=%" PRIu32
" rejected=%" PRIu32 " capacity=%" PRIu32 "\n",
tickets.active, WEB_ADMIN_TICKET_CAPACITY, tickets.issued, tickets.consumed,
tickets.rejected, tickets.capacity_rejections);
printf(" connected=%" PRIu32 " disconnected=%" PRIu32 " capacity=%" PRIu32
" authorization=%" PRIu32 " protocol=%" PRIu32 " input-backpressure=%" PRIu32 "\n",
admin.connections, admin.disconnections, admin.capacity_rejections,
admin.authorization_rejections, admin.protocol_errors, admin.input_backpressure);
printf(" rx-bytes=%" PRIu32 " tx-bytes=%" PRIu32 " send-failures=%" PRIu32
" queue-failures=%" PRIu32 " static=%u ticket-storage=%u PSRAM-payload=%u bytes\n",
admin.rx_bytes, admin.tx_bytes, admin.send_failures, admin.queue_failures,
(unsigned)admin.static_bytes, (unsigned)tickets.storage_bytes, (unsigned)admin.payload_bytes);
printf(" Admin counters are saturating lifetime counts (not reset by web clear-counters).\n");
}
static int show_status(void)
{
web_server_snapshot_t snapshot;
@@ -61,6 +87,8 @@ static int show_status(void)
}
printf("Endpoints: GET /, GET /api/status, POST /api/ws-ticket, WSS /ws/serial\n");
printf("Authentication routes: GET /login, GET /api/login-challenge, POST /api/login, GET /api/session, POST /api/logout\n");
printf("Admin-only backend: POST /api/admin/ws-ticket, WSS /ws/admin (no normal UI entry)\n");
show_admin_transport();
web_cookie_auth_snapshot_t auth;
web_cookie_auth_get_snapshot(&auth);
web_session_store_snapshot_t sessions;
@@ -122,6 +150,7 @@ static int show_counters(void)
return 1;
}
show_admin_transport();
const web_server_counters_t *counter = &snapshot.counters;
printf("Lifecycle: starts=%" PRIu64 " start-failures=%" PRIu64
" stops=%" PRIu64 "\n",