/* SPDX-License-Identifier: GPL-3.0-only */ /* Deliberately isolated dependency on the installed IDF HTTPD layout. */ #include "web_httpd_adapter.h" #include #include #include #include #include #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); }