Replace Web Basic Auth With Cookie Sessions

Add bounded login challenges, CSRF/origin enforcement, logout, and
session-bound WebSocket admission. Isolate private HTTPD access behind a
version-guarded adapter and add focused host coverage. Also let empty
admin
SSH input reach the normal console handler.
This commit is contained in:
2026-09-05 23:55:05 +02:00
parent 4435a7fddd
commit 5a609fa40b
36 changed files with 1940 additions and 360 deletions
+2 -1
View File
@@ -62,7 +62,8 @@ def run():
*sanitizer,
"-I" + str(tmp), "-I" + str(ROOT / "src"),
"-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections",
str(HERE / ("serial_test.c" if serial else "test.c")), str(source), *crypto,
str(HERE / ("serial_test.c" if serial else "test.c")), str(source),
*([str(ROOT / "src/web_auth_parse.c")] if serial else []), *crypto,
"-o", str(tmp / "test")], check=True, timeout=30)
subprocess.run([str(tmp / "test")], check=True, timeout=10)
+2 -1
View File
@@ -24,12 +24,13 @@ TaskHandle_t xTaskCreateStatic(void (*)(void *), const char *, uint32_t, void *,
#include "esp_err.h"
typedef void *httpd_handle_t;
typedef struct { httpd_handle_t handle; void *sess_ctx; void (*free_ctx)(void *);
int method; size_t content_len; } httpd_req_t;
int method; size_t content_len; const char *uri; void *aux; } httpd_req_t;
typedef enum { HTTPD_WS_TYPE_CONTINUE, HTTPD_WS_TYPE_TEXT, HTTPD_WS_TYPE_BINARY } httpd_ws_type_t;
typedef enum { HTTPD_WS_CLIENT_HTTP, HTTPD_WS_CLIENT_WEBSOCKET } httpd_ws_client_info_t;
typedef struct { bool final, fragmented; httpd_ws_type_t type; unsigned char *payload;
size_t len; } httpd_ws_frame_t;
#define HTTP_POST 1
#define HTTP_GET 0
size_t httpd_req_get_url_query_len(httpd_req_t *);
esp_err_t httpd_req_get_url_query_str(httpd_req_t *, char *, size_t);
esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *, const char *, char *, size_t);
+25 -8
View File
@@ -10,15 +10,29 @@ static unsigned broker_connections, broker_disconnects, writes, closes;
static esp_err_t close_result = ESP_OK;
static httpd_req_t request = { .handle = (void *)1 };
static void (*connect_hook)(void);
esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *s) { (void)r; (void)s; return ESP_OK; }
esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *s) { (void)r; (void)s; return ESP_OK; }
esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *k, const char *v) { (void)r; (void)k; (void)v; return ESP_OK; }
esp_err_t httpd_resp_send(httpd_req_t *r, const char *s, int n) { (void)r; (void)s; (void)n; return ESP_OK; }
void xTaskNotifyGive(TaskHandle_t task) { (void)task; assert(!host_lock_depth); }
size_t httpd_req_get_url_query_len(httpd_req_t *r) { (void)r; return strlen(query); }
esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *out, size_t n) {
(void)r; assert(strlen(query) < n); strcpy(out, query); return ESP_OK;
}
esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *key, char *out, size_t n) {
(void)r; (void)key; (void)out; (void)n; return ESP_ERR_NOT_FOUND;
(void)r;
const char *value = !strcmp(key, "Host") ? "device.example" :
!strcmp(key, "Origin") ? "https://device.example" : NULL;
if (!value) return ESP_ERR_NOT_FOUND;
assert(strlen(value) < n); strcpy(out, value); return ESP_OK;
}
size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *key) {
char value[140]; return httpd_req_get_hdr_value_str(r, key, value, sizeof(value)) == ESP_OK ? strlen(value) : 0;
}
bool web_httpd_upgrade_requested(httpd_req_t *r) { (void)r; return true; }
esp_err_t web_httpd_upgrade(httpd_req_t *r, esp_err_t (*handler)(httpd_req_t *)) {
(void)r; (void)handler; return ESP_OK;
}
size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *key) { (void)r; (void)key; return 0; }
int httpd_req_to_sockfd(httpd_req_t *r) { (void)r; return 10; }
httpd_ws_client_info_t httpd_ws_get_fd_info(httpd_handle_t h, int fd) {
(void)h; (void)fd; return HTTPD_WS_CLIENT_WEBSOCKET;
@@ -126,12 +140,9 @@ int main(void) {
now += WEB_SERIAL_CURRENTNESS_INTERVAL_US; stale_user = alice.user_id; db_hook = reuse_slot_hook;
process_principal_currentness(sa); assert(!sa->close_requested);
/* Basic tickets still work with a disabled store, but never accept bound tickets. */
/* Zero identity is no longer a Basic compatibility route. */
serial_reset(); web_session_store_stop();
assert(web_serial_transport_mint_ticket(&alice, 0, ta, sizeof(ta)) == ESP_OK);
snprintf(query, sizeof(query), "ticket=%s", ta);
assert(connect_websocket(&request, 10, 0) == ESP_OK);
before = writes; assert(process_websocket_frame(&request) == ESP_OK && writes == before + 1);
assert(web_serial_transport_mint_ticket(&alice, 0, ta, sizeof(ta)) != ESP_OK);
serial_reset(); a = mint(&alice); b = mint(&bob);
assert(web_serial_transport_revoke_sessions() == ESP_OK); absent(&a); absent(&b);
assert(snapshot().initialized); assert(!host_lock_depth && closes > 0);
@@ -153,6 +164,12 @@ int main(void) {
assert(web_session_store_check_principal(a.view.id, &p, &current) != ESP_OK && !current);
present(&a);
}
puts("PASS: serial/session binding, isolation, cleanup, failure fallback, races, Basic regression");
serial_reset(); a = mint(&alice);
char tickets[4][33];
for (unsigned i = 0; i < 4; ++i) ticket_for(&a, tickets[i]);
assert(web_serial_transport_mint_ticket(&alice, a.view.id, ta, sizeof(ta)) == ESP_ERR_NO_MEM);
for (unsigned i = 0; i < 4; ++i)
assert(consume_ticket(tickets[i], a.view.id, &p, &consumed) == ESP_OK && consumed);
puts("PASS: serial/session binding, isolation, cleanup, races, no Basic fallback or live ticket eviction");
return 0;
}