fix(ws): enforce RFC 6455 §4.2.1 handshake requirements

This commit is contained in:
Ashish Sharma
2026-05-25 15:04:10 +08:00
parent 64287ae681
commit 7d53b4740b
7 changed files with 286 additions and 46 deletions

View File

@@ -471,6 +471,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 */
/********* URL query / header pointer-accessor tests *********

View File

@@ -13,27 +13,6 @@
* 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.
*
* Typical usage:
* @code
* // 1. Start the server
* httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
* cfg.server_port = 8099;
* httpd_handle_t server;
* httpd_start(&server, &cfg);
* httpd_register_uri_handler(server, &my_handler);
*
* // 2. Send a scripted request and capture the response
* mock_server_request_t req = {
* .data = "GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n",
* };
* mock_server_response_t resp = {0};
* mock_server_send_request(8099, &req, &resp);
* TEST_ASSERT_EQUAL(200, resp.status_code);
*
* // 3. Stop the server
* httpd_stop(server);
* @endcode
*/
#pragma once

View File

@@ -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