240 lines
11 KiB
C
240 lines
11 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* Deliberately isolated dependency on the installed IDF HTTPD layout. */
|
|
#include "web_httpd_adapter.h"
|
|
#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 headers, upgrade, idle cleanup and WS/TLS send contracts 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;
|
|
}
|
|
}
|
|
|
|
static int web_httpd_aborted_send(httpd_handle_t server, int fd,
|
|
const char *buffer, size_t length, int flags)
|
|
{
|
|
(void)server; (void)fd; (void)buffer; (void)length; (void)flags;
|
|
return HTTPD_SOCK_ERR_FAIL;
|
|
}
|
|
|
|
esp_err_t web_httpd_ws_send_binary(httpd_handle_t server, int fd,
|
|
const void *expected_context,
|
|
const uint8_t *payload, size_t length)
|
|
{
|
|
struct httpd_data *hd = server;
|
|
if (!hd || fd < 0 || !expected_context || (!payload && length) ||
|
|
length > WEB_HTTPD_WS_BINARY_MAX_PAYLOAD) return ESP_ERR_INVALID_ARG;
|
|
if (httpd_os_thread_handle() != hd->hd_td.handle || hd->hd_req_aux.sd)
|
|
return ESP_ERR_INVALID_STATE;
|
|
struct sock_db *sd = httpd_sess_get(hd, fd);
|
|
if (!sd || sd->ctx != expected_context || !sd->ws_handshake_done ||
|
|
sd->ws_close || sd->for_async_req || !sd->send_fn)
|
|
return ESP_ERR_INVALID_STATE;
|
|
|
|
/* IDF 5.5.0 httpd_ws_send_frame_async emits header/payload separately.
|
|
* Keep the existing send override (HTTPS -> esp_tls_conn_write), not raw
|
|
* socket IO. Owner-local scratch lives through the synchronous call only. */
|
|
uint8_t wire[WEB_HTTPD_WS_BINARY_MAX_PAYLOAD + 4U];
|
|
size_t header = length <= 125U ? 2U : 4U;
|
|
wire[0] = 0x82; /* FIN, binary; server frames are never masked. */
|
|
wire[1] = header == 2U ? (uint8_t)length : 126U;
|
|
if (header == 4U) {
|
|
wire[2] = (uint8_t)(length >> 8U);
|
|
wire[3] = (uint8_t)length;
|
|
}
|
|
if (length) memcpy(wire + header, payload, length);
|
|
size_t total = header + length;
|
|
int sent = sd->send_fn(hd, fd, (const char *)wire, total, 0);
|
|
if (sent == (int)total) return ESP_OK;
|
|
/* Owner retains this validated session across the synchronous send. TLS
|
|
* may hold pending output after short/zero/WANT/error: never retry it with
|
|
* different arguments. Deferred close alone permits SDK automatic PONG or
|
|
* CLOSE first. Shutdown alone cannot block buffered-input TLS calls either.
|
|
* Reject all sends before shutdown, even if shutdown fails. ws_close also
|
|
* skips SDK request processing; normal HTTPD still owns TLS destruction. */
|
|
sd->send_fn = web_httpd_aborted_send;
|
|
sd->ws_close = true;
|
|
(void)shutdown(fd, SHUT_RDWR);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
esp_err_t web_httpd_register_optional(httpd_handle_t server, const httpd_uri_t *uri)
|
|
{
|
|
struct httpd_data *hd = server;
|
|
if (!hd || !uri || !uri->uri || !uri->handler ||
|
|
(uri->method != HTTP_GET && uri->method != HTTP_POST) ||
|
|
uri->is_websocket || uri->supported_subprotocol || hd->config.uri_match_fn)
|
|
return ESP_ERR_INVALID_ARG;
|
|
size_t length = 0;
|
|
while (length < 128 && uri->uri[length]) ++length;
|
|
if (!length || length == 128) return ESP_ERR_INVALID_ARG;
|
|
int slot = -1;
|
|
for (unsigned i = 0; i < hd->config.max_uri_handlers; ++i) {
|
|
if (!hd->hd_calls[i]) { if (slot < 0) slot = (int)i; }
|
|
else if (!strcmp(hd->hd_calls[i]->uri, uri->uri) && hd->hd_calls[i]->method == uri->method)
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (slot < 0) return ESP_ERR_NO_MEM;
|
|
/* IDF 5.5.0 publishes its descriptor before strdup; strdup failure leaves a
|
|
* freed hd_calls entry. Optional registration must leave the table intact. */
|
|
httpd_uri_t *copy = malloc(sizeof(*copy));
|
|
if (!copy) return ESP_ERR_NO_MEM;
|
|
char *name = malloc(length + 1);
|
|
if (!name) { free(copy); return ESP_ERR_NO_MEM; }
|
|
memcpy(name, uri->uri, length + 1);
|
|
*copy = *uri;
|
|
copy->uri = name;
|
|
hd->hd_calls[slot] = copy;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t web_httpd_register_optional_get(httpd_handle_t server, const httpd_uri_t *uri)
|
|
{
|
|
if (!uri || uri->method != HTTP_GET) return ESP_ERR_INVALID_ARG;
|
|
return web_httpd_register_optional(server, uri);
|
|
}
|