mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 03:00:34 +03:00
fix(ws): validate CLOSE frame semantics, echo payload, validate UTF-8 text
This commit is contained in:
@@ -60,8 +60,11 @@ menu "HTTP Server"
|
||||
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; and blocks outbound data frames once a CLOSE has been
|
||||
sent or received.
|
||||
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.
|
||||
|
||||
UTF-8 is validated for a complete, unfragmented text frame only.
|
||||
|
||||
This option defaults to off in this release cycle so existing deployments
|
||||
see no behavior change. Enable to opt into stricter enforcement; lenient
|
||||
|
||||
@@ -1857,6 +1857,13 @@ typedef void (*transfer_complete_cb)(esp_err_t err, int socket, void *arg);
|
||||
*
|
||||
* @note Calling httpd_ws_recv_frame() with max_len as 0 will give actual frame size in pkt->len.
|
||||
* The user can dynamically allocate space for pkt->payload as per this length and call httpd_ws_recv_frame() again to get the actual data.
|
||||
*
|
||||
* @note Fragmented messages (RFC 6455 §5.4) are not supported. Each frame is
|
||||
* returned on its own; the library never joins the fragments of one
|
||||
* message, and it does not validate the fragment sequence. Read pkt->final
|
||||
* and pkt->type to detect a fragment, and join the payloads in the
|
||||
* application. UTF-8 validation of a TEXT message that arrives in
|
||||
* fragments is the caller's responsibility; see httpd_ws_validate_utf8().
|
||||
* Please refer to the corresponding example for usage.
|
||||
*
|
||||
* @param[in] req Current request
|
||||
@@ -1878,6 +1885,13 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *pkt, size_t ma
|
||||
* The user can dynamically allocate space for pkt->payload or user defined chunk size and call httpd_ws_recv_frame_part() again to get the actual data.
|
||||
* In contrast to httpd_ws_recv_frame, this method is able to read frame payload partially. The amount of data that is yet to be received is stored in pkt->left_len
|
||||
*
|
||||
* @note UTF-8 validation required by RFC 6455 §8.1 for TEXT frames is only
|
||||
* performed by the library when a whole frame is consumed in a single
|
||||
* call (which includes a httpd_ws_recv_frame_part() call whose max_len
|
||||
* covers the entire payload). Callers that assemble a TEXT payload
|
||||
* across multiple calls are responsible for validating the assembled
|
||||
* buffer themselves; see httpd_ws_validate_utf8().
|
||||
*
|
||||
* @param[in] req Current request
|
||||
* @param[out] pkt WebSocket packet
|
||||
* @param[in] max_len Maximum length for receive
|
||||
@@ -1889,6 +1903,22 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *pkt, size_t ma
|
||||
*/
|
||||
esp_err_t httpd_ws_recv_frame_part(httpd_req_t *req, httpd_ws_frame_t *pkt, size_t max_len);
|
||||
|
||||
/**
|
||||
* @brief Validate that a byte buffer is well-formed UTF-8 per RFC 3629.
|
||||
*
|
||||
* Intended for application code that assembles a WebSocket TEXT message
|
||||
* across multiple httpd_ws_recv_frame_part() calls and needs to enforce
|
||||
* RFC 6455 §8.1 on the assembled payload before processing it.
|
||||
*
|
||||
* @param[in] data Pointer to the buffer to validate. May be NULL only if len is 0.
|
||||
* @param[in] len Length of the buffer in bytes.
|
||||
* @return
|
||||
* - ESP_OK : Buffer is valid UTF-8 (an empty buffer is always valid).
|
||||
* - ESP_ERR_INVALID_ARG : data is NULL with non-zero len, or the buffer is not
|
||||
* well-formed UTF-8 (overlong, surrogate, or beyond U+10FFFF).
|
||||
*/
|
||||
esp_err_t httpd_ws_validate_utf8(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Construct and send a WebSocket frame
|
||||
* @param[in] req Current request
|
||||
|
||||
@@ -87,6 +87,7 @@ struct sock_db {
|
||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||
bool ws_handshake_done; /*!< True if it has done WebSocket handshake (if this socket is a valid WS) */
|
||||
bool ws_close; /*!< Set to true to close the socket later (when WS Close frame received) */
|
||||
bool ws_close_sent; /*!< Set to true once this endpoint has sent its own WS Close frame */
|
||||
esp_err_t (*ws_handler)(httpd_req_t *r); /*!< WebSocket handler, leave to null if it's not WebSocket */
|
||||
bool ws_control_frames; /*!< WebSocket flag indicating that control frames should be passed to user handlers */
|
||||
esp_err_t (*ws_control_handler)(httpd_req_t *r, const httpd_ws_frame_t *frame); /*!< Dedicated WebSocket control-frame handler, NULL if not used */
|
||||
|
||||
@@ -56,6 +56,7 @@ static const char *TAG="httpd_ws";
|
||||
|
||||
/* RFC 6455 §7.4 close status codes used for protocol-error Close frames */
|
||||
#define HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR 1002U
|
||||
#define HTTPD_WS_CLOSE_CODE_INVALID_UTF8 1007U
|
||||
#define HTTPD_WS_CLOSE_CODE_TOO_BIG 1009U
|
||||
|
||||
/*
|
||||
@@ -370,6 +371,16 @@ static inline void httpd_ws_mark_closing(struct httpd_req_aux *aux)
|
||||
aux->ws_type = HTTPD_WS_TYPE_CLOSE;
|
||||
}
|
||||
|
||||
/* Record that this endpoint has put its own CLOSE frame on the wire. Called from
|
||||
* the single send path, so an application that replies to a CLOSE itself is
|
||||
* tracked as well. */
|
||||
static inline void httpd_ws_mark_close_sent(struct sock_db *sess, httpd_ws_type_t type)
|
||||
{
|
||||
if (type == HTTPD_WS_TYPE_CLOSE) {
|
||||
sess->ws_close_sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Send a Close frame with the given status code and mark the session as closing.
|
||||
* Always returns ESP_FAIL so callers can write: return httpd_ws_fail_connection(...).
|
||||
* RFC 6455 §7.1.7
|
||||
@@ -388,10 +399,10 @@ static esp_err_t httpd_ws_fail_connection(httpd_req_t *req, uint16_t close_code)
|
||||
|
||||
httpd_ws_mark_closing(aux);
|
||||
|
||||
bool already_closing = aux->sd->ws_close;
|
||||
bool close_already_sent = aux->sd->ws_close_sent;
|
||||
aux->sd->ws_close = true;
|
||||
|
||||
if (aux->sd->ws_handshake_done && !already_closing) {
|
||||
if (aux->sd->ws_handshake_done && !close_already_sent) {
|
||||
uint8_t close_payload[2] = {
|
||||
(uint8_t)(close_code >> 8U),
|
||||
(uint8_t)(close_code & 0xffU),
|
||||
@@ -412,6 +423,86 @@ static esp_err_t httpd_ws_fail_connection(httpd_req_t *req, uint16_t close_code)
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_validate_utf8(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (data == NULL && len != 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
size_t idx = 0;
|
||||
while (idx < len) {
|
||||
uint8_t c = data[idx++];
|
||||
if (c <= 0x7F) {
|
||||
continue;
|
||||
}
|
||||
uint8_t trail = 0;
|
||||
uint8_t lo = 0x80, hi = 0xBF;
|
||||
if (c >= 0xC2 && c <= 0xDF) {
|
||||
trail = 1;
|
||||
} else if (c == 0xE0) {
|
||||
trail = 2;
|
||||
lo = 0xA0;
|
||||
} else if (c == 0xED) {
|
||||
trail = 2;
|
||||
hi = 0x9F;
|
||||
} else if (c >= 0xE1 && c <= 0xEF) {
|
||||
trail = 2;
|
||||
} else if (c == 0xF0) {
|
||||
trail = 3;
|
||||
lo = 0x90;
|
||||
} else if (c == 0xF4) {
|
||||
trail = 3;
|
||||
hi = 0x8F;
|
||||
} else if (c >= 0xF1 && c <= 0xF3) {
|
||||
trail = 3;
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
for (uint8_t i = 0; i < trail; i++) {
|
||||
if (idx >= len || data[idx] < lo || data[idx] > hi) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
idx++;
|
||||
lo = 0x80;
|
||||
hi = 0xBF;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static bool httpd_ws_is_valid_close_code(uint16_t code)
|
||||
{
|
||||
if (code >= 1000 && code <= 1014 && code != 1004 && code != 1005 && code != 1006) {
|
||||
return true;
|
||||
}
|
||||
return code >= 3000 && code <= 4999;
|
||||
}
|
||||
|
||||
/* Validates the payload of a CLOSE frame (RFC 6455 §5.5.1 / §7.4 / §8.1). */
|
||||
static esp_err_t httpd_ws_validate_close_frame(httpd_req_t *req, const httpd_ws_frame_t *frame)
|
||||
{
|
||||
if (frame->type != HTTPD_WS_TYPE_CLOSE) {
|
||||
return ESP_OK;
|
||||
}
|
||||
/* A one-byte body is never valid. */
|
||||
if (frame->len == 1) {
|
||||
ESP_LOGE(TAG, LOG_FMT("Invalid CLOSE frame payload length"));
|
||||
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
|
||||
}
|
||||
if (frame->len >= 2) {
|
||||
uint16_t close_code = ((uint16_t)frame->payload[0] << 8U) | frame->payload[1];
|
||||
if (!httpd_ws_is_valid_close_code(close_code)) {
|
||||
ESP_LOGE(TAG, LOG_FMT("Invalid CLOSE frame code: %u"), close_code);
|
||||
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
|
||||
}
|
||||
if (frame->len > 2 && httpd_ws_validate_utf8(frame->payload + 2, frame->len - 2) != ESP_OK) {
|
||||
ESP_LOGE(TAG, LOG_FMT("Invalid CLOSE frame UTF-8 reason"));
|
||||
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_INVALID_UTF8);
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
static esp_err_t httpd_ws_unmask_payload(uint8_t *payload, size_t len, const uint8_t *mask_key, size_t mask_offset)
|
||||
{
|
||||
if (len < 1 || !payload) {
|
||||
@@ -598,6 +689,20 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
/* Unmask payload */
|
||||
httpd_ws_unmask_payload(frame->payload, offset, aux->mask_key, mask_offset);
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
/* Validate complete frames (left_len == 0 and all bytes unmasked). */
|
||||
if (frame->left_len == 0 && offset == frame->len) {
|
||||
/* RFC 6455 §8.1: text frames must carry valid UTF-8. */
|
||||
if (frame->type == HTTPD_WS_TYPE_TEXT && frame->final &&
|
||||
httpd_ws_validate_utf8(frame->payload, frame->len) != ESP_OK) {
|
||||
ESP_LOGE(TAG, LOG_FMT("Invalid WS text payload UTF-8"));
|
||||
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_INVALID_UTF8);
|
||||
}
|
||||
/* Validate CLOSE frame payload and UTF-8 reason (Issues 16, 19). */
|
||||
return httpd_ws_validate_close_frame(req, frame);
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -672,6 +777,22 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t
|
||||
/* WebSocket server does not required to mask response payload, so leave the MASK bit as 0. */
|
||||
header_buf[1] &= (~HTTPD_WS_MASK_BIT);
|
||||
|
||||
/* Send a short frame as a single write. A CLOSE reply is followed at once by
|
||||
* the socket teardown, and if unread request data is still pending the stack
|
||||
* aborts the connection with an RST. That RST can discard a second, separate
|
||||
* write, so the peer sees a header that promises a payload it never gets. */
|
||||
if (frame->len > 0 && frame->payload != NULL && frame->len <= HTTPD_WS_CONTROL_PAYLOAD_MAX) {
|
||||
uint8_t frame_out[sizeof(header_buf) + HTTPD_WS_CONTROL_PAYLOAD_MAX];
|
||||
memcpy(frame_out, header_buf, tx_len);
|
||||
memcpy(frame_out + tx_len, frame->payload, frame->len);
|
||||
if (sess->send_fn(hd, fd, (const char *)frame_out, tx_len + frame->len, 0) < 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to send WS frame"));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpd_ws_mark_close_sent(sess, frame->type);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Send off header */
|
||||
if (sess->send_fn(hd, fd, (const char *)header_buf, tx_len, 0) < 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to send WS header"));
|
||||
@@ -686,6 +807,7 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t
|
||||
}
|
||||
}
|
||||
|
||||
httpd_ws_mark_close_sent(sess, frame->type);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -713,10 +835,14 @@ esp_err_t httpd_ws_reply_to_control_frame(httpd_req_t *req, httpd_ws_frame_t *fr
|
||||
frame->type = HTTPD_WS_TYPE_PONG;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
case HTTPD_WS_TYPE_CLOSE:
|
||||
/* Reply to a CLOSE with an empty CLOSE (RFC 6455 §5.5.1) */
|
||||
/* Reply to a CLOSE with a CLOSE (RFC 6455 §5.5.1). Strict mode echoes the
|
||||
* received status code and reason, as §5.5.1 recommends. Lenient
|
||||
* (pre-strict) behavior replies with an empty CLOSE. */
|
||||
ESP_LOGD(TAG, LOG_FMT("Got a WS CLOSE frame, Replying CLOSE..."));
|
||||
#if !CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
frame->len = 0;
|
||||
frame->payload = NULL;
|
||||
#endif /* !CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
default:
|
||||
/* PONG and any other control frame require no reply */
|
||||
|
||||
@@ -742,6 +742,33 @@ TEST_CASE("WS handshake unsupported version returns 426 with Sec-WebSocket-Versi
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
|
||||
}
|
||||
|
||||
TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t expected_header[] = { 0x82, 0x7E, 0xFF, 0xFF };
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
httpd_ws_frame_t frame = {
|
||||
.type = HTTPD_WS_TYPE_BINARY,
|
||||
.payload = NULL,
|
||||
.len = UINT16_MAX,
|
||||
};
|
||||
|
||||
memset(&ws_send_capture_ctx, 0, sizeof(ws_send_capture_ctx));
|
||||
|
||||
hd.config = config;
|
||||
hd.config.max_open_sockets = 1;
|
||||
hd.hd_sd = &session;
|
||||
session.fd = 123;
|
||||
session.handle = (httpd_handle_t)&hd;
|
||||
session.send_fn = ws_scripted_send_override;
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_send_frame_async(&hd, session.fd, &frame));
|
||||
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));
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
TEST_CASE("WS HTTP/1.0 upgrade request returns 400", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
@@ -824,11 +851,11 @@ static void ws_setup_recv_fixture(struct httpd_data *hd, httpd_req_t *req,
|
||||
session->ws_handshake_done = true;
|
||||
}
|
||||
|
||||
/* Asserts the session was marked closing and a 1002 (protocol-error) CLOSE
|
||||
* frame was emitted on the wire. */
|
||||
static void ws_assert_close_1002_sent(const struct sock_db *session)
|
||||
/* Asserts the session was marked closing and a CLOSE frame carrying the given
|
||||
* status code was emitted on the wire. */
|
||||
static void ws_assert_close_sent(const struct sock_db *session, uint16_t code)
|
||||
{
|
||||
static const uint8_t expected_reply[] = { 0x88, 0x02, 0x03, 0xEA };
|
||||
const uint8_t expected_reply[] = { 0x88, 0x02, (uint8_t)(code >> 8U), (uint8_t)(code & 0xFFU) };
|
||||
|
||||
TEST_ASSERT_TRUE(session->ws_close);
|
||||
TEST_ASSERT_EQUAL(sizeof(expected_reply), ws_send_capture_ctx.len);
|
||||
@@ -846,7 +873,7 @@ TEST_CASE("WS recv RSV bit set sends CLOSE 1002 and marks close", "[HTTP SERVER]
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req));
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv reserved non-control opcode sends CLOSE 1002", "[HTTP SERVER][websocket]")
|
||||
@@ -861,7 +888,7 @@ TEST_CASE("WS recv reserved non-control opcode sends CLOSE 1002", "[HTTP SERVER]
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv reserved control opcode sends CLOSE 1002", "[HTTP SERVER][websocket]")
|
||||
@@ -876,7 +903,7 @@ TEST_CASE("WS recv reserved control opcode sends CLOSE 1002", "[HTTP SERVER][web
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv fragmented control frame sends CLOSE 1002", "[HTTP SERVER][websocket]")
|
||||
@@ -891,7 +918,7 @@ TEST_CASE("WS recv fragmented control frame sends CLOSE 1002", "[HTTP SERVER][we
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv unmasked frame sends CLOSE 1002 and marks close", "[HTTP SERVER][websocket]")
|
||||
@@ -911,7 +938,7 @@ TEST_CASE("WS recv unmasked frame sends CLOSE 1002 and marks close", "[HTTP SERV
|
||||
aux.ws_final = true;
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_recv_frame(&req, &frame, 0));
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv control frame with payload > 125 sends CLOSE 1002", "[HTTP SERVER][websocket]")
|
||||
@@ -928,7 +955,7 @@ TEST_CASE("WS recv control frame with payload > 125 sends CLOSE 1002", "[HTTP SE
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv rejects non-minimal 16-bit payload length encoding", "[HTTP SERVER][websocket]")
|
||||
@@ -947,7 +974,7 @@ TEST_CASE("WS recv rejects non-minimal 16-bit payload length encoding", "[HTTP S
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_recv_frame(&req, &frame, 0));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv rejects non-minimal 64-bit payload length encoding", "[HTTP SERVER][websocket]")
|
||||
@@ -966,7 +993,7 @@ TEST_CASE("WS recv rejects non-minimal 64-bit payload length encoding", "[HTTP S
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_recv_frame(&req, &frame, 0));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_1002_sent(&session);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS send refuses non-CLOSE frame once session is closing", "[HTTP SERVER][websocket]")
|
||||
@@ -1008,147 +1035,165 @@ TEST_CASE("WS send refuses non-CLOSE frame once session is closing", "[HTTP SERV
|
||||
TEST_ASSERT_EQUAL(sizeof(expected_close), ws_send_capture_ctx.len);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_close, ws_send_capture_ctx.data, sizeof(expected_close));
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload", "[HTTP SERVER][websocket]")
|
||||
TEST_CASE("WS auto CLOSE reply echoes received payload", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t expected_header[] = { 0x82, 0x7E, 0xFF, 0xFF };
|
||||
/* CLOSE frame: FIN|CLOSE, MASK=1 len=4, zero mask, payload = 1000 + "OK" */
|
||||
static const uint8_t ws_frame[] = {
|
||||
0x88, 0x84, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 'O', 'K'
|
||||
};
|
||||
static const uint8_t expected_reply[] = { 0x88, 0x04, 0x03, 0xE8, 'O', 'K' };
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
struct httpd_data hd = {0};
|
||||
struct httpd_req_aux aux = {0};
|
||||
struct sock_db session = {0};
|
||||
httpd_ws_frame_t frame = {
|
||||
.type = HTTPD_WS_TYPE_BINARY,
|
||||
.payload = NULL,
|
||||
.len = UINT16_MAX,
|
||||
httpd_req_t req = {0};
|
||||
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(sizeof(expected_reply), ws_send_capture_ctx.len);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_reply, ws_send_capture_ctx.data, sizeof(expected_reply));
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv rejects CLOSE frame with 1-byte payload", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* CLOSE with 1-byte body — always invalid */
|
||||
static const uint8_t ws_frame[] = { 0x88, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
httpd_req_t req = {0};
|
||||
struct httpd_req_aux aux = {0};
|
||||
struct sock_db session = {0};
|
||||
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv rejects CLOSE frame with invalid close code", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* Close code 1005 is reserved and must not appear on the wire */
|
||||
static const uint8_t ws_frame[] = { 0x88, 0x82, 0x00, 0x00, 0x00, 0x00, 0x03, 0xED };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
httpd_req_t req = {0};
|
||||
struct httpd_req_aux aux = {0};
|
||||
struct sock_db session = {0};
|
||||
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv rejects CLOSE frame with invalid UTF-8 reason", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* Code 1000 (valid) but reason is overlong UTF-8 0xC0 0xAF */
|
||||
static const uint8_t ws_frame[] = {
|
||||
0x88, 0x84, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE8, 0xC0, 0xAF
|
||||
};
|
||||
|
||||
memset(&ws_send_capture_ctx, 0, sizeof(ws_send_capture_ctx));
|
||||
|
||||
hd.config = config;
|
||||
hd.config.max_open_sockets = 1;
|
||||
hd.hd_sd = &session;
|
||||
session.fd = 123;
|
||||
session.handle = (httpd_handle_t)&hd;
|
||||
session.send_fn = ws_scripted_send_override;
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_send_frame_async(&hd, session.fd, &frame));
|
||||
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};
|
||||
httpd_req_t req = {0};
|
||||
struct httpd_req_aux aux = {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);
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
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_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
|
||||
ws_assert_close_sent(&session, 1007);
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler receives CLOSE and server replies CLOSE", "[HTTP SERVER][websocket]")
|
||||
TEST_CASE("WS recv rejects invalid UTF-8 in text frame", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t close_frame[] = { 0x88, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
/* Binary frame mismarked as TEXT; payload 0xC0 0xAF is an overlong encoding */
|
||||
static const uint8_t ws_frame[] = { 0x82, 0x82, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xAF };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
httpd_req_t req = {0};
|
||||
struct httpd_req_aux aux = {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;
|
||||
httpd_ws_frame_t frame = {0};
|
||||
uint8_t payload[2] = {0};
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
frame.payload = payload;
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
aux.ws_type = HTTPD_WS_TYPE_TEXT;
|
||||
aux.ws_final = true;
|
||||
|
||||
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_ASSERT_EQUAL(ESP_FAIL, httpd_ws_recv_frame(&req, &frame, 2));
|
||||
ws_assert_close_sent(&session, 1007);
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler receives PONG with no reply", "[HTTP SERVER][websocket]")
|
||||
TEST_CASE("WS recv accepts CLOSE frame with IANA close code 1013", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t pong_frame[] = { 0x8A, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
/* CLOSE, MASK=1 len=2, zero mask, payload code 1013 (0x03F5) */
|
||||
static const uint8_t ws_frame[] = {
|
||||
0x88, 0x82, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF5
|
||||
};
|
||||
/* Strict mode echoes the received CLOSE payload back to the client. */
|
||||
static const uint8_t expected_reply[] = { 0x88, 0x02, 0x03, 0xF5 };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
struct httpd_req_aux aux = {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;
|
||||
httpd_req_t req = {0};
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
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);
|
||||
/* Validator accepts 1013 → auto CLOSE-echo path runs successfully. */
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_get_frame_type(&req));
|
||||
TEST_ASSERT_EQUAL(sizeof(expected_reply), ws_send_capture_ctx.len);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_reply, ws_send_capture_ctx.data, sizeof(expected_reply));
|
||||
}
|
||||
|
||||
TEST_CASE("WS without control handler auto-replies PING (backward compatible)", "[HTTP SERVER][websocket]")
|
||||
TEST_CASE("httpd_ws_validate_utf8 accepts valid and rejects malformed UTF-8", "[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;
|
||||
/* Empty buffer is valid; NULL with len=0 is also valid. */
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_validate_utf8(NULL, 0));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_validate_utf8((const uint8_t *)"", 0));
|
||||
|
||||
esp_err_t ret = httpd_req_new(&hd, &session);
|
||||
/* NULL with non-zero length is rejected. */
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(NULL, 4));
|
||||
|
||||
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 */
|
||||
/* Valid ASCII and well-formed multi-byte sequences. */
|
||||
static const uint8_t valid_ascii[] = { 'h', 'e', 'l', 'l', 'o' };
|
||||
static const uint8_t valid_two[] = { 0xC3, 0xB1 }; /* U+00F1 */
|
||||
static const uint8_t valid_three[] = { 0xE2, 0x82, 0xAC }; /* U+20AC */
|
||||
static const uint8_t valid_four[] = { 0xF0, 0x9F, 0x98, 0x80 }; /* U+1F600 */
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_validate_utf8(valid_ascii, sizeof(valid_ascii)));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_validate_utf8(valid_two, sizeof(valid_two)));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_validate_utf8(valid_three, sizeof(valid_three)));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_validate_utf8(valid_four, sizeof(valid_four)));
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
/* Overlong 2-byte (C0 80 would encode U+0000). */
|
||||
static const uint8_t overlong2[] = { 0xC0, 0x80 };
|
||||
/* Overlong 3-byte (E0 80 80). */
|
||||
static const uint8_t overlong3[] = { 0xE0, 0x80, 0x80 };
|
||||
/* UTF-16 surrogate (ED A0 80 = U+D800). */
|
||||
static const uint8_t surrogate[] = { 0xED, 0xA0, 0x80 };
|
||||
/* Beyond U+10FFFF (F4 90 80 80). */
|
||||
static const uint8_t beyond_max[] = { 0xF4, 0x90, 0x80, 0x80 };
|
||||
/* Invalid lead byte F5. */
|
||||
static const uint8_t bad_lead[] = { 0xF5, 0x80, 0x80, 0x80 };
|
||||
/* Stray continuation byte without a lead. */
|
||||
static const uint8_t stray_trail[] = { 0x80 };
|
||||
/* Truncated 2-byte sequence (C3 without trail). */
|
||||
static const uint8_t truncated[] = { 0xC3 };
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(overlong2, sizeof(overlong2)));
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(overlong3, sizeof(overlong3)));
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(surrogate, sizeof(surrogate)));
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(beyond_max, sizeof(beyond_max)));
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(bad_lead, sizeof(bad_lead)));
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(stray_trail, sizeof(stray_trail)));
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(truncated, sizeof(truncated)));
|
||||
}
|
||||
|
||||
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_STRICTER_RFC6455 */
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
/********* URL query / header pointer-accessor tests *********
|
||||
|
||||
@@ -139,6 +139,20 @@ To use the WebSocket post-handshake callback, you must enable :menuitem:`CONFIG_
|
||||
httpd_register_uri_handler(server, &ws);
|
||||
|
||||
|
||||
WebSocket Message Fragmentation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The server does not support fragmented WebSocket messages, as `RFC 6455, section 5.4 <https://tools.ietf.org/html/rfc6455#section-5.4>`_ defines them. An application that must accept or send fragments must apply the rules below itself.
|
||||
|
||||
On receive, the server passes each frame to the handler on its own. It does not join the fragments of one message. A handler that gets a TEXT message in three fragments sees three separate frames. Use the ``final`` and ``fragmented`` fields of :cpp:type:`httpd_ws_frame_t` to detect a fragment, and join the payloads in the application.
|
||||
|
||||
The server does not validate the fragment sequence. It accepts a CONTINUE frame that continues no message. It also accepts a new TEXT or BINARY frame while a fragmented message is still open. RFC 6455 requires a close with status code 1002 in both cases.
|
||||
|
||||
:menuitem:`CONFIG_HTTPD_WS_STRICTER_RFC6455` validates the UTF-8 of a complete, unfragmented TEXT frame only. It does not validate a TEXT message that arrives in fragments. To enforce `RFC 6455, section 8.1 <https://tools.ietf.org/html/rfc6455#section-8.1>`_ on such a message, join the fragments and call :cpp:func:`httpd_ws_validate_utf8` on the result.
|
||||
|
||||
On transmit, the server does not fragment a message automatically. To send fragments, set the ``fragmented`` option and mark the last fragment with the ``final`` option.
|
||||
|
||||
|
||||
WebSocket Control Frame Handler
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -69,9 +69,21 @@ httpd_ws_recv_frame(req, &ws_pkt, MAX_PAYLOAD_LEN);
|
||||
2) Allocate the size based on the received packet length
|
||||
3) Call `httpd_ws_recv_frame()` with the allocated buffer
|
||||
|
||||
#### Fragmented messages
|
||||
|
||||
The WebSocket HTTP server does not support fragmented messages, as [RFC6455, section 5.4](https://tools.ietf.org/html/rfc6455#section-5.4) defines them.
|
||||
|
||||
On receive, the server passes each frame to the handler on its own. It does not join the fragments of one message. A handler that gets a message in three fragments sees three separate frames. Use the `final` and `fragmented` fields of `httpd_ws_frame_t` to detect a fragment, and join the payloads in the application.
|
||||
|
||||
The server also does not validate the fragment sequence. It accepts a CONTINUE frame that continues no message. It also accepts a new TEXT or BINARY frame while a fragmented message is still open. RFC 6455 requires a close with status code 1002 in both cases.
|
||||
|
||||
`CONFIG_HTTPD_WS_STRICTER_RFC6455` validates the UTF-8 of a complete, unfragmented TEXT frame only. It does not validate a TEXT message that arrives in fragments. Join the fragments and call `httpd_ws_validate_utf8()` on the result.
|
||||
|
||||
This example echoes each frame as a complete message, so it does not handle fragments either.
|
||||
|
||||
#### Handling outgoing data
|
||||
|
||||
Please note that the WebSocket HTTP server does not automatically fragment messages.
|
||||
On transmit, the server does not automatically fragment messages.
|
||||
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).
|
||||
|
||||
@@ -215,9 +215,11 @@ static esp_err_t echo_handler(httpd_req_t *req)
|
||||
}
|
||||
}
|
||||
|
||||
ret = httpd_ws_send_frame(req, &ws_pkt);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
|
||||
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT || ws_pkt.type == HTTPD_WS_TYPE_BINARY) {
|
||||
ret = httpd_ws_send_frame(req, &ws_pkt);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
return ret;
|
||||
|
||||
Reference in New Issue
Block a user