mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
test(esp_http_server): cover WebSocket control-frame handler
This commit is contained in:
@@ -561,6 +561,98 @@ TEST_CASE("httpd_queue_work fast-fails on ctrl mbox saturation", "[HTTP SERVER]"
|
||||
}
|
||||
|
||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* White-box fixtures for the dedicated control-frame handler.
|
||||
*
|
||||
* These tests drive httpd_req_new() directly against a fake sock_db, feeding a
|
||||
* crafted (zero-masked) WebSocket frame through a recv_fn override and capturing
|
||||
* the server's reply through a send_fn override. No TCP/IP is involved, so they
|
||||
* run identically on hardware and under QEMU.
|
||||
* ------------------------------------------------------------------------- */
|
||||
static int s_ws_data_handler_calls;
|
||||
static int s_ws_control_handler_calls;
|
||||
static httpd_ws_type_t s_ws_control_seen_type;
|
||||
static size_t s_ws_control_seen_len;
|
||||
static esp_err_t s_ws_control_ret;
|
||||
|
||||
static const uint8_t *s_ws_recv_data;
|
||||
static size_t s_ws_recv_len;
|
||||
static size_t s_ws_recv_off;
|
||||
|
||||
static uint8_t s_ws_sent[128];
|
||||
static size_t s_ws_sent_len;
|
||||
|
||||
/* recv_fn override: serve the crafted frame byte stream, one chunk per call. */
|
||||
static int ws_feed_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags)
|
||||
{
|
||||
(void)hd; (void)sockfd; (void)flags;
|
||||
size_t remaining = s_ws_recv_len - s_ws_recv_off;
|
||||
if (remaining == 0) {
|
||||
return HTTPD_SOCK_ERR_FAIL;
|
||||
}
|
||||
size_t n = (buf_len < remaining) ? buf_len : remaining;
|
||||
memcpy(buf, s_ws_recv_data + s_ws_recv_off, n);
|
||||
s_ws_recv_off += n;
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
/* send_fn override: capture whatever the server sends back (the protocol reply). */
|
||||
static int ws_capture_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags)
|
||||
{
|
||||
(void)hd; (void)sockfd; (void)flags;
|
||||
for (size_t i = 0; i < buf_len && s_ws_sent_len < sizeof(s_ws_sent); i++) {
|
||||
s_ws_sent[s_ws_sent_len++] = (uint8_t)buf[i];
|
||||
}
|
||||
return (int)buf_len;
|
||||
}
|
||||
|
||||
static esp_err_t ws_data_handler_spy(httpd_req_t *req)
|
||||
{
|
||||
(void)req;
|
||||
s_ws_data_handler_calls++;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t ws_control_handler_spy(httpd_req_t *req, const httpd_ws_frame_t *frame)
|
||||
{
|
||||
(void)req;
|
||||
s_ws_control_handler_calls++;
|
||||
s_ws_control_seen_type = frame->type;
|
||||
s_ws_control_seen_len = frame->len;
|
||||
return s_ws_control_ret;
|
||||
}
|
||||
|
||||
/* Wire a fake session/server and reset all fixtures around a single frame. */
|
||||
static void ws_unit_ctx_init(struct httpd_data *hd, struct sock_db *session,
|
||||
const uint8_t *frame, size_t frame_len)
|
||||
{
|
||||
s_ws_data_handler_calls = 0;
|
||||
s_ws_control_handler_calls = 0;
|
||||
s_ws_control_seen_type = HTTPD_WS_TYPE_CONTINUE;
|
||||
s_ws_control_seen_len = 0;
|
||||
s_ws_control_ret = ESP_OK;
|
||||
s_ws_recv_data = frame;
|
||||
s_ws_recv_len = frame_len;
|
||||
s_ws_recv_off = 0;
|
||||
s_ws_sent_len = 0;
|
||||
memset(s_ws_sent, 0, sizeof(s_ws_sent));
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_open_sockets = 1; /* keep any session enumeration in bounds */
|
||||
hd->config = config;
|
||||
hd->hd_sd = session; /* non-NULL so httpd_sess_get() finds the reply target */
|
||||
hd->hd_req_aux.resp_hdrs = calloc(config.max_resp_headers, sizeof(*hd->hd_req_aux.resp_hdrs));
|
||||
TEST_ASSERT_NOT_NULL(hd->hd_req_aux.resp_hdrs);
|
||||
|
||||
session->fd = 123;
|
||||
session->handle = (httpd_handle_t) hd;
|
||||
session->recv_fn = ws_feed_recv;
|
||||
session->send_fn = ws_capture_send;
|
||||
session->ws_handshake_done = true;
|
||||
session->ws_handler = ws_data_handler_spy;
|
||||
session->ws_close = false;
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv failure marks close without dispatching handler", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
@@ -944,6 +1036,119 @@ TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload",
|
||||
TEST_ASSERT_EQUAL(sizeof(expected_header), ws_send_capture_ctx.len);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_header, ws_send_capture_ctx.data, sizeof(expected_header));
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler receives PING and server replies PONG", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* Masked (zero-key) PING carrying a 2-byte payload "Hi". */
|
||||
static const uint8_t ping_frame[] = { 0x89, 0x82, 0x00, 0x00, 0x00, 0x00, 'H', 'i' };
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
ws_unit_ctx_init(&hd, &session, ping_frame, sizeof(ping_frame));
|
||||
session.ws_control_frames = true;
|
||||
session.ws_control_handler = ws_control_handler_spy;
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, ret);
|
||||
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_PING, s_ws_control_seen_type);
|
||||
TEST_ASSERT_EQUAL(2, s_ws_control_seen_len);
|
||||
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls); /* control frame must not reach data handler */
|
||||
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* FIN | PONG */
|
||||
TEST_ASSERT_FALSE(session.ws_close);
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler receives CLOSE and server replies CLOSE", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t close_frame[] = { 0x88, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
ws_unit_ctx_init(&hd, &session, close_frame, sizeof(close_frame));
|
||||
session.ws_control_frames = true;
|
||||
session.ws_control_handler = ws_control_handler_spy;
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, ret);
|
||||
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, s_ws_control_seen_type);
|
||||
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x88, s_ws_sent[0]); /* FIN | CLOSE */
|
||||
TEST_ASSERT_TRUE(session.ws_close); /* server marked the session for close */
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler receives PONG with no reply", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t pong_frame[] = { 0x8A, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
ws_unit_ctx_init(&hd, &session, pong_frame, sizeof(pong_frame));
|
||||
session.ws_control_frames = true;
|
||||
session.ws_control_handler = ws_control_handler_spy;
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, ret);
|
||||
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_PONG, s_ws_control_seen_type);
|
||||
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls);
|
||||
TEST_ASSERT_EQUAL(0, s_ws_sent_len); /* a PONG is never answered */
|
||||
TEST_ASSERT_FALSE(session.ws_close);
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
TEST_CASE("WS without control handler auto-replies PING (backward compatible)", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* Mode 1: no control handler, flag off -> server must still auto-reply PONG and
|
||||
* must not dispatch the PING to the data handler (unchanged legacy behavior). */
|
||||
static const uint8_t ping_frame[] = { 0x89, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
ws_unit_ctx_init(&hd, &session, ping_frame, sizeof(ping_frame));
|
||||
session.ws_control_frames = false;
|
||||
session.ws_control_handler = NULL;
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, ret);
|
||||
TEST_ASSERT_EQUAL(0, s_ws_control_handler_calls);
|
||||
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* auto PONG */
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler error still replies then closes socket", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* A PING is used so ws_close stays false and cleanup does not touch the fake
|
||||
* control socket. The handler fails, but the server must still send the PONG,
|
||||
* and httpd_req_new() must propagate the error so the caller closes the socket. */
|
||||
static const uint8_t ping_frame[] = { 0x89, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
ws_unit_ctx_init(&hd, &session, ping_frame, sizeof(ping_frame));
|
||||
session.ws_control_frames = true;
|
||||
session.ws_control_handler = ws_control_handler_spy;
|
||||
s_ws_control_ret = ESP_FAIL;
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, ret); /* error propagated to caller */
|
||||
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
|
||||
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* reply sent despite handler error */
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
/********* URL query / header pointer-accessor tests *********
|
||||
|
||||
@@ -76,6 +76,19 @@ Each outgoing frame has the FIN flag set by default.
|
||||
In case an application wants to send fragmented data, it must be done manually by setting the
|
||||
`fragmented` option and using the `final` flag as described in [RFC6455, section 5.4](https://tools.ietf.org/html/rfc6455#section-5.4).
|
||||
|
||||
#### Handling control frames
|
||||
|
||||
By default the server replies to control frames (PING, CLOSE) automatically. Setting `handle_ws_control_frames = true` alone routes control frames to the data handler instead, which then has to receive them and send the protocol replies itself.
|
||||
|
||||
This example registers a dedicated control-frame handler on the `/ws` endpoint (see `CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER`, enabled by default):
|
||||
|
||||
```c
|
||||
.handle_ws_control_frames = true,
|
||||
.ws_control_handler = ws_control_frame_handler, // observes PING/PONG/CLOSE
|
||||
```
|
||||
|
||||
The handler only observes the frames (this example logs them); the server still sends the protocol replies (PONG for PING, CLOSE for CLOSE) itself. Send the text message `Ping` to the server to watch the full heartbeat round trip: the server sends a PING and the client's PONG response is logged by the control-frame handler.
|
||||
|
||||
|
||||
### Hardware Required
|
||||
|
||||
|
||||
@@ -20,4 +20,14 @@ menu "Example Configuration"
|
||||
In this example, the post-handshake callback is used to send a welcome message
|
||||
to the client after the handshake is complete.
|
||||
|
||||
config EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER
|
||||
bool "Enable dedicated WebSocket control-frame handler"
|
||||
default y
|
||||
help
|
||||
Enable this option to register a dedicated handler for WebSocket
|
||||
control frames (PING, PONG, CLOSE) on the /ws endpoint. The handler
|
||||
only observes the frames (e.g. for heartbeat tracking or logging);
|
||||
the server still sends the protocol replies (PONG for PING, CLOSE
|
||||
for CLOSE) itself.
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -139,6 +139,36 @@ static esp_err_t ws_post_handshake_cb(httpd_req_t *req)
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_ENABLE_WS_POST_HANDSHAKE_CB */
|
||||
|
||||
#ifdef CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER
|
||||
/*
|
||||
* Dedicated control-frame handler: observes PING/PONG/CLOSE frames without
|
||||
* receiving them in the data handler. The frame is read-only and owned by the
|
||||
* server, which sends the protocol reply (PONG for PING, CLOSE for CLOSE)
|
||||
* itself after this handler returns.
|
||||
*
|
||||
* Type "Ping" in the client to see the full heartbeat round trip: the server
|
||||
* sends a PING and the client's PONG response lands here.
|
||||
*/
|
||||
static esp_err_t ws_control_frame_handler(httpd_req_t *req, const httpd_ws_frame_t *frame)
|
||||
{
|
||||
switch (frame->type) {
|
||||
case HTTPD_WS_TYPE_PING:
|
||||
ESP_LOGI(TAG, "Control frame: PING (len %d), server replies PONG", frame->len);
|
||||
break;
|
||||
case HTTPD_WS_TYPE_PONG:
|
||||
ESP_LOGI(TAG, "Control frame: PONG, heartbeat alive");
|
||||
break;
|
||||
case HTTPD_WS_TYPE_CLOSE:
|
||||
ESP_LOGI(TAG, "Control frame: CLOSE (len %d), server replies CLOSE", frame->len);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGI(TAG, "Control frame: type %d", frame->type);
|
||||
break;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
|
||||
|
||||
/*
|
||||
* This handler echos back the received ws data
|
||||
* and triggers an async send if certain message received
|
||||
@@ -278,7 +308,13 @@ static const httpd_uri_t ws = {
|
||||
.method = HTTP_GET,
|
||||
.handler = echo_handler,
|
||||
.user_ctx = NULL,
|
||||
.is_websocket = true
|
||||
.is_websocket = true,
|
||||
#ifdef CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER
|
||||
/* Route control frames to the dedicated handler; the server still
|
||||
* sends the protocol replies itself. */
|
||||
.handle_ws_control_frames = true,
|
||||
.ws_control_handler = ws_control_frame_handler,
|
||||
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
|
||||
};
|
||||
|
||||
static const httpd_uri_t ws_partial = {
|
||||
|
||||
@@ -111,6 +111,11 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
|
||||
|
||||
got_ip, got_port = _wait_for_server_ready(dut)
|
||||
|
||||
# With the dedicated control-frame handler enabled, PING/PONG/CLOSE are
|
||||
# observed (logged) by the control handler on the DUT, while the server
|
||||
# still sends the protocol replies itself.
|
||||
control_handler_enabled = dut.app.sdkconfig.get('EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER') is True
|
||||
|
||||
# Start ws server test
|
||||
with WsClient(got_ip, got_port, uri='ws') as ws:
|
||||
DATA = 'Espressif'
|
||||
@@ -122,6 +127,9 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
|
||||
if expected_opcode == OPCODE_PING:
|
||||
if opcode != OPCODE_PONG or data != DATA:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode} or data:{data}')
|
||||
if control_handler_enabled:
|
||||
# The control-frame handler must have observed the client's PING
|
||||
dut.expect(rf'Control frame: PING \(len {len(DATA)}\), server replies PONG', timeout=10)
|
||||
continue
|
||||
dut_data = dut.expect(r'Got packet with message: ([A-Za-z0-9_]*)')[1]
|
||||
dut_opcode = dut.expect(r'Packet type: ([0-9]*)')[1].decode()
|
||||
@@ -150,11 +158,17 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
|
||||
data = data.decode()
|
||||
if opcode != OPCODE_PING:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
|
||||
# Now we should get a pong in response to our ping
|
||||
opcode, data = ws.read()
|
||||
data = data.decode()
|
||||
if opcode != OPCODE_PONG:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
|
||||
# The client library auto-replies PONG to the server's PING. With the
|
||||
# control-frame handler enabled, the DUT observes that PONG in the
|
||||
# control handler and does not echo it; otherwise the data handler
|
||||
# echoes the PONG back to the client.
|
||||
if control_handler_enabled:
|
||||
dut.expect('Control frame: PONG, heartbeat alive', timeout=10)
|
||||
else:
|
||||
opcode, data = ws.read()
|
||||
data = data.decode()
|
||||
if opcode != OPCODE_PONG:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
|
||||
ws.write(data='Ping', opcode=OPCODE_TEXT)
|
||||
# Wait for server to receive the message and send a ping
|
||||
dut.expect(r'Got packet with message: Ping', timeout=10)
|
||||
@@ -164,11 +178,22 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
|
||||
data = data.decode()
|
||||
if opcode != OPCODE_PING:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
|
||||
# Now we should get a pong in response to our ping
|
||||
opcode, data = ws.read()
|
||||
data = data.decode()
|
||||
if opcode != OPCODE_PONG:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
|
||||
# The client library auto-replies PONG to the server's PING. With the
|
||||
# control-frame handler enabled, the DUT observes that PONG in the
|
||||
# control handler and does not echo it; otherwise the data handler
|
||||
# echoes the PONG back to the client.
|
||||
if control_handler_enabled:
|
||||
dut.expect('Control frame: PONG, heartbeat alive', timeout=10)
|
||||
else:
|
||||
opcode, data = ws.read()
|
||||
data = data.decode()
|
||||
if opcode != OPCODE_PONG:
|
||||
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
|
||||
|
||||
# Leaving the context closes the client connection: the CLOSE frame must be
|
||||
# delivered to the control-frame handler (the server still replies CLOSE).
|
||||
if control_handler_enabled:
|
||||
dut.expect('Control frame: CLOSE', timeout=10)
|
||||
|
||||
|
||||
@pytest.mark.wifi_router
|
||||
|
||||
Reference in New Issue
Block a user