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
+106
View File
@@ -0,0 +1,106 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Deliberately isolated dependency on the installed IDF HTTPD layout. */
#include "web_httpd_adapter.h"
#include <string.h>
#include <strings.h>
#include "esp_idf_version.h"
#include "esp_httpd_priv.h"
#include "secure_random.h"
#if ESP_IDF_VERSION != ESP_IDF_VERSION_VAL(5, 5, 0)
#error "Reaudit HTTPD header storage and pre-handler upgrade behavior for this IDF"
#endif
bool web_httpd_headers_valid(httpd_req_t *request)
{
if (!request || !request->aux) return false;
const struct httpd_req_aux *aux = request->aux;
const char *start = aux->scratch;
if (!start || aux->scratch_cur_size > 1024U) return false;
const char *end = start + aux->scratch_cur_size;
const char *line = start;
for (unsigned i = 0; i < aux->req_hdrs_count; ++i) {
if (line >= end) return false;
while (line < end && !*line) ++line;
const char *stop = memchr(line, 0, (size_t)(end - line));
if (!stop) return false;
const char *colon = memchr(line, ':', (size_t)(stop - line));
if (!colon || colon == line) return false;
size_t length = (size_t)(colon - line);
for (const char *p = line; p < colon; ++p) {
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || strchr("!#$%&'*+-.^_`|~", *p))) return false;
}
for (const char *p = colon + 1; p < stop; ++p) {
if ((unsigned char)*p < 32U || (unsigned char)*p == 127U) return false;
}
/* Reject transfer coding and Expect rather than draining an unbounded
* body after an authentication failure. No application route uses them. */
if ((length == 17U && !strncasecmp(line, "Transfer-Encoding", length)) ||
(length == 6U && !strncasecmp(line, "Expect", length))) return false;
const char *previous = start;
for (unsigned j = 0; j < i; ++j) {
while (previous < line && !*previous) ++previous;
const char *previous_end = memchr(previous, 0, (size_t)(line - previous));
if (!previous_end) return false;
const char *previous_colon = memchr(previous, ':', (size_t)(previous_end - previous));
if (!previous_colon) return false;
if ((size_t)(previous_colon - previous) == length &&
!strncasecmp(previous, line, length)) return false;
previous = previous_end + 1;
}
line = stop + 1;
}
return true;
}
bool web_httpd_upgrade_requested(httpd_req_t *request)
{
const struct httpd_req_aux *aux = request->aux;
if (!aux || !aux->sd || !aux->ws_handshake_detect || aux->sd->ws_handshake_done)
return false;
char version[3], key[25];
if (httpd_req_get_hdr_value_len(request, "Sec-WebSocket-Version") != 2U ||
httpd_req_get_hdr_value_str(request, "Sec-WebSocket-Version", version, sizeof(version)) != ESP_OK ||
strcmp(version, "13") || httpd_req_get_hdr_value_len(request, "Sec-WebSocket-Key") != 24U ||
httpd_req_get_hdr_value_str(request, "Sec-WebSocket-Key", key, sizeof(key)) != ESP_OK ||
key[22] != '=' || key[23] != '=' || !strchr("AQgw", key[21])) return false;
for (unsigned i = 0; i < 21; ++i)
if (!strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", key[i])) return false;
return true;
}
esp_err_t web_httpd_upgrade(httpd_req_t *request,
esp_err_t (*handler)(httpd_req_t *))
{
if (!web_httpd_upgrade_requested(request)) return ESP_ERR_INVALID_STATE;
esp_err_t error = httpd_ws_respond_server_handshake(request, NULL);
if (error == ESP_OK) {
struct httpd_req_aux *aux = request->aux;
aux->sd->ws_handshake_done = true;
aux->sd->ws_handler = handler;
aux->sd->ws_control_frames = false;
aux->sd->ws_user_ctx = NULL;
}
return error;
}
void web_httpd_wipe_request(httpd_req_t *request, bool closing)
{
struct httpd_req_aux *aux = request->aux;
if (!aux) return;
if (aux->scratch) secure_wipe(aux->scratch, aux->scratch_cur_size);
aux->req_hdrs_count = 0;
if (aux->sd) {
size_t keep = closing ? 0 : aux->sd->pending_len;
/* httpd_unrecv()/httpd_recv_pending() right-align unread bytes. */
if (keep <= sizeof(aux->sd->pending_data))
secure_wipe(aux->sd->pending_data, sizeof(aux->sd->pending_data) - keep);
}
}
bool web_httpd_unread_body(httpd_req_t *request)
{
const struct httpd_req_aux *aux = request->aux;
return aux && aux->remaining_len != 0;
}