405 lines
18 KiB
C
405 lines
18 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* HTTPS lifecycle and TLS certificate commands. */
|
|
|
|
#include "web_console.h"
|
|
#include "admin_ssh_console.h"
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "esp_console.h"
|
|
#include "user_database.h"
|
|
#include "web_security.h"
|
|
#include "web_serial_transport.h"
|
|
#include "web_server.h"
|
|
#include "web_cookie_auth.h"
|
|
#include "web_admin_transport.h"
|
|
#include "web_admin_tickets.h"
|
|
#include "web_diagnostics.h"
|
|
|
|
static void print_usage(void)
|
|
{
|
|
printf("Usage:\n");
|
|
printf(" web status|start|stop\n");
|
|
printf(" web counters|clear-counters\n");
|
|
printf(" web diagnostics enable|disable|show|clear\n");
|
|
printf(" web performance enable|disable|show|clear\n");
|
|
printf(" web certificate info\n");
|
|
printf(" web certificate rotate --force\n");
|
|
printf(" web reset --force (TLS certificate and private key only)\n");
|
|
}
|
|
|
|
static void print_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH])
|
|
{
|
|
for (size_t index = 0U; index < WEB_SECURITY_SHA256_LENGTH; ++index) {
|
|
printf(index == 0U ? "%02X" : ":%02X", (unsigned int)fingerprint[index]);
|
|
}
|
|
}
|
|
|
|
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;
|
|
esp_err_t error = web_server_get_snapshot(&snapshot);
|
|
if (error != ESP_OK) {
|
|
printf("HTTPS runtime unavailable: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
|
|
user_database_snapshot_t users;
|
|
esp_err_t users_error = user_database_get_snapshot(&users);
|
|
printf("HTTPS: initialized=%s running=%s transitioning=%s port=%u last-error=%s\n",
|
|
snapshot.initialized ? "yes" : "no",
|
|
snapshot.running ? "yes" : "no",
|
|
snapshot.transitioning ? "yes" : "no",
|
|
(unsigned int)snapshot.port,
|
|
esp_err_to_name(snapshot.last_error));
|
|
if (users_error == ESP_OK) {
|
|
printf("Authentication: HTTPS cookie sessions via user database, users=%u admins=%u\n",
|
|
(unsigned int)users.user_count, (unsigned int)users.admin_count);
|
|
} else {
|
|
printf("Authentication database unavailable: %s; use 'user recover --force'.\n",
|
|
esp_err_to_name(users_error));
|
|
}
|
|
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;
|
|
if (web_session_store_get_snapshot(&sessions) == ESP_OK)
|
|
printf("Cookie authentication: ready=%s sessions=%" PRIu32 "/4 challenges=%" PRIu32 "/4\n",
|
|
auth.ready ? "yes" : "no", sessions.active, auth.active_challenges);
|
|
printf("Login attempts=%" PRIu32 " invalid-credentials=%" PRIu32 " throttled=%" PRIu32
|
|
" auth-capacity-rejections=%" PRIu32 " CSRF/origin-rejections=%" PRIu32 " logouts=%" PRIu32 "\n",
|
|
auth.login_attempts, auth.login_failures, auth.throttled, auth.capacity_rejections,
|
|
auth.security_rejections, auth.logouts);
|
|
|
|
web_serial_transport_snapshot_t transport;
|
|
esp_err_t transport_error = web_serial_transport_get_snapshot(&transport);
|
|
if (transport_error != ESP_OK) {
|
|
printf("WebSocket serial transport unavailable: %s\n",
|
|
esp_err_to_name(snapshot.serial_transport_error));
|
|
return 0;
|
|
}
|
|
|
|
printf("WebSocket serial: attached=%s sessions=%" PRIu32 "/%u tickets=%" PRIu32 "\n",
|
|
transport.server_attached ? "yes" : "no",
|
|
transport.active_sessions,
|
|
WEB_SERIAL_TRANSPORT_MAX_SESSIONS,
|
|
transport.active_tickets);
|
|
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
|
|
const web_serial_transport_session_snapshot_t *session =
|
|
&transport.sessions[index];
|
|
if (!session->active) {
|
|
continue;
|
|
}
|
|
printf(" slot=%u fd=%d generation=%" PRIu32 " account=%s user-role=%s"
|
|
" method=%s broker=%" PRIu32
|
|
" broker-role=%s tx-pending=%s closing=%s\n",
|
|
(unsigned int)index,
|
|
session->socket_fd,
|
|
session->generation,
|
|
session->principal_valid ? session->username : "-",
|
|
session->principal_valid
|
|
? user_role_to_string(session->user_role)
|
|
: "-",
|
|
session->principal_valid &&
|
|
session->auth_method == USER_AUTH_METHOD_PASSWORD
|
|
? "password"
|
|
: "unknown",
|
|
session->broker_client_id,
|
|
session->writer ? "writer" : "observer",
|
|
session->tx_pending ? "yes" : "no",
|
|
session->close_requested ? "yes" : "no");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int show_counters(void)
|
|
{
|
|
web_server_snapshot_t snapshot;
|
|
esp_err_t error = web_server_get_snapshot(&snapshot);
|
|
if (error != ESP_OK) {
|
|
printf("Could not read HTTPS counters: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
|
|
show_admin_transport();
|
|
const web_server_counters_t *counter = &snapshot.counters;
|
|
printf("Lifecycle: starts=%" PRIu64 " start-failures=%" PRIu64
|
|
" stops=%" PRIu64 "\n",
|
|
counter->starts, counter->start_failures, counter->stops);
|
|
printf("Requests: total=%" PRIu64 " authenticated=%" PRIu64
|
|
" auth-failures=%" PRIu64 " root=%" PRIu64
|
|
" status=%" PRIu64 " tickets=%" PRIu64 " assets=%" PRIu64
|
|
" response-errors=%" PRIu64 "\n",
|
|
counter->requests, counter->authenticated_requests,
|
|
counter->authentication_failures, counter->root_requests,
|
|
counter->status_requests, counter->ticket_requests,
|
|
counter->asset_requests, counter->response_errors);
|
|
|
|
web_serial_transport_snapshot_t transport;
|
|
error = web_serial_transport_get_snapshot(&transport);
|
|
if (error != ESP_OK) {
|
|
printf("WebSocket serial counters unavailable: %s\n", esp_err_to_name(error));
|
|
return 0;
|
|
}
|
|
const web_serial_transport_counters_t *websocket = &transport.counters;
|
|
printf("Tickets: issued=%" PRIu64 " consumed=%" PRIu64
|
|
" rejected=%" PRIu64 " expired=%" PRIu64 "\n",
|
|
websocket->tickets_issued, websocket->tickets_consumed,
|
|
websocket->tickets_rejected, websocket->tickets_expired);
|
|
printf("WebSocket sessions: connect=%" PRIu64 " failures=%" PRIu64
|
|
" disconnect=%" PRIu64 " service-start-failures=%" PRIu64
|
|
" broker-failures=%" PRIu64 "\n",
|
|
websocket->connections, websocket->connection_failures,
|
|
websocket->disconnections, websocket->service_start_failures,
|
|
websocket->broker_failures);
|
|
printf("WebSocket RX: frames-ok=%" PRIu64 " frames-rejected=%" PRIu64
|
|
" bytes-ok=%" PRIu64 " bytes-rejected=%" PRIu64 "\n",
|
|
websocket->rx_ws_frames_accepted,
|
|
websocket->rx_ws_frames_rejected,
|
|
websocket->rx_ws_bytes_accepted,
|
|
websocket->rx_ws_bytes_rejected);
|
|
printf("WebSocket TX: binary-frames=%" PRIu64 " binary-bytes=%" PRIu64
|
|
" control-frames=%" PRIu64 " control-bytes=%" PRIu64 "\n",
|
|
websocket->tx_binary_frames, websocket->tx_binary_bytes,
|
|
websocket->tx_control_frames, websocket->tx_control_bytes);
|
|
printf("WebSocket control: writer-requests=%" PRIu64
|
|
" grants=%" PRIu64 " denials=%" PRIu64
|
|
" releases=%" PRIu64 " revocations=%" PRIu64 "\n",
|
|
websocket->writer_requests, websocket->writer_grants,
|
|
websocket->writer_denials, websocket->writer_releases,
|
|
websocket->writer_revocations);
|
|
printf("WebSocket failures: send=%" PRIu64 " queue=%" PRIu64
|
|
" protocol=%" PRIu64 " closes=%" PRIu64 "\n",
|
|
websocket->send_failures, websocket->queue_failures,
|
|
websocket->protocol_errors, websocket->close_requests);
|
|
return 0;
|
|
}
|
|
|
|
static int show_certificate(void)
|
|
{
|
|
web_security_certificate_metadata_t metadata;
|
|
esp_err_t error = web_security_get_certificate_metadata(&metadata);
|
|
if (error != ESP_OK) {
|
|
printf("Could not read certificate information: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
|
|
printf("Security material generation=%" PRIu32 " type=ECDSA-P256 signature=SHA-256\n",
|
|
metadata.material_generation);
|
|
printf("Subject/issuer CN: %s\n", metadata.common_name);
|
|
printf("Validity: %sZ to %sZ\n", metadata.not_before, metadata.not_after);
|
|
printf("SAN: DNS:%s, IP:%u.%u.%u.%u\n",
|
|
metadata.dns_name,
|
|
(unsigned int)metadata.ipv4_address[0],
|
|
(unsigned int)metadata.ipv4_address[1],
|
|
(unsigned int)metadata.ipv4_address[2],
|
|
(unsigned int)metadata.ipv4_address[3]);
|
|
printf("SHA-256 fingerprint: ");
|
|
print_fingerprint(metadata.sha256_fingerprint);
|
|
putchar('\n');
|
|
return 0;
|
|
}
|
|
|
|
static bool force_is_present(int argc, char **argv, int expected_argc)
|
|
{
|
|
return argc == expected_argc && strcmp(argv[expected_argc - 1], "--force") == 0;
|
|
}
|
|
|
|
static int replace_material(bool reset)
|
|
{
|
|
bool committed = false;
|
|
esp_err_t error = web_server_replace_identity(0, 0, reset, &committed);
|
|
if (error != ESP_OK) {
|
|
printf("%s: %s\n", committed
|
|
? "New HTTPS identity persisted, but stop/start failed; no rollback. Inspect via UART0 before retrying"
|
|
: "HTTPS identity replacement rejected or failed before publication",
|
|
esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("HTTPS certificate and private key replaced and persisted; user accounts unchanged.\n");
|
|
printf("Verify the new fingerprint via trusted UART0, renew browser trust, and sign in again.\n");
|
|
return 0;
|
|
}
|
|
|
|
static int rotate_certificate(void) { return replace_material(false); }
|
|
static int reset_material(void) { return replace_material(true); }
|
|
|
|
static void print_performance_time(const char *name, const web_serial_performance_timing_t *t)
|
|
{
|
|
printf(" %s: count=%" PRIu64 " sum_us=%" PRIu64 " avg_us_est=%" PRIu64 " max_us=%" PRIu64 "\n",
|
|
name, t->count, t->sum_us, t->count ? t->sum_us / t->count : 0, t->max_us);
|
|
}
|
|
|
|
static int performance_command(const char *action)
|
|
{
|
|
esp_err_t result = ESP_OK;
|
|
if (!strcmp(action, "enable")) result = web_serial_performance_enable(true);
|
|
else if (!strcmp(action, "disable")) result = web_serial_performance_enable(false);
|
|
else if (!strcmp(action, "clear")) result = web_serial_performance_clear();
|
|
else if (strcmp(action, "show")) return 1;
|
|
if (result != ESP_OK) {
|
|
printf("Web performance: %s\n", esp_err_to_name(result));
|
|
return 1;
|
|
}
|
|
web_serial_performance_snapshot_t s;
|
|
web_serial_performance_snapshot(&s);
|
|
printf("Web performance: enabled=%u epoch=%" PRIu32 " epoch_exhausted=%u; binary TX only\n",
|
|
s.enabled, s.epoch, s.epoch_exhausted);
|
|
printf("Send-call return is synchronous HTTPD-owner bytes send, not peer receipt. Timings are instrumented estimates; saturated=1 invalidates averages/count totals.\n");
|
|
printf("Completion->nonempty includes idle gaps; first-nonempty excludes observed empty attempts, not proof of backlog.\n");
|
|
for (unsigned i = 0; i < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++i) {
|
|
const web_serial_performance_session_t *r = &s.sessions[i];
|
|
if (!r->active) continue;
|
|
printf("slot=%u fd=%d generation=%" PRIu32 " broker=%" PRIu32
|
|
" pending=%u measured_pending=%u executing=%u pending_age_us=%" PRIu64 " saturated=%u\n",
|
|
i, r->socket_fd, r->generation, (uint32_t)r->broker_client_id,
|
|
r->pending, r->measured_pending, r->executing, r->pending_age_us, r->saturated);
|
|
printf(" queued_frames=%" PRIu64 " queued_bytes=%" PRIu64 " queue_errors=%" PRIu64
|
|
" sent_frames=%" PRIu64 " sent_bytes=%" PRIu64 " send_errors=%" PRIu64 " retired=%" PRIu64 "\n",
|
|
r->queued_frames, r->queued_bytes, r->queue_errors, r->sent_frames,
|
|
r->sent_bytes, r->send_errors, r->retired);
|
|
print_performance_time("queue->callback-entry", &r->queue_wait);
|
|
print_performance_time("send-call", &r->send_call);
|
|
print_performance_time("completion->first-drain-attempt-return", &r->completion_attempt);
|
|
print_performance_time("completion->next-nonempty-drain-return (includes idle)", &r->completion_nonempty);
|
|
print_performance_time("completion->first-attempt-nonempty-return", &r->completion_first_nonempty);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int command_web(int argc, char **argv)
|
|
{
|
|
if (argc == 1 || (argc == 2 && strcmp(argv[1], "help") == 0)) {
|
|
print_usage();
|
|
return 0;
|
|
}
|
|
if (argc == 3 && strcmp(argv[1], "performance") == 0) {
|
|
if (performance_command(argv[2]) == 0) return 0;
|
|
print_usage();
|
|
return 1;
|
|
}
|
|
if (argc == 3 && strcmp(argv[1], "diagnostics") == 0) {
|
|
if (web_diagnostics_command(argv[2]) == 0) return 0;
|
|
print_usage();
|
|
return 1;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "status") == 0) {
|
|
return show_status();
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "start") == 0) {
|
|
esp_err_t error = web_server_start();
|
|
if (error != ESP_OK) {
|
|
printf("Could not start HTTPS: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("HTTPS started on TCP port 443.\n");
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
|
|
if (admin_ssh_console_dispatch_is_web()) {
|
|
esp_err_t error = admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_WEB_STOP, 0U);
|
|
if (error != ESP_OK) {
|
|
printf("Could not schedule HTTPS stop: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("HTTPS stop scheduled after console output drains; both browser connections will close.\n");
|
|
return 0;
|
|
}
|
|
esp_err_t error = web_server_stop();
|
|
if (error != ESP_OK) {
|
|
printf("Could not stop HTTPS: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("HTTPS stopped.\n");
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
|
|
return show_counters();
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
|
|
esp_err_t error = web_server_clear_counters();
|
|
if (error == ESP_OK) {
|
|
error = web_serial_transport_clear_counters();
|
|
}
|
|
if (error != ESP_OK) {
|
|
printf("Could not clear web counters: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("HTTPS and WebSocket counters cleared.\n");
|
|
return 0;
|
|
}
|
|
if (argc == 3 && strcmp(argv[1], "certificate") == 0 &&
|
|
strcmp(argv[2], "info") == 0) {
|
|
return show_certificate();
|
|
}
|
|
if (strcmp(argv[1], "certificate") == 0 && argc >= 3 &&
|
|
strcmp(argv[2], "rotate") == 0) {
|
|
if (!force_is_present(argc, argv, 4)) {
|
|
printf("Certificate rotation requires: web certificate rotate --force\n");
|
|
return 1;
|
|
}
|
|
if (admin_ssh_console_dispatch_is_web()) {
|
|
esp_err_t error = admin_ssh_console_dispatch_defer(
|
|
ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE, 0U);
|
|
if (error != ESP_OK) {
|
|
printf("Could not schedule HTTPS certificate rotation: %s\n", esp_err_to_name(error));
|
|
return 1;
|
|
}
|
|
printf("HTTPS identity rotation scheduled after console output drains; all web logins and browser terminals will close. A new identity may persist even if stop/start fails; no rollback. Verify the new fingerprint via trusted UART0 web certificate info before renewing browser trust, then reload and sign in. SSH and USB UART1 access remain independent.\n");
|
|
return 0;
|
|
}
|
|
return rotate_certificate();
|
|
}
|
|
if (strcmp(argv[1], "reset") == 0) {
|
|
if (!force_is_present(argc, argv, 3)) {
|
|
printf("TLS-only certificate/private-key replacement requires: web reset --force\n");
|
|
return 1;
|
|
}
|
|
return reset_material();
|
|
}
|
|
|
|
print_usage();
|
|
return 1;
|
|
}
|
|
|
|
esp_err_t web_console_register_commands(void)
|
|
{
|
|
const esp_console_cmd_t command = {
|
|
.command = "web",
|
|
.help = "Manage authenticated HTTPS and recover TLS certificate/private key",
|
|
.hint = NULL,
|
|
.func = &command_web,
|
|
.argtable = NULL,
|
|
};
|
|
return esp_console_cmd_register(&command);
|
|
}
|