Add Bounded Ordinary HTTPS Idle Cleanup
This commit is contained in:
@@ -42,6 +42,7 @@ idf_component_register(
|
||||
"web_session_store.c"
|
||||
"web_auth_parse.c"
|
||||
"web_httpd_adapter.c"
|
||||
"web_httpd_idle.c"
|
||||
"web_cookie_auth.c"
|
||||
"web_login_ui.c"
|
||||
"web_console.c"
|
||||
|
||||
+49
-1
@@ -4,14 +4,62 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.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"
|
||||
#error "Reaudit HTTPD headers, upgrade, request completion and idle cleanup for this IDF"
|
||||
#endif
|
||||
|
||||
/* IDF 5.5.0 httpd_sess_process increments lru_counter only AFTER successful
|
||||
* req_new + req_delete (handler, response, leftover-body purge and cleanup).
|
||||
* Work callbacks run between sessions, never inside synchronous parse/TLS/send.
|
||||
* This counter is an observation marker, NOT permission to enable LRU purge. */
|
||||
void web_httpd_idle_sweep(httpd_handle_t server,
|
||||
web_httpd_idle_row_t rows[WEB_HTTPD_IDLE_SOCKETS], int64_t now)
|
||||
{
|
||||
struct httpd_data *hd = server;
|
||||
if (!hd || !rows || hd->config.max_open_sockets > WEB_HTTPD_IDLE_SOCKETS ||
|
||||
httpd_os_thread_handle() != hd->hd_td.handle || hd->hd_req_aux.sd) return;
|
||||
for (unsigned i = 0; i < hd->config.max_open_sockets; ++i) {
|
||||
struct sock_db *sd = &hd->hd_sd[i];
|
||||
web_httpd_idle_row_t *row = &rows[i];
|
||||
/* Actual SDK classification, not delayed diagnostic route metadata. */
|
||||
if (sd->fd < 0 || sd->for_async_req || sd->ws_handshake_done || sd->ws_close) {
|
||||
memset(row, 0, sizeof(*row));
|
||||
continue;
|
||||
}
|
||||
if (!row->observed || row->fd != sd->fd || row->completed != sd->lru_counter) {
|
||||
*row = (web_httpd_idle_row_t){.fd = sd->fd, .completed = sd->lru_counter,
|
||||
.idle_since_us = now, .observed = true};
|
||||
continue;
|
||||
}
|
||||
if (row->shutdown_sent) continue;
|
||||
/* Control work precedes data processing in httpd_main. Do not expire a
|
||||
* connection whose next request is buffered in HTTPD, TLS or TCP. Zero
|
||||
* timeout select does not consume bytes or change TLS receive ownership.
|
||||
* Errors are conservative too; normal HTTPD owns error cleanup. */
|
||||
fd_set ready;
|
||||
FD_ZERO(&ready);
|
||||
if (sd->fd >= FD_SETSIZE) { row->idle_since_us = now; continue; }
|
||||
FD_SET(sd->fd, &ready);
|
||||
struct timeval timeout = {0};
|
||||
if (sd->pending_len || (sd->pending_fn && sd->pending_fn(hd, sd->fd) != 0) ||
|
||||
select(sd->fd + 1, &ready, NULL, NULL, &timeout) != 0) {
|
||||
row->idle_since_us = now;
|
||||
continue;
|
||||
}
|
||||
if (now - row->idle_since_us < WEB_HTTPD_IDLE_TIMEOUT_US) continue;
|
||||
/* Still the current fd on its owner; no queued sock_db pointer can later
|
||||
* target a replacement. HTTPD performs normal TLS/session destruction
|
||||
* on the next read. A failed shutdown retries on the next probe. */
|
||||
if (shutdown(sd->fd, SHUT_RDWR) == 0) row->shutdown_sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool web_httpd_headers_valid(httpd_req_t *request)
|
||||
{
|
||||
if (!request || !request->aux) return false;
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#pragma once
|
||||
#include "esp_http_server.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#define WEB_HTTPD_IDLE_SOCKETS 6U
|
||||
#define WEB_HTTPD_IDLE_TIMEOUT_US INT64_C(15000000)
|
||||
typedef struct {
|
||||
uint64_t completed;
|
||||
int64_t idle_since_us;
|
||||
int fd;
|
||||
bool observed, shutdown_sent;
|
||||
} web_httpd_idle_row_t;
|
||||
/* HTTPD-owner work boundary only. TLS create must invalidate reused fd rows. */
|
||||
void web_httpd_idle_sweep(httpd_handle_t server,
|
||||
web_httpd_idle_row_t rows[WEB_HTTPD_IDLE_SOCKETS], int64_t now);
|
||||
|
||||
/* HTTPD-owner only, before body reads or any response. Reject duplicate lines,
|
||||
* including Cookie, rather than trusting first-match public getters. */
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "web_httpd_idle.h"
|
||||
#include "web_httpd_adapter.h"
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define IDLE_POLL_US INT64_C(1000000)
|
||||
#define IDLE_FENCE_US INT64_C(1000000)
|
||||
|
||||
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static httpd_handle_t s_server;
|
||||
static esp_timer_handle_t s_timer;
|
||||
static uintptr_t s_generation;
|
||||
static bool s_accepting, s_queued, s_submitting;
|
||||
/* Only HTTPD touches rows while alive; prepare runs before SSL startup. */
|
||||
static web_httpd_idle_row_t s_rows[WEB_HTTPD_IDLE_SOCKETS];
|
||||
|
||||
static void idle_work(void *argument)
|
||||
{
|
||||
uintptr_t generation = (uintptr_t)argument;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool current = s_queued && generation == s_generation;
|
||||
httpd_handle_t server = current && s_accepting ? s_server : NULL;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (server) web_httpd_idle_sweep(server, s_rows, esp_timer_get_time());
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
if (current && generation == s_generation) s_queued = false;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
static void idle_timer(void *argument)
|
||||
{
|
||||
(void)argument;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
httpd_handle_t server = NULL;
|
||||
uintptr_t generation = s_generation;
|
||||
if (s_accepting && !s_queued && !s_submitting) {
|
||||
server = s_server;
|
||||
s_queued = s_submitting = true;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (!server) return;
|
||||
esp_err_t error = httpd_queue_work(server, idle_work, (void *)generation);
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
/* A callback may finish before queue_work returns. Keep the submission
|
||||
* reservation until here so it cannot clear a newer probe's queued flag. */
|
||||
if (error != ESP_OK) s_queued = false;
|
||||
s_submitting = false;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
esp_err_t web_httpd_idle_prepare(void)
|
||||
{
|
||||
#if defined(CONFIG_HTTPD_QUEUE_WORK_BLOCKING) && CONFIG_HTTPD_QUEUE_WORK_BLOCKING
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#else
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool allowed = !s_server && !s_queued && !s_submitting && s_generation != UINTPTR_MAX;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (!allowed) return ESP_ERR_INVALID_STATE;
|
||||
if (!s_timer) {
|
||||
esp_timer_handle_t timer = NULL;
|
||||
const esp_timer_create_args_t args = {
|
||||
.callback = idle_timer, .name = "web_idle", .skip_unhandled_events = true,
|
||||
};
|
||||
esp_err_t error = esp_timer_create(&args, &timer);
|
||||
if (error == ESP_OK) error = esp_timer_start_periodic(timer, IDLE_POLL_US);
|
||||
if (error != ESP_OK) {
|
||||
if (timer) (void)esp_timer_delete(timer);
|
||||
return error;
|
||||
}
|
||||
s_timer = timer;
|
||||
}
|
||||
memset(s_rows, 0, sizeof(s_rows));
|
||||
return ESP_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
esp_err_t web_httpd_idle_attach(httpd_handle_t server)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool allowed = server && s_timer && !s_server && !s_queued && !s_submitting &&
|
||||
s_generation != UINTPTR_MAX;
|
||||
if (allowed) {
|
||||
++s_generation; /* Never reused, including when HTTPD's handle is reused. */
|
||||
s_server = server;
|
||||
s_accepting = true;
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return allowed ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t web_httpd_idle_detach(httpd_handle_t server)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool owned = server && s_server == server;
|
||||
bool absent = !s_server;
|
||||
if (owned) s_accepting = false;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
/* Partial startup may never have attached. */
|
||||
if (!owned) return absent ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||
int64_t deadline = esp_timer_get_time() + IDLE_FENCE_US;
|
||||
for (;;) {
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
bool submitting = s_submitting;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
if (!submitting) return ESP_OK;
|
||||
if (esp_timer_get_time() >= deadline) return ESP_ERR_TIMEOUT;
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void web_httpd_idle_stopped(httpd_handle_t server)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
if (server && s_server == server && !s_accepting && !s_submitting) {
|
||||
s_server = NULL;
|
||||
s_queued = false; /* Successful HTTPD stop joined owner and destroyed queue. */
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
void web_httpd_idle_tls(esp_https_server_user_cb_arg_t *arg)
|
||||
{
|
||||
if (!arg || !arg->tls || arg->user_cb_state != HTTPD_SSL_USER_CB_SESS_CREATE) return;
|
||||
int fd = -1;
|
||||
if (esp_tls_get_conn_sockfd(arg->tls, &fd) != ESP_OK || fd < 0) {
|
||||
/* Identity unavailable: conservatively restart every idle observation. */
|
||||
memset(s_rows, 0, sizeof(s_rows));
|
||||
return;
|
||||
}
|
||||
for (unsigned i = 0; i < WEB_HTTPD_IDLE_SOCKETS; ++i)
|
||||
if (s_rows[i].fd == fd) memset(&s_rows[i], 0, sizeof(s_rows[i]));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#pragma once
|
||||
#include "esp_https_server.h"
|
||||
|
||||
/* Serialized web_server lifecycle. Prepare before SSL start; stop must fence
|
||||
* submissions before destroying HTTPD, and retire only after successful stop. */
|
||||
esp_err_t web_httpd_idle_prepare(void);
|
||||
esp_err_t web_httpd_idle_attach(httpd_handle_t server);
|
||||
esp_err_t web_httpd_idle_detach(httpd_handle_t server);
|
||||
void web_httpd_idle_stopped(httpd_handle_t server);
|
||||
/* Synchronous HTTPD-owner TLS callback, composed with diagnostics by server. */
|
||||
void web_httpd_idle_tls(esp_https_server_user_cb_arg_t *arg);
|
||||
+24
-3
@@ -29,6 +29,7 @@
|
||||
#include "web_session_store.h"
|
||||
#include "web_cookie_auth.h"
|
||||
#include "web_httpd_adapter.h"
|
||||
#include "web_httpd_idle.h"
|
||||
#include "web_diagnostics.h"
|
||||
#include "web_ui.h"
|
||||
#include "wifi_manager.h"
|
||||
@@ -546,6 +547,12 @@ static esp_err_t route_error_handler(httpd_req_t *request, httpd_err_code_t code
|
||||
return ESP_FAIL; /* Do not drain a rejected request body on keepalive. */
|
||||
}
|
||||
|
||||
static void tls_session_callback(esp_https_server_user_cb_arg_t *arg)
|
||||
{
|
||||
web_httpd_idle_tls(arg);
|
||||
web_diagnostics_tls(arg);
|
||||
}
|
||||
|
||||
esp_err_t web_server_init(void)
|
||||
{
|
||||
esp_err_t error = ensure_mutex();
|
||||
@@ -600,6 +607,7 @@ esp_err_t web_server_start(void)
|
||||
|
||||
/* Initialize only after lifecycle admission; failure gates all HTTPS auth. */
|
||||
error = web_cookie_auth_start();
|
||||
if (error == ESP_OK) error = web_httpd_idle_prepare();
|
||||
|
||||
uint8_t certificate[WEB_SECURITY_CERTIFICATE_DER_CAPACITY] = {0};
|
||||
uint8_t private_key[WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY] = {0};
|
||||
@@ -627,8 +635,8 @@ esp_err_t web_server_start(void)
|
||||
config.prvtkey_len = private_key_length;
|
||||
config.port_secure = WEB_SERVER_PORT;
|
||||
config.tls_handshake_timeout_ms = 5000U;
|
||||
/* Public synchronous post-TLS observation; HTTPS retains all cleanup. */
|
||||
config.user_cb = web_diagnostics_tls;
|
||||
/* Public synchronous identity reset/observation; HTTPS retains cleanup. */
|
||||
config.user_cb = tls_session_callback;
|
||||
error = httpd_ssl_start(&server, &config);
|
||||
}
|
||||
secure_wipe(certificate, sizeof(certificate));
|
||||
@@ -648,6 +656,7 @@ esp_err_t web_server_start(void)
|
||||
error = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, route_error_handler);
|
||||
if (error == ESP_OK)
|
||||
error = httpd_register_err_handler(server, HTTPD_405_METHOD_NOT_ALLOWED, route_error_handler);
|
||||
if (error == ESP_OK) error = web_httpd_idle_attach(server);
|
||||
esp_err_t attach_error = s_serial_transport_error;
|
||||
if (error == ESP_OK && serial_transport_ready) {
|
||||
attach_error = web_serial_transport_attach_server(server);
|
||||
@@ -680,8 +689,10 @@ esp_err_t web_server_start(void)
|
||||
web_cookie_auth_stop();
|
||||
}
|
||||
if (error != ESP_OK && server != NULL) {
|
||||
esp_err_t cleanup_error = httpd_ssl_stop(server);
|
||||
esp_err_t cleanup_error = web_httpd_idle_detach(server);
|
||||
if (cleanup_error == ESP_OK) cleanup_error = httpd_ssl_stop(server);
|
||||
if (cleanup_error == ESP_OK) {
|
||||
web_httpd_idle_stopped(server);
|
||||
server = NULL;
|
||||
} else {
|
||||
/* Retain ownership so stop can retry and start cannot allocate a second server. */
|
||||
@@ -726,6 +737,15 @@ esp_err_t web_server_stop(void)
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
|
||||
web_cookie_auth_stop();
|
||||
esp_err_t idle_error = web_httpd_idle_detach(server);
|
||||
if (idle_error != ESP_OK) {
|
||||
/* Never destroy HTTPD while a timer submission still holds its handle. */
|
||||
xSemaphoreTake(s_server_mutex, portMAX_DELAY);
|
||||
s_transitioning = false;
|
||||
s_last_error = idle_error;
|
||||
xSemaphoreGive(s_server_mutex);
|
||||
return idle_error;
|
||||
}
|
||||
if (admin_transport_owned) {
|
||||
esp_err_t detach_error = web_admin_transport_detach(server);
|
||||
if (detach_error != ESP_OK) {
|
||||
@@ -752,6 +772,7 @@ esp_err_t web_server_stop(void)
|
||||
}
|
||||
|
||||
esp_err_t error = httpd_ssl_stop(server);
|
||||
if (error == ESP_OK) web_httpd_idle_stopped(server);
|
||||
if (error == ESP_OK && admin_transport_owned) web_admin_transport_stopped(server);
|
||||
if (error != ESP_OK && serial_transport_attached) {
|
||||
/* Stay detached: old HTTPD work may still be reading static TX storage. */
|
||||
|
||||
Reference in New Issue
Block a user