mirror of
https://github.com/espressif/esp-idf.git
synced 2026-08-18 06:35:35 +03:00
Merge branch 'feat/esp_http_server-ws-strictness-foundation-and-handshake' into 'master'
feat(esp_http_server): update websocket server to check RFC6455 headers See merge request espressif/esp-idf!48903
This commit is contained in:
@@ -50,6 +50,26 @@ menu "HTTP Server"
|
||||
help
|
||||
This sets the WebSocket server support.
|
||||
|
||||
config HTTPD_WS_STRICTER_RFC6455
|
||||
bool "Enforce stricter RFC 6455 WebSocket conformance"
|
||||
default n
|
||||
depends on HTTPD_WS_SUPPORT
|
||||
help
|
||||
Enable stricter RFC 6455 enforcement in the WebSocket server. When enabled,
|
||||
the server validates handshake headers (Host, Sec-WebSocket-Version,
|
||||
Sec-WebSocket-Key), rejects frames with reserved RSV bits, reserved or
|
||||
fragmented control opcodes, non-minimal payload length encodings, and
|
||||
frames whose 64-bit length has the MSB set; sends a CLOSE frame on
|
||||
protocol errors; validates CLOSE frame status codes and UTF-8 reason;
|
||||
validates UTF-8 text payloads; and blocks outbound data frames once a
|
||||
CLOSE has been sent or received.
|
||||
|
||||
This option defaults to off in this release cycle so existing deployments
|
||||
see no behavior change. Enable to opt into stricter enforcement; lenient
|
||||
mode (the default) silently accepts protocol violations that may produce
|
||||
security or interoperability issues, so enable as soon as your clients are
|
||||
verified conformant.
|
||||
|
||||
config HTTPD_QUEUE_WORK_BLOCKING
|
||||
bool "httpd_queue_work as blocking API"
|
||||
help
|
||||
|
||||
@@ -520,17 +520,23 @@ int httpd_default_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len,
|
||||
|
||||
|
||||
/**
|
||||
* @brief This function is for responding a WebSocket handshake
|
||||
* @brief Respond to a WebSocket opening handshake.
|
||||
*
|
||||
* On any RFC 6455 §4.2.1 handshake validation failure (missing Host,
|
||||
* malformed Sec-WebSocket-Key, unsupported Sec-WebSocket-Version, etc.),
|
||||
* this function sends the appropriate HTTP error response (400 Bad Request
|
||||
* or 426 Upgrade Required) to the client itself and returns ESP_FAIL.
|
||||
* Callers MUST NOT attempt to send another response on the ESP_FAIL path.
|
||||
*
|
||||
* @param[in] req Pointer to handshake request that will be handled
|
||||
* @param[in] supported_subprotocol Pointer to the subprotocol supported by this URI
|
||||
* @return
|
||||
* - ESP_OK : When handshake is successful
|
||||
* - ESP_ERR_NOT_FOUND : When some headers (Sec-WebSocket-*) are not found
|
||||
* - ESP_ERR_INVALID_VERSION : The WebSocket version is not "13"
|
||||
* - ESP_ERR_INVALID_STATE : Handshake was done beforehand
|
||||
* - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket)
|
||||
* - ESP_FAIL : Socket failures
|
||||
* - ESP_OK : Handshake successful; 101 Switching Protocols sent
|
||||
* - ESP_ERR_INVALID_ARG : @p req or its aux pointer is NULL
|
||||
* - ESP_ERR_INVALID_STATE : Handshake was already performed on this session
|
||||
* - ESP_FAIL : Handshake-validation failure (HTTP error already sent),
|
||||
* memory allocation failure, hash/encode failure,
|
||||
* or socket send failure
|
||||
*/
|
||||
esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *supported_subprotocol);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@@ -86,7 +86,11 @@ static esp_err_t verify_url (http_parser *parser)
|
||||
((char *)r->uri)[length] = '\0';
|
||||
ESP_LOGD(TAG, LOG_FMT("received URI = %s"), r->uri);
|
||||
|
||||
/* Make sure version is HTTP/1.1 or HTTP/1.0 (legacy compliance purpose) */
|
||||
/* Make sure version is HTTP/1.1 or HTTP/1.0 (legacy compliance purpose).
|
||||
* The stricter HTTP/1.1 requirement for WebSocket handshakes is enforced
|
||||
* later in cb_headers_complete(), once the Upgrade header confirms that
|
||||
* the request is actually a WS handshake. Enforcing it here would also
|
||||
* reject HTTP/1.0 clients on regular (non-WS) endpoints. */
|
||||
if (!((parser->http_major == 1) && ((parser->http_minor == 0) || (parser->http_minor == 1)))) {
|
||||
ESP_LOGW(TAG, LOG_FMT("unsupported HTTP version = %d.%d"),
|
||||
parser->http_major, parser->http_minor);
|
||||
@@ -396,6 +400,20 @@ static esp_err_t cb_headers_complete(http_parser *parser)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
/* RFC 6455 §4.2.1: the WebSocket opening handshake MUST use HTTP/1.1.
|
||||
* This check is intentionally scoped to confirmed WS handshakes so
|
||||
* regular HTTP/1.0 traffic on non-WS endpoints keeps working. */
|
||||
if (!(parser->http_major == 1 && parser->http_minor == 1)) {
|
||||
ESP_LOGW(TAG, LOG_FMT("WebSocket handshake requires HTTP/1.1, got %d.%d"),
|
||||
parser->http_major, parser->http_minor);
|
||||
|
||||
parser_data->error = HTTPD_400_BAD_REQUEST;
|
||||
parser_data->status = PARSING_FAILED;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Now set handshake flag to true */
|
||||
ra->ws_handshake_detect = true;
|
||||
#else
|
||||
|
||||
@@ -22,8 +22,12 @@
|
||||
|
||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||
|
||||
#define WS_SEND_OK (1 << 0)
|
||||
#define WS_SEND_FAILED (1 << 1)
|
||||
#define WS_SEND_OK (1 << 0)
|
||||
#define WS_SEND_FAILED (1 << 1)
|
||||
|
||||
#define SEC_WEBSOCKET_VERSION_HDR_MAX_LEN 255
|
||||
#define SEC_WEBSOCKET_KEY_HDR_MAX_LEN 255
|
||||
#define SEC_WEBSOCKET_VERSION "13"
|
||||
|
||||
typedef struct {
|
||||
httpd_ws_frame_t frame;
|
||||
@@ -99,6 +103,23 @@ static bool httpd_ws_get_response_subprotocol(const char *supported_subprotocol,
|
||||
|
||||
}
|
||||
|
||||
/* Send an HTTP error response for a rejected WS handshake.
|
||||
* Optionally adds a Sec-WebSocket-Version header (RFC 6455 §4.4).
|
||||
* Returns ESP_FAIL so callers can write: return httpd_ws_send_handshake_error(...).
|
||||
*/
|
||||
static esp_err_t httpd_ws_send_handshake_error(httpd_req_t *req, const char *status,
|
||||
const char *message, const char *supported_version)
|
||||
{
|
||||
if (supported_version != NULL) {
|
||||
if (httpd_resp_set_hdr(req, "Sec-WebSocket-Version", supported_version) != ESP_OK) {
|
||||
ESP_LOGE(TAG, LOG_FMT("Failed to set Sec-WebSocket-Version header"));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
esp_err_t ret = httpd_resp_send_custom_err(req, status, message);
|
||||
return (ret == ESP_OK) ? ESP_FAIL : ret;
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *supported_subprotocol)
|
||||
{
|
||||
/* Probe if input parameters are valid or not */
|
||||
@@ -114,32 +135,96 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Detect WS version existence */
|
||||
char version_val[3] = { '\0' };
|
||||
if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Version", version_val, sizeof(version_val)) != ESP_OK) {
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
/* RFC 6455 §4.2.1: Host header MUST be present */
|
||||
size_t host_hdr_len = httpd_req_get_hdr_value_len(req, "Host");
|
||||
if (host_hdr_len == 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("\"Host\" is not found"));
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request", "Missing Host header", NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* RFC 6455 §4.2.1: Sec-WebSocket-Version must be present and equal "13" */
|
||||
size_t version_hdr_len = httpd_req_get_hdr_value_len(req, "Sec-WebSocket-Version");
|
||||
if (version_hdr_len == 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is not found"));
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Missing Sec-WebSocket-Version header", NULL);
|
||||
}
|
||||
|
||||
/* Detect if WS version is "13" or not.
|
||||
* WS version must be 13 for now. Please refer to RFC6455 Section 4.1, Page 18 for more details. */
|
||||
if (strcasecmp(version_val, "13") != 0) {
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
if (version_hdr_len > SEC_WEBSOCKET_VERSION_HDR_MAX_LEN) {
|
||||
ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is too long"));
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Version header", NULL);
|
||||
}
|
||||
#endif
|
||||
char *version_val = calloc(1, version_hdr_len + 1);
|
||||
if (version_val == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate version header buffer");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Version", version_val, version_hdr_len + 1) != ESP_OK) {
|
||||
free(version_val);
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Version header", NULL);
|
||||
}
|
||||
/* WS version must be 13. Please refer to RFC6455 Section 4.1, Page 18 for more details. */
|
||||
if (strcasecmp(version_val, SEC_WEBSOCKET_VERSION) != 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is not \"13\", it is: %s"), version_val);
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
free(version_val);
|
||||
return httpd_ws_send_handshake_error(req, "426 Upgrade Required",
|
||||
"WebSocket version not supported", "13");
|
||||
}
|
||||
free(version_val);
|
||||
|
||||
/* Grab Sec-WebSocket-Key (client key) from the header */
|
||||
/* Size of base64 coded string is equal '((input_size * 4) / 3) + (input_size / 96) + 6' including Z-term */
|
||||
char sec_key_encoded[28] = { '\0' };
|
||||
if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Key", sec_key_encoded, sizeof(sec_key_encoded)) != ESP_OK) {
|
||||
/* RFC 6455 §4.2.1 / §4.3: Sec-WebSocket-Key must be present (strict mode
|
||||
* additionally validates that it base64-decodes to a 16-byte nonce). */
|
||||
size_t sec_key_hdr_len = httpd_req_get_hdr_value_len(req, "Sec-WebSocket-Key");
|
||||
if (sec_key_hdr_len == 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Cannot find client key"));
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Missing Sec-WebSocket-Key header", NULL);
|
||||
}
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
if (sec_key_hdr_len > SEC_WEBSOCKET_KEY_HDR_MAX_LEN) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Sec-WebSocket-Key header is too long"));
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Key header", NULL);
|
||||
}
|
||||
#endif
|
||||
char *sec_key_encoded = calloc(1, sec_key_hdr_len + 1);
|
||||
if (sec_key_encoded == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate Sec-WebSocket-Key buffer");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Key", sec_key_encoded, sec_key_hdr_len + 1) != ESP_OK) {
|
||||
free(sec_key_encoded);
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Key header", NULL);
|
||||
}
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
uint8_t decoded_key[16] = { 0 };
|
||||
size_t decoded_key_len = 0;
|
||||
if (mbedtls_base64_decode(decoded_key, sizeof(decoded_key), &decoded_key_len,
|
||||
(const unsigned char *)sec_key_encoded, strlen(sec_key_encoded)) != 0 ||
|
||||
decoded_key_len != sizeof(decoded_key)) {
|
||||
free(sec_key_encoded);
|
||||
ESP_LOGW(TAG, LOG_FMT("Sec-WebSocket-Key is not a valid base64-encoded 16-byte nonce"));
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Key header", NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Prepare server key (Sec-WebSocket-Accept), concat the string */
|
||||
char server_key_encoded[33] = { '\0' };
|
||||
uint8_t server_key_hash[20] = { 0 };
|
||||
char server_raw_text[sizeof(sec_key_encoded) + sizeof(ws_magic_uuid) + 1] = { '\0' };
|
||||
size_t server_raw_text_len = strlen(sec_key_encoded) + strlen(ws_magic_uuid) + 1;
|
||||
char *server_raw_text = calloc(1, server_raw_text_len);
|
||||
if (server_raw_text == NULL) {
|
||||
free(sec_key_encoded);
|
||||
ESP_LOGE(TAG, "Failed to allocate handshake hash input buffer");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
strcpy(server_raw_text, sec_key_encoded);
|
||||
strcat(server_raw_text, ws_magic_uuid);
|
||||
@@ -150,12 +235,16 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
psa_hash_operation_t sha1_operation = PSA_HASH_OPERATION_INIT;
|
||||
psa_status_t status = psa_hash_setup(&sha1_operation, PSA_ALG_SHA_1);
|
||||
if (status != PSA_SUCCESS) {
|
||||
free(server_raw_text);
|
||||
free(sec_key_encoded);
|
||||
ESP_LOGE(TAG, "Failed to setup SHA-1 operation");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
status = psa_hash_update(&sha1_operation, (uint8_t *)server_raw_text, strlen(server_raw_text));
|
||||
if (status != PSA_SUCCESS) {
|
||||
free(server_raw_text);
|
||||
free(sec_key_encoded);
|
||||
ESP_LOGE(TAG, "Failed to update SHA-1 hash");
|
||||
psa_hash_abort(&sha1_operation);
|
||||
return ESP_FAIL;
|
||||
@@ -163,7 +252,9 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
|
||||
size_t hash_length;
|
||||
status = psa_hash_finish(&sha1_operation, server_key_hash, sizeof(server_key_hash), &hash_length);
|
||||
free(server_raw_text);
|
||||
if (status != PSA_SUCCESS || hash_length != sizeof(server_key_hash)) {
|
||||
free(sec_key_encoded);
|
||||
ESP_LOGE(TAG, "Failed to finish SHA-1 hash");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -173,6 +264,8 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
mbedtls_base64_encode((uint8_t *)server_key_encoded, sizeof(server_key_encoded), &encoded_len,
|
||||
server_key_hash, sizeof(server_key_hash));
|
||||
|
||||
free(sec_key_encoded);
|
||||
|
||||
ESP_LOGD(TAG, LOG_FMT("Generated server key: %s"), server_key_encoded);
|
||||
|
||||
char subprotocol[50] = { '\0' };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
idf_component_register(SRC_DIRS "."
|
||||
PRIV_INCLUDE_DIRS "." "../../src" "../../src/port/esp32"
|
||||
idf_component_register(SRC_DIRS "." "../mock_client"
|
||||
PRIV_INCLUDE_DIRS "." "../../src" "../../src/port/esp32" "../mock_client"
|
||||
PRIV_REQUIRES esp_http_server test_utils unity esp_timer)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_heap_caps.h>
|
||||
@@ -20,6 +21,7 @@
|
||||
|
||||
#include "unity.h"
|
||||
#include "test_utils.h"
|
||||
#include "mock_http_server_client.h"
|
||||
|
||||
int pre_start_mem, post_stop_mem, post_stop_min_mem;
|
||||
bool basic_sanity = true;
|
||||
@@ -31,6 +33,30 @@ esp_err_t null_func(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||
static httpd_handle_t start_test_ws_server(uint16_t server_port, uint16_t ctrl_port)
|
||||
{
|
||||
httpd_handle_t hd = NULL;
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = server_port;
|
||||
config.ctrl_port = ctrl_port;
|
||||
|
||||
httpd_uri_t ws_uri = {
|
||||
.uri = "/ws",
|
||||
.method = HTTP_GET,
|
||||
.handler = null_func,
|
||||
.user_ctx = NULL,
|
||||
.is_websocket = true,
|
||||
.handle_ws_control_frames = false,
|
||||
.supported_subprotocol = NULL,
|
||||
};
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_start(&hd, &config));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_register_uri_handler(hd, &ws_uri));
|
||||
return hd;
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
httpd_uri_t handler_limit_uri (char* path)
|
||||
{
|
||||
httpd_uri_t uri = {
|
||||
@@ -519,6 +545,129 @@ TEST_CASE("WS recv failure marks close without dispatching handler", "[HTTP SERV
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
TEST_CASE("WS HTTP/1.0 upgrade request returns 400", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
test_case_uses_tcpip();
|
||||
httpd_handle_t hd = start_test_ws_server(8091, ESP_HTTPD_DEF_CTRL_PORT + 10);
|
||||
mock_server_request_t req = {
|
||||
.data = "GET /ws HTTP/1.0\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n\r\n",
|
||||
};
|
||||
mock_server_response_t *resp = mock_server_send_request(8091, &req);
|
||||
TEST_ASSERT_NOT_NULL(resp);
|
||||
|
||||
mock_server_assert_status(resp, 400);
|
||||
mock_server_response_free(resp);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
/* Regression test: enabling CONFIG_HTTPD_WS_SUPPORT must not reject HTTP/1.0
|
||||
* traffic on non-WS endpoints. The HTTP/1.1 requirement only applies once
|
||||
* an "Upgrade: websocket" header confirms the request is a WS handshake. */
|
||||
TEST_CASE("Non-WS HTTP/1.0 request is not rejected by WS version check", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
test_case_uses_tcpip();
|
||||
httpd_handle_t hd = start_test_ws_server(8097, ESP_HTTPD_DEF_CTRL_PORT + 16);
|
||||
mock_server_request_t req = {
|
||||
.data = "GET /non-ws HTTP/1.0\r\n"
|
||||
"Host: localhost\r\n\r\n",
|
||||
};
|
||||
mock_server_response_t *resp = mock_server_send_request(8097, &req);
|
||||
TEST_ASSERT_NOT_NULL(resp);
|
||||
/* No handler is registered for /non-ws, so the server returns 404.
|
||||
* The crucial assertion is that it is NOT 400 from the WS version check —
|
||||
* i.e., HTTP/1.0 was accepted at the parser level. */
|
||||
mock_server_assert_status(resp, 404);
|
||||
mock_server_response_free(resp);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
TEST_CASE("WS handshake missing Host returns 400", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
test_case_uses_tcpip();
|
||||
httpd_handle_t hd = start_test_ws_server(8090, ESP_HTTPD_DEF_CTRL_PORT + 9);
|
||||
mock_server_request_t req = {
|
||||
.data = "GET /ws HTTP/1.1\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n\r\n",
|
||||
};
|
||||
mock_server_response_t *resp = mock_server_send_request(8090, &req);
|
||||
TEST_ASSERT_NOT_NULL(resp);
|
||||
mock_server_assert_status(resp, 400);
|
||||
mock_server_response_free(resp);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
TEST_CASE("WS handshake missing Sec-WebSocket-Version returns 400", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
test_case_uses_tcpip();
|
||||
httpd_handle_t hd = start_test_ws_server(8094, ESP_HTTPD_DEF_CTRL_PORT + 13);
|
||||
mock_server_request_t req = {
|
||||
.data = "GET /ws HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n",
|
||||
};
|
||||
mock_server_response_t *resp = mock_server_send_request(8094, &req);
|
||||
TEST_ASSERT_NOT_NULL(resp);
|
||||
mock_server_assert_status(resp, 400);
|
||||
mock_server_response_free(resp);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
|
||||
TEST_CASE("WS handshake unsupported version returns 426 with Sec-WebSocket-Version header", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
test_case_uses_tcpip();
|
||||
httpd_handle_t hd = start_test_ws_server(8093, ESP_HTTPD_DEF_CTRL_PORT + 12);
|
||||
mock_server_request_t req = {
|
||||
.data = "GET /ws HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 12\r\n\r\n",
|
||||
};
|
||||
mock_server_response_t *resp = mock_server_send_request(8093, &req);
|
||||
TEST_ASSERT_NOT_NULL(resp);
|
||||
mock_server_assert_status(resp, 426);
|
||||
TEST_ASSERT_NOT_NULL(strstr(resp->data, "Sec-WebSocket-Version: 13"));
|
||||
mock_server_response_free(resp);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
TEST_CASE("WS handshake invalid Sec-WebSocket-Key returns 400", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
test_case_uses_tcpip();
|
||||
httpd_handle_t hd = start_test_ws_server(8092, ESP_HTTPD_DEF_CTRL_PORT + 11);
|
||||
mock_server_request_t req = {
|
||||
.data = "GET /ws HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: AQID\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n\r\n",
|
||||
};
|
||||
mock_server_response_t *resp = mock_server_send_request(8092, &req);
|
||||
TEST_ASSERT_NOT_NULL(resp);
|
||||
mock_server_assert_status(resp, 400);
|
||||
mock_server_response_free(resp);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
void app_main(void)
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/time.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "unity.h"
|
||||
|
||||
#include "mock_http_server_client.h"
|
||||
|
||||
static const char *TAG = "mock_srv_client";
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
* Internal helpers
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Parse the HTTP status code from the beginning of a raw response buffer.
|
||||
* Expects "HTTP/1.x NNN ..." format. Returns -1 on any parse failure.
|
||||
*/
|
||||
static int parse_status_code(const char *data, size_t len)
|
||||
{
|
||||
/* Minimum: "HTTP/1.1 200 " = 13 chars */
|
||||
if (len < 13) {
|
||||
return -1;
|
||||
}
|
||||
/* Find the first space (end of "HTTP/1.x") */
|
||||
const char *p = memchr(data, ' ', len < 16 ? len : 16);
|
||||
if (!p) {
|
||||
return -1;
|
||||
}
|
||||
p++; /* point at status code digits */
|
||||
if ((size_t)(p - data) + 3 > len) {
|
||||
return -1;
|
||||
}
|
||||
int code = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (p[i] < '0' || p[i] > '9') {
|
||||
return -1;
|
||||
}
|
||||
code = code * 10 + (p[i] - '0');
|
||||
}
|
||||
/* Sanity: valid HTTP status codes are 100–599 */
|
||||
if (code < 100 || code > 599) {
|
||||
return -1;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
* Public API
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
mock_server_response_t *mock_server_send_request(uint16_t port,
|
||||
const mock_server_request_t *req)
|
||||
{
|
||||
mock_server_response_t *resp = calloc(1, sizeof(*resp));
|
||||
if (!resp) {
|
||||
ESP_LOGE(TAG, "calloc() failed");
|
||||
return NULL;
|
||||
}
|
||||
resp->status_code = -1;
|
||||
|
||||
/* ---- Create socket and connect ---- */
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
ESP_LOGE(TAG, "socket() failed: errno=%d", errno);
|
||||
free(resp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(port),
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
};
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
ESP_LOGE(TAG, "connect() to 127.0.0.1:%u failed: errno=%d", port, errno);
|
||||
close(fd);
|
||||
free(resp);
|
||||
return NULL;
|
||||
}
|
||||
ESP_LOGD(TAG, "connected to 127.0.0.1:%u", port);
|
||||
|
||||
/* ---- Send the scripted request ---- */
|
||||
size_t total_len = req->len ? req->len : strlen(req->data);
|
||||
size_t sent = 0;
|
||||
|
||||
/* How many bytes to send in each write() call */
|
||||
int max_per_write = (req->max_bytes_per_write > 0)
|
||||
? req->max_bytes_per_write
|
||||
: (int)total_len;
|
||||
|
||||
/* After how many sent bytes to stop writing.
|
||||
* 0 or negative = send everything (zero is the zero-init default for
|
||||
* unset struct fields, so it must mean "no early close"). */
|
||||
int close_after = req->close_after_bytes;
|
||||
|
||||
while (sent < total_len) {
|
||||
/* Honour early-close limit */
|
||||
if (close_after > 0 && (int)sent >= close_after) {
|
||||
break;
|
||||
}
|
||||
|
||||
int chunk = max_per_write;
|
||||
/* Don't exceed the early-close byte count */
|
||||
if (close_after > 0 && (int)(sent + chunk) > close_after) {
|
||||
chunk = close_after - (int)sent;
|
||||
}
|
||||
/* Don't exceed total length */
|
||||
if ((size_t)chunk > total_len - sent) {
|
||||
chunk = (int)(total_len - sent);
|
||||
}
|
||||
|
||||
int n = send(fd, req->data + sent, (size_t)chunk, 0);
|
||||
if (n <= 0) {
|
||||
ESP_LOGE(TAG, "send() failed after %zu bytes: errno=%d", sent, errno);
|
||||
close(fd);
|
||||
free(resp);
|
||||
return NULL;
|
||||
}
|
||||
sent += (size_t)n;
|
||||
ESP_LOGD(TAG, "sent %d bytes (%zu/%zu total)", n, sent, total_len);
|
||||
|
||||
if (req->write_delay_ms > 0 && sent < total_len) {
|
||||
vTaskDelay(pdMS_TO_TICKS(req->write_delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
/* Signal end-of-request to the server if we stopped sending early */
|
||||
if (close_after > 0) {
|
||||
shutdown(fd, SHUT_WR);
|
||||
ESP_LOGD(TAG, "shutdown(SHUT_WR) after %zu bytes", sent);
|
||||
}
|
||||
|
||||
/* ---- Read the response ---- */
|
||||
int timeout_ms = (req->recv_timeout_ms > 0) ? req->recv_timeout_ms : 2000;
|
||||
|
||||
/* Record absolute deadline so that EINTR retries don't reset the clock. */
|
||||
struct timeval deadline;
|
||||
gettimeofday(&deadline, NULL);
|
||||
deadline.tv_sec += timeout_ms / 1000;
|
||||
deadline.tv_usec += (timeout_ms % 1000) * 1000;
|
||||
if (deadline.tv_usec >= 1000000) {
|
||||
deadline.tv_sec++;
|
||||
deadline.tv_usec -= 1000000;
|
||||
}
|
||||
|
||||
size_t received = 0;
|
||||
while (received < sizeof(resp->data) - 1) {
|
||||
/* Calculate remaining time until deadline. */
|
||||
struct timeval now, remaining;
|
||||
gettimeofday(&now, NULL);
|
||||
remaining.tv_sec = deadline.tv_sec - now.tv_sec;
|
||||
remaining.tv_usec = deadline.tv_usec - now.tv_usec;
|
||||
if (remaining.tv_usec < 0) {
|
||||
remaining.tv_sec--;
|
||||
remaining.tv_usec += 1000000;
|
||||
}
|
||||
if (remaining.tv_sec < 0) {
|
||||
ESP_LOGD(TAG, "recv deadline elapsed after %zu bytes", received);
|
||||
break;
|
||||
}
|
||||
|
||||
/* Wait for data (or deadline) — handles EINTR safely. */
|
||||
fd_set read_fds;
|
||||
FD_ZERO(&read_fds);
|
||||
FD_SET(fd, &read_fds);
|
||||
int ready = select(fd + 1, &read_fds, NULL, NULL, &remaining);
|
||||
if (ready < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue; /* signal interrupted select — recalculate remaining time */
|
||||
}
|
||||
ESP_LOGE(TAG, "select() error: errno=%d", errno);
|
||||
break;
|
||||
}
|
||||
if (ready == 0) {
|
||||
ESP_LOGD(TAG, "recv deadline elapsed after %zu bytes", received);
|
||||
break;
|
||||
}
|
||||
|
||||
int n = recv(fd, resp->data + received, sizeof(resp->data) - 1 - received, 0);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGE(TAG, "recv() error after %zu bytes: errno=%d", received, errno);
|
||||
break;
|
||||
}
|
||||
if (n == 0) {
|
||||
resp->server_closed = true;
|
||||
ESP_LOGD(TAG, "server closed connection after %zu bytes", received);
|
||||
break;
|
||||
}
|
||||
received += (size_t)n;
|
||||
}
|
||||
|
||||
resp->len = received;
|
||||
resp->data[received] = '\0'; /* NUL-terminate for easy string inspection */
|
||||
|
||||
if (received > 0) {
|
||||
resp->status_code = parse_status_code(resp->data, received);
|
||||
ESP_LOGD(TAG, "response: status=%d, len=%zu", resp->status_code, received);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "no response received from server");
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return resp;
|
||||
}
|
||||
|
||||
void mock_server_response_free(mock_server_response_t *resp)
|
||||
{
|
||||
free(resp);
|
||||
}
|
||||
|
||||
void mock_server_assert_status(const mock_server_response_t *resp, int expected_status)
|
||||
{
|
||||
if (resp->len == 0) {
|
||||
TEST_FAIL_MESSAGE("mock_server: no response received from server");
|
||||
return;
|
||||
}
|
||||
if (resp->status_code == -1) {
|
||||
/* Print the raw response to help diagnose the issue */
|
||||
ESP_LOGE(TAG, "unparsable response (%zu bytes): %.80s", resp->len, resp->data);
|
||||
TEST_FAIL_MESSAGE("mock_server: server response is not a valid HTTP status line");
|
||||
return;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_MESSAGE(expected_status, resp->status_code,
|
||||
"HTTP status code mismatch");
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file mock_http_server_client.h
|
||||
* @brief Scripted TCP client for black-box testing of esp_http_server.
|
||||
*
|
||||
* Allows tests to send arbitrary (including malformed or fragmented) byte
|
||||
* sequences to a running httpd instance over loopback and capture the raw
|
||||
* response. The server under test is started/stopped by the test itself using
|
||||
* the normal httpd_start() / httpd_stop() APIs; this module only provides the
|
||||
* "client" side.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Maximum number of response bytes the mock client will buffer. */
|
||||
#define MOCK_SERVER_CLIENT_RESP_BUF_SIZE 4096
|
||||
|
||||
/**
|
||||
* @brief Describes a single scripted request the mock client sends.
|
||||
*
|
||||
* Only @p data is required. All other fields have safe defaults when zero.
|
||||
*/
|
||||
typedef struct {
|
||||
const char *data; /**< Raw bytes to send (must not be NULL) */
|
||||
size_t len; /**< Length of @p data; 0 → strlen(data) */
|
||||
|
||||
/** Maximum bytes per write() call. -1 (or 0) = send all at once.
|
||||
* Set to a small value to simulate a slow/trickled sender. */
|
||||
int max_bytes_per_write;
|
||||
|
||||
/** Delay in milliseconds between fragmented writes. 0 = no delay. */
|
||||
int write_delay_ms;
|
||||
|
||||
/** Close the TCP write-side after sending this many bytes.
|
||||
* 0 (default) or negative = send everything, keep write-side open.
|
||||
* Positive N = close after N bytes (useful for partial-request / abrupt-close tests). */
|
||||
int close_after_bytes;
|
||||
|
||||
/** How long (ms) to wait for a response before giving up. 0 → 2000 ms. */
|
||||
int recv_timeout_ms;
|
||||
} mock_server_request_t;
|
||||
|
||||
/**
|
||||
* @brief Result captured from the server after sending a scripted request.
|
||||
*/
|
||||
typedef struct {
|
||||
char data[MOCK_SERVER_CLIENT_RESP_BUF_SIZE]; /**< Raw response bytes */
|
||||
size_t len; /**< Bytes actually received */
|
||||
int status_code; /**< HTTP status code parsed from the status line,
|
||||
* or -1 if the response is not a valid HTTP response. */
|
||||
bool server_closed; /**< True if the server closed the connection (recv returned 0). */
|
||||
} mock_server_response_t;
|
||||
|
||||
/**
|
||||
* @brief Connect to a running httpd on 127.0.0.1:@p port, send the scripted
|
||||
* request and capture the response.
|
||||
*
|
||||
* The response struct is heap-allocated; the caller must free it with
|
||||
* mock_server_response_free() when done. The TCP connection is always
|
||||
* closed before returning.
|
||||
*
|
||||
* @param[in] port TCP port the httpd server is listening on.
|
||||
* @param[in] req Scripted request to send.
|
||||
*
|
||||
* @return Heap-allocated response, or NULL if a TCP-level error occurred.
|
||||
*/
|
||||
mock_server_response_t *mock_server_send_request(uint16_t port,
|
||||
const mock_server_request_t *req);
|
||||
|
||||
/**
|
||||
* @brief Free a response returned by mock_server_send_request().
|
||||
*/
|
||||
void mock_server_response_free(mock_server_response_t *resp);
|
||||
|
||||
/**
|
||||
* @brief Assert that @p resp contains a valid HTTP response with the expected
|
||||
* status code. Calls TEST_FAIL() (Unity) if the assertion does not hold.
|
||||
*/
|
||||
void mock_server_assert_status(const mock_server_response_t *resp, int expected_status);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -8,3 +8,4 @@ CONFIG_COMPILER_STACK_CHECK=y
|
||||
|
||||
CONFIG_ESP_TASK_WDT_EN=n
|
||||
CONFIG_HTTPD_WS_SUPPORT=y
|
||||
CONFIG_HTTPD_WS_STRICTER_RFC6455=y
|
||||
|
||||
Reference in New Issue
Block a user