mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
Merge branch 'fix/esp_http_server-ws-close-and-utf8-strictness' into 'master'
Fix(esp_http_server): ws close and utf8 strictness See merge request espressif/esp-idf!48908
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
|
||||
|
||||
@@ -493,18 +493,29 @@ typedef struct httpd_uri {
|
||||
* Optional dedicated handler for WebSocket control frames (PING, PONG, CLOSE).
|
||||
*
|
||||
* Only takes effect when handle_ws_control_frames is true. When set, control
|
||||
* frames are delivered to this handler instead of the data handler. The server
|
||||
* has already received the frame (passed via the read-only frame argument), and
|
||||
* after this handler returns the server performs the protocol reply itself
|
||||
* (PONG for PING, CLOSE for CLOSE). The frame and its payload are owned by the
|
||||
* server and are only valid for the duration of the call; the handler must not
|
||||
* free or retain them. If left NULL, control frames continue to be delivered to
|
||||
* the data handler (unchanged behavior).
|
||||
* frames are delivered to this handler instead of the data handler, so the data
|
||||
* handler only ever sees data frames (CONTINUE, TEXT, BINARY).
|
||||
*
|
||||
* The server has already received the frame body for you - no allocation or
|
||||
* httpd_ws_recv_frame() call is needed - but it does *not* reply. Consistent with
|
||||
* handle_ws_control_frames being true, this handler owns the protocol reply:
|
||||
* answer a PING with a PONG echoing the payload and a CLOSE with a CLOSE
|
||||
* (RFC 6455 section 5.5); a PONG needs no reply. The frame may be reused for the
|
||||
* reply by overwriting frame->type and passing it to httpd_ws_send_frame().
|
||||
*
|
||||
* The frame and its payload are owned by the server and are only valid for the
|
||||
* duration of the call: the handler must not free or retain them, and must not
|
||||
* grow frame->len beyond the received length, as the payload buffer is sized for
|
||||
* a control frame only. Returning an error closes the connection.
|
||||
*
|
||||
* If left NULL, control frames continue to be delivered to the data handler,
|
||||
* which remains responsible for receiving and replying to them (unchanged
|
||||
* behavior).
|
||||
*
|
||||
* Placed at the end of the struct to keep positional initialization of existing
|
||||
* fields backward compatible.
|
||||
*/
|
||||
esp_err_t (*ws_control_handler)(httpd_req_t *req, const httpd_ws_frame_t *frame);
|
||||
esp_err_t (*ws_control_handler)(httpd_req_t *req, httpd_ws_frame_t *frame);
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
} httpd_uri_t;
|
||||
|
||||
@@ -1857,6 +1868,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 +1896,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 +1914,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
|
||||
@@ -1960,6 +2001,33 @@ esp_err_t httpd_ws_send_data(httpd_handle_t handle, int socket, httpd_ws_frame_t
|
||||
esp_err_t httpd_ws_send_data_async(httpd_handle_t handle, int socket, httpd_ws_frame_t *frame,
|
||||
transfer_complete_cb callback, void *arg);
|
||||
|
||||
/**
|
||||
* @brief Initiate a graceful WebSocket close handshake on a session.
|
||||
*
|
||||
* Sends a CLOSE frame with the given status code and optional UTF-8 reason,
|
||||
* marks the session as closing (which blocks any further outbound data
|
||||
* frames per RFC 6455 §1.4), and lets the underlying TCP socket be torn
|
||||
* down at the next dispatch boundary. The call is idempotent: if the
|
||||
* session was already closing it returns ESP_OK without emitting a second
|
||||
* CLOSE frame, and the code and reason arguments are not validated.
|
||||
*
|
||||
* @param[in] hd Server handle.
|
||||
* @param[in] fd Socket descriptor of the session to close.
|
||||
* @param[in] code Status code per RFC 6455 §7.4 (e.g., 1000 for normal
|
||||
* closure). Reserved codes (1005, 1006) and out-of-range
|
||||
* values are rejected.
|
||||
* @param[in] reason Optional NUL-terminated UTF-8 reason, or NULL. Must be
|
||||
* at most 123 bytes so the total control frame payload
|
||||
* (2-byte code + reason) fits in 125 bytes.
|
||||
* @return
|
||||
* - ESP_OK : CLOSE sent (or session was already closing).
|
||||
* - ESP_ERR_INVALID_ARG : Invalid fd, invalid code, reason exceeds 123 bytes,
|
||||
* or reason is not valid UTF-8.
|
||||
* - ESP_ERR_INVALID_STATE : Socket is not an established WebSocket session.
|
||||
* - ESP_FAIL : Underlying transport send failed.
|
||||
*/
|
||||
esp_err_t httpd_ws_close_session(httpd_handle_t hd, int fd, uint16_t code, const char *reason);
|
||||
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT || __DOXYGEN__ */
|
||||
/** End of WebSocket related stuff
|
||||
* @}
|
||||
|
||||
@@ -87,9 +87,10 @@ 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 */
|
||||
esp_err_t (*ws_control_handler)(httpd_req_t *r, httpd_ws_frame_t *frame); /*!< Dedicated WebSocket control-frame handler, NULL if not used */
|
||||
void *ws_user_ctx; /*!< Pointer to user context data which will be available to handler for websocket*/
|
||||
#endif
|
||||
};
|
||||
@@ -581,8 +582,10 @@ esp_err_t httpd_ws_recv_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame,
|
||||
/**
|
||||
* @brief Send the protocol reply for a received WebSocket control frame.
|
||||
*
|
||||
* @note PING is answered with a PONG echoing the payload; CLOSE is answered with
|
||||
* an empty CLOSE; all other control frames (e.g. PONG) require no reply.
|
||||
* @note PING is answered with a PONG echoing the payload. CLOSE is answered with
|
||||
* a CLOSE that echoes the received status code and reason when
|
||||
* CONFIG_HTTPD_WS_STRICTER_RFC6455 is set, and with an empty CLOSE
|
||||
* otherwise. All other control frames (for example PONG) require no reply.
|
||||
*
|
||||
* @param[in] req WebSocket request
|
||||
* @param[in] frame Control frame previously received (modified in place)
|
||||
@@ -596,15 +599,18 @@ esp_err_t httpd_ws_reply_to_control_frame(httpd_req_t *req, httpd_ws_frame_t *fr
|
||||
* @brief Handle an incoming WebSocket control frame via the dedicated control handler.
|
||||
*
|
||||
* @note Used only when a ws_control_handler is registered (handle_ws_control_frames
|
||||
* must be true). The server receives the frame body, passes a read-only view
|
||||
* to the control handler, then performs the protocol reply itself. If the
|
||||
* control handler returns an error, the reply is still sent and the error is
|
||||
* propagated so the caller closes the socket.
|
||||
* must be true). The server receives the frame body and hands it to the
|
||||
* control handler, which owns the protocol reply. The server does not reply
|
||||
* on its own, so a handler that answers a PING cannot produce a duplicate
|
||||
* PONG on the wire. A handler error is propagated so the caller closes the
|
||||
* socket.
|
||||
*
|
||||
* @param[in] req WebSocket request
|
||||
* @return
|
||||
* - ESP_OK : Control frame handled and replied
|
||||
* - others : Control handler error, or frame could not be received/replied
|
||||
* - ESP_OK : Control frame received and handled
|
||||
* - ESP_ERR_INVALID_ARG : Argument is invalid (null request aux or session)
|
||||
* - ESP_ERR_INVALID_STATE : No control handler registered for this session
|
||||
* - others : Control handler error, or frame could not be received
|
||||
*/
|
||||
esp_err_t httpd_ws_handle_control_frame(httpd_req_t *req);
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
@@ -853,7 +853,7 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
|
||||
|
||||
/* Dispatch the frame:
|
||||
* - Control frames (CLOSE/PING/PONG) go to the dedicated control handler
|
||||
* when one is registered; the server then sends the protocol reply.
|
||||
* when one is registered; that handler owns the protocol reply.
|
||||
* - Otherwise dispatch to the data handler for non-control frames, PONG
|
||||
* frames, or when the handler opted in to receiving control frames.
|
||||
* PONG must be dispatched so that user heartbeat code can track it and
|
||||
|
||||
@@ -54,8 +54,14 @@ static const char *TAG="httpd_ws";
|
||||
#define HTTPD_WS_MASK_BIT 0x80U
|
||||
#define HTTPD_WS_LENGTH_BITS 0x7fU
|
||||
|
||||
/* RFC 6455 §5.5: a control frame payload is at most 125 bytes, so a CLOSE
|
||||
* reason gets whatever is left after the 2-byte status code. */
|
||||
#define HTTPD_WS_CONTROL_PAYLOAD_MAX 125U
|
||||
#define HTTPD_WS_CLOSE_REASON_MAX (HTTPD_WS_CONTROL_PAYLOAD_MAX - 2U)
|
||||
|
||||
/* 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
|
||||
|
||||
/*
|
||||
@@ -149,7 +155,7 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
ESP_LOGW(TAG, LOG_FMT("\"Host\" is not found"));
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request", "Missing Host header", NULL);
|
||||
}
|
||||
#endif
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
/* 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");
|
||||
@@ -164,7 +170,7 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Version header", NULL);
|
||||
}
|
||||
#endif
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
char *version_val = calloc(1, version_hdr_len + 1);
|
||||
if (version_val == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to allocate version header buffer");
|
||||
@@ -198,7 +204,7 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Key header", NULL);
|
||||
}
|
||||
#endif
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
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");
|
||||
@@ -220,7 +226,7 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
return httpd_ws_send_handshake_error(req, "400 Bad Request",
|
||||
"Invalid Sec-WebSocket-Key header", NULL);
|
||||
}
|
||||
#endif
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
/* Prepare server key (Sec-WebSocket-Accept), concat the string */
|
||||
char server_key_encoded[33] = { '\0' };
|
||||
@@ -370,6 +376,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 +404,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 +428,87 @@ 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;
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
/* 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) {
|
||||
@@ -426,6 +523,21 @@ static esp_err_t httpd_ws_unmask_payload(uint8_t *payload, size_t len, const uin
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Short reads inside a frame leave the WS stream desynchronized, so fail the
|
||||
* connection per RFC 6455 §7.1.7 to mark the session closing and let the
|
||||
* dispatcher tear down the TCP socket cleanly. A CLOSE frame is still
|
||||
* attempted; if the peer is already gone the send simply fails and
|
||||
* httpd_ws_fail_connection() logs a warning. */
|
||||
static inline esp_err_t httpd_ws_recv_short_read_fail(httpd_req_t *req)
|
||||
{
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
|
||||
#else
|
||||
(void)req;
|
||||
return ESP_FAIL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t *frame, size_t max_len, bool partial)
|
||||
{
|
||||
esp_err_t ret = httpd_ws_check_req(req);
|
||||
@@ -454,7 +566,7 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
int recv_ret = httpd_recv_with_opt(req, (char *)&second_byte, sizeof(second_byte), HTTPD_RECV_OPT_BLOCKING);
|
||||
if (recv_ret != (int)sizeof(second_byte)) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to receive the second byte"));
|
||||
return ESP_FAIL;
|
||||
return httpd_ws_recv_short_read_fail(req);
|
||||
}
|
||||
|
||||
/* Parse the second byte */
|
||||
@@ -483,7 +595,7 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
recv_ret = httpd_recv_with_opt(req, (char *)length_bytes, sizeof(length_bytes), HTTPD_RECV_OPT_BLOCKING);
|
||||
if (recv_ret != (int)sizeof(length_bytes)) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to receive 2 bytes length"));
|
||||
return ESP_FAIL;
|
||||
return httpd_ws_recv_short_read_fail(req);
|
||||
}
|
||||
|
||||
uint16_t length = ((uint16_t)(length_bytes[0] << 8U) | (length_bytes[1]));
|
||||
@@ -501,7 +613,7 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
recv_ret = httpd_recv_with_opt(req, (char *)length_bytes, sizeof(length_bytes), HTTPD_RECV_OPT_BLOCKING);
|
||||
if (recv_ret != (int)sizeof(length_bytes)) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to receive 8 bytes length"));
|
||||
return ESP_FAIL;
|
||||
return httpd_ws_recv_short_read_fail(req);
|
||||
}
|
||||
|
||||
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
|
||||
@@ -540,7 +652,7 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
recv_ret = httpd_recv_with_opt(req, (char *)aux->mask_key, sizeof(aux->mask_key), HTTPD_RECV_OPT_BLOCKING);
|
||||
if (recv_ret != (int)sizeof(aux->mask_key)) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to receive mask key"));
|
||||
return ESP_FAIL;
|
||||
return httpd_ws_recv_short_read_fail(req);
|
||||
}
|
||||
} else {
|
||||
/* If the WS frame from client to server is not masked, it should be rejected.
|
||||
@@ -561,6 +673,9 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
/* When reading entire packet at once, we only accept the incoming packet length that is smaller than the max_len (or it will overflow the buffer!) */
|
||||
if (!partial) {
|
||||
ESP_LOGW(TAG, LOG_FMT("WS Message too long"));
|
||||
/* The payload is still queued in the socket. Fail the connection
|
||||
* (RFC 6455 §7.4.1, close code 1009) so the session is torn down. */
|
||||
httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_TOO_BIG);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
ESP_LOGD(TAG, LOG_FMT("WS Message too long. User will have to call read again"));
|
||||
@@ -584,7 +699,7 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t
|
||||
int read_len = httpd_recv_with_opt(req, (char *)frame->payload + offset, left_len, HTTPD_RECV_OPT_NONE);
|
||||
if (read_len <= 0) {
|
||||
ESP_LOGW(TAG, LOG_FMT("Failed to receive payload"));
|
||||
return ESP_FAIL;
|
||||
return httpd_ws_recv_short_read_fail(req);
|
||||
}
|
||||
offset += read_len;
|
||||
left_len -= read_len;
|
||||
@@ -598,6 +713,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 +801,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 +831,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 +859,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 */
|
||||
@@ -731,6 +881,9 @@ esp_err_t httpd_ws_handle_control_frame(httpd_req_t *req)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
struct sock_db *sd = aux->sd;
|
||||
if (sd->ws_control_handler == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* The server receives the control-frame body itself. Oversized or malformed
|
||||
* frames are rejected by the max_len cap (zero-trust on client input). */
|
||||
@@ -741,17 +894,9 @@ esp_err_t httpd_ws_handle_control_frame(httpd_req_t *req)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Notify the user's control handler with a read-only view of the frame. */
|
||||
esp_err_t handler_ret = ESP_OK;
|
||||
if (sd->ws_control_handler != NULL) {
|
||||
handler_ret = sd->ws_control_handler(req, &frame);
|
||||
}
|
||||
|
||||
/* The server always performs the protocol reply (PONG for PING, CLOSE for
|
||||
* CLOSE). If the handler failed, still reply, then propagate the error so the
|
||||
* caller closes the socket. */
|
||||
esp_err_t reply_ret = httpd_ws_reply_to_control_frame(req, &frame);
|
||||
return (handler_ret != ESP_OK) ? handler_ret : reply_ret;
|
||||
/* Hand the frame over. The server deliberately does not reply: with
|
||||
* handle_ws_control_frames true the application owns the protocol reply */
|
||||
return sd->ws_control_handler(req, &frame);
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
|
||||
@@ -925,4 +1070,62 @@ esp_err_t httpd_ws_send_data_async(httpd_handle_t handle, int socket, httpd_ws_f
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_close_session(httpd_handle_t hd, int fd, uint16_t code, const char *reason)
|
||||
{
|
||||
struct sock_db *sess = httpd_sess_get(hd, fd);
|
||||
if (!sess) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!sess->ws_handshake_done) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Idempotent: do not emit a second CLOSE if the session is already closing.
|
||||
* Checked before argument validation since nothing will be sent. */
|
||||
if (sess->ws_close) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (!httpd_ws_is_valid_close_code(code)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Reason is optional. Reject anything that would push the control frame
|
||||
* payload past the 125-byte cap or that isn't well-formed UTF-8 (§5.5/§8.1). */
|
||||
size_t reason_len = (reason != NULL) ? strlen(reason) : 0;
|
||||
if (reason_len > HTTPD_WS_CLOSE_REASON_MAX) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (reason_len > 0 && httpd_ws_validate_utf8((const uint8_t *)reason, reason_len) != ESP_OK) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t payload[HTTPD_WS_CONTROL_PAYLOAD_MAX];
|
||||
payload[0] = (uint8_t)(code >> 8U);
|
||||
payload[1] = (uint8_t)(code & 0xFFU);
|
||||
if (reason_len > 0) {
|
||||
memcpy(&payload[2], reason, reason_len);
|
||||
}
|
||||
|
||||
httpd_ws_frame_t close_frame = {
|
||||
.final = true,
|
||||
.fragmented = false,
|
||||
.type = HTTPD_WS_TYPE_CLOSE,
|
||||
.payload = payload,
|
||||
.len = 2 + reason_len,
|
||||
};
|
||||
|
||||
/* Mark closing before send so the gate in httpd_ws_send_frame_async
|
||||
* blocks any data frame a concurrent task tries to emit after this point.
|
||||
* If the send fails (transport error), roll back the flag so the caller
|
||||
* can retry; otherwise a transient TCP failure would permanently strand
|
||||
* the session. */
|
||||
sess->ws_close = true;
|
||||
esp_err_t ret = httpd_ws_send_frame_async(hd, fd, &close_frame);
|
||||
if (ret != ESP_OK) {
|
||||
sess->ws_close = false;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
@@ -107,6 +107,16 @@ static int ws_recv_fail_override(httpd_handle_t hd, int sockfd, char *buf, size_
|
||||
return HTTPD_SOCK_ERR_FAIL;
|
||||
}
|
||||
|
||||
static int ws_send_fail_override(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags)
|
||||
{
|
||||
(void)hd;
|
||||
(void)sockfd;
|
||||
(void)buf;
|
||||
(void)buf_len;
|
||||
(void)flags;
|
||||
return HTTPD_SOCK_ERR_FAIL;
|
||||
}
|
||||
|
||||
static int ws_scripted_recv_override(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags)
|
||||
{
|
||||
(void)hd;
|
||||
@@ -613,13 +623,29 @@ static esp_err_t ws_data_handler_spy(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t ws_control_handler_spy(httpd_req_t *req, const httpd_ws_frame_t *frame)
|
||||
/* Mirrors what an application does: the control handler owns the protocol reply.
|
||||
* On a handler failure nothing is sent, because no other code replies for it. */
|
||||
static esp_err_t ws_control_handler_spy(httpd_req_t *req, 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;
|
||||
if (s_ws_control_ret != ESP_OK) {
|
||||
return s_ws_control_ret;
|
||||
}
|
||||
|
||||
switch (frame->type) {
|
||||
case HTTPD_WS_TYPE_PING:
|
||||
frame->type = HTTPD_WS_TYPE_PONG;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
case HTTPD_WS_TYPE_CLOSE:
|
||||
frame->len = 0;
|
||||
frame->payload = NULL;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
default:
|
||||
/* A PONG needs no reply */
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wire a fake session/server and reset all fixtures around a single frame. */
|
||||
@@ -824,11 +850,28 @@ 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)
|
||||
/* Common fake-session wiring for the send-path tests: a single-socket server
|
||||
* whose send is captured. ws_handshake_done is left to the caller so tests can
|
||||
* exercise the not-yet-handshaken path. */
|
||||
static void ws_setup_send_fixture(struct httpd_data *hd, struct sock_db *session)
|
||||
{
|
||||
static const uint8_t expected_reply[] = { 0x88, 0x02, 0x03, 0xEA };
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
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 +889,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 +904,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 +919,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 +934,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 +954,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 +971,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 +990,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 +1009,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,6 +1051,306 @@ 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));
|
||||
}
|
||||
|
||||
TEST_CASE("WS auto CLOSE reply echoes received payload", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* 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' };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
struct httpd_req_aux aux = {0};
|
||||
struct sock_db session = {0};
|
||||
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 oversized frame fails connection with CLOSE 1009", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* A masked TEXT frame with a 4-byte payload, read with max_len = 2. The frame
|
||||
* does not fit, and the payload is still queued in the socket. The library
|
||||
* must fail the connection (CLOSE 1009). The scripted stream carries a second TEXT frame
|
||||
* after the payload to represent that queued data. */
|
||||
static const uint8_t ws_frame[] = {
|
||||
0x84, 0x00, 0x00, 0x00, 0x00, 'd', 'a', 't', 'a', /* first frame, len 4, zero mask */
|
||||
0x81, 0x80, 0x00, 0x00, 0x00, 0x00 /* a second frame that must never be read */
|
||||
};
|
||||
struct httpd_data hd = {0};
|
||||
httpd_req_t req = {0};
|
||||
struct httpd_req_aux aux = {0};
|
||||
struct sock_db session = {0};
|
||||
httpd_ws_frame_t frame = {0};
|
||||
|
||||
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_ERR_INVALID_SIZE, httpd_ws_recv_frame(&req, &frame, 2));
|
||||
ws_assert_close_sent(&session, 1009);
|
||||
|
||||
/* The library must fail, not drain: only the length byte and the 4 mask
|
||||
* bytes are consumed. The 4 payload bytes and the whole second frame stay
|
||||
* unread. */
|
||||
TEST_ASSERT_EQUAL(5, ws_scripted_recv_ctx.offset);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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, 1007);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv rejects invalid UTF-8 in text frame", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* recv_frame starts at the second byte (get_frame_type already consumed the
|
||||
* opcode). Second byte 0x82: MASK=1 len=2, zero mask key, payload 0xC0 0xAF
|
||||
* (an overlong UTF-8 encoding). aux.ws_type is forced to TEXT below so the
|
||||
* UTF-8 validator runs on this payload. */
|
||||
static const uint8_t ws_frame[] = { 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};
|
||||
httpd_ws_frame_t frame = {0};
|
||||
uint8_t payload[2] = {0};
|
||||
|
||||
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_FAIL, httpd_ws_recv_frame(&req, &frame, 2));
|
||||
ws_assert_close_sent(&session, 1007);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv accepts CLOSE frame with IANA close code 1013", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* 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};
|
||||
httpd_req_t req = {0};
|
||||
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
|
||||
/* 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("httpd_ws_validate_utf8 accepts valid and rejects malformed UTF-8", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* 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));
|
||||
|
||||
/* NULL with non-zero length is rejected. */
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_validate_utf8(NULL, 4));
|
||||
|
||||
/* 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)));
|
||||
|
||||
/* 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("httpd_ws_close_session emits CLOSE with code and reason, marks closing", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* Expected wire bytes: FIN|CLOSE, len=5, 0x03E8 (=1000), "bye" */
|
||||
static const uint8_t expected[] = { 0x88, 0x05, 0x03, 0xE8, 'b', 'y', 'e' };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
|
||||
ws_setup_send_fixture(&hd, &session);
|
||||
session.ws_handshake_done = true;
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_close_session(&hd, session.fd, 1000, "bye"));
|
||||
TEST_ASSERT_TRUE(session.ws_close);
|
||||
TEST_ASSERT_EQUAL(sizeof(expected), ws_send_capture_ctx.len);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, ws_send_capture_ctx.data, sizeof(expected));
|
||||
}
|
||||
|
||||
TEST_CASE("httpd_ws_close_session rejects invalid input and is idempotent", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
/* Reason buffer 124 bytes (exceeds 123-byte cap). */
|
||||
char too_long[125];
|
||||
memset(too_long, 'x', sizeof(too_long) - 1);
|
||||
too_long[sizeof(too_long) - 1] = '\0';
|
||||
/* Reason containing overlong UTF-8. */
|
||||
static const char bad_utf8[] = { (char)0xC0, (char)0x80, '\0' };
|
||||
|
||||
ws_setup_send_fixture(&hd, &session);
|
||||
|
||||
/* Not-yet-handshaken socket → ESP_ERR_INVALID_STATE, no send. */
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_close_session(&hd, session.fd, 1000, NULL));
|
||||
TEST_ASSERT_EQUAL(0, ws_send_capture_ctx.len);
|
||||
|
||||
session.ws_handshake_done = true;
|
||||
|
||||
/* Reserved close code → ESP_ERR_INVALID_ARG, no send. */
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_close_session(&hd, session.fd, 1005, NULL));
|
||||
TEST_ASSERT_EQUAL(0, ws_send_capture_ctx.len);
|
||||
|
||||
/* Reason over 123 bytes → ESP_ERR_INVALID_ARG, no send. */
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_close_session(&hd, session.fd, 1000, too_long));
|
||||
TEST_ASSERT_EQUAL(0, ws_send_capture_ctx.len);
|
||||
|
||||
/* Reason with malformed UTF-8 → ESP_ERR_INVALID_ARG, no send. */
|
||||
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, httpd_ws_close_session(&hd, session.fd, 1000, bad_utf8));
|
||||
TEST_ASSERT_EQUAL(0, ws_send_capture_ctx.len);
|
||||
|
||||
/* Valid close emits a CLOSE frame and marks ws_close. */
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_close_session(&hd, session.fd, 1000, NULL));
|
||||
TEST_ASSERT_TRUE(session.ws_close);
|
||||
TEST_ASSERT_EQUAL(4, ws_send_capture_ctx.len); /* 0x88 0x02 0x03 0xE8 */
|
||||
|
||||
/* Second call on an already-closing session is a no-op (returns OK, no second send). */
|
||||
size_t bytes_after_first = ws_send_capture_ctx.len;
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_close_session(&hd, session.fd, 1001, "again"));
|
||||
TEST_ASSERT_EQUAL(bytes_after_first, ws_send_capture_ctx.len);
|
||||
|
||||
/* Already-closing wins over argument validation: invalid code and reason
|
||||
* are not inspected because nothing will be sent. */
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_close_session(&hd, session.fd, 1005, bad_utf8));
|
||||
TEST_ASSERT_EQUAL(bytes_after_first, ws_send_capture_ctx.len);
|
||||
}
|
||||
|
||||
TEST_CASE("WS recv short mask-key read fails the connection", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
/* Second byte 0x82: MASK=1 len=2. The buffer ends here, so the mask-key
|
||||
* read returns short. The desynchronized stream must fail the connection
|
||||
* per RFC 6455 §7.1.7: session marked closing, CLOSE 1002 emitted. */
|
||||
static const uint8_t ws_frame[] = { 0x82 };
|
||||
|
||||
struct httpd_data hd = {0};
|
||||
httpd_req_t req = {0};
|
||||
struct httpd_req_aux aux = {0};
|
||||
struct sock_db session = {0};
|
||||
httpd_ws_frame_t frame = {0};
|
||||
|
||||
ws_setup_recv_fixture(&hd, &req, &aux, &session, ws_frame, sizeof(ws_frame));
|
||||
aux.ws_type = HTTPD_WS_TYPE_BINARY;
|
||||
aux.ws_final = true;
|
||||
|
||||
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_sent(&session, 1002);
|
||||
}
|
||||
|
||||
TEST_CASE("httpd_ws_close_session rolls back ws_close when send fails", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
struct httpd_data hd = {0};
|
||||
struct sock_db session = {0};
|
||||
|
||||
ws_setup_send_fixture(&hd, &session);
|
||||
session.send_fn = ws_send_fail_override; /* every send returns -1 */
|
||||
session.ws_handshake_done = true;
|
||||
|
||||
/* First attempt: transport send fails, ws_close must be rolled back so
|
||||
* the caller can retry and the post-CLOSE outbound gate doesn't strand
|
||||
* the session. */
|
||||
TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_close_session(&hd, session.fd, 1000, NULL));
|
||||
TEST_ASSERT_FALSE(session.ws_close);
|
||||
|
||||
/* A second attempt with a working transport now succeeds (proving the
|
||||
* idempotency short-circuit didn't permanently no-op the session). */
|
||||
session.send_fn = ws_scripted_send_override;
|
||||
memset(&ws_send_capture_ctx, 0, sizeof(ws_send_capture_ctx));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_close_session(&hd, session.fd, 1000, NULL));
|
||||
TEST_ASSERT_TRUE(session.ws_close);
|
||||
TEST_ASSERT_EQUAL(4, ws_send_capture_ctx.len); /* 0x88 0x02 0x03 0xE8 */
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload", "[HTTP SERVER][websocket]")
|
||||
@@ -1037,7 +1380,7 @@ TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload",
|
||||
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]")
|
||||
TEST_CASE("WS control handler owns the PONG reply for PING", "[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' };
|
||||
@@ -1055,13 +1398,13 @@ TEST_CASE("WS control handler receives PING and server replies PONG", "[HTTP SER
|
||||
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_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* FIN | PONG, sent by the handler */
|
||||
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]")
|
||||
TEST_CASE("WS control handler owns the CLOSE reply for CLOSE", "[HTTP SERVER][websocket]")
|
||||
{
|
||||
static const uint8_t close_frame[] = { 0x88, 0x80, 0x00, 0x00, 0x00, 0x00 };
|
||||
struct httpd_data hd = {0};
|
||||
@@ -1077,7 +1420,7 @@ TEST_CASE("WS control handler receives CLOSE and server replies CLOSE", "[HTTP S
|
||||
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_EQUAL_HEX8(0x88, s_ws_sent[0]); /* FIN | CLOSE, sent by the handler */
|
||||
TEST_ASSERT_TRUE(session.ws_close); /* server marked the session for close */
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
@@ -1126,11 +1469,12 @@ TEST_CASE("WS without control handler auto-replies PING (backward compatible)",
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
TEST_CASE("WS control handler error still replies then closes socket", "[HTTP SERVER][websocket]")
|
||||
TEST_CASE("WS control handler error sends no reply and 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. */
|
||||
* control socket. The handler owns the reply, so a handler failure puts nothing
|
||||
* on the wire, and httpd_req_new() propagates 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};
|
||||
@@ -1143,8 +1487,7 @@ TEST_CASE("WS control handler error still replies then closes socket", "[HTTP SE
|
||||
|
||||
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 */
|
||||
TEST_ASSERT_EQUAL(0, s_ws_sent_len); /* no reply: the handler owns it */
|
||||
|
||||
free(hd.hd_req_aux.resp_hdrs);
|
||||
}
|
||||
|
||||
@@ -139,22 +139,48 @@ 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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By default, the server replies to WebSocket control frames automatically — a PING frame is answered with a PONG and a CLOSE frame is answered with a CLOSE — without involving the application. Setting ``handle_ws_control_frames`` to true in :cpp:type:`httpd_uri_t` disables this behavior and delivers control frames to the data handler, which then becomes responsible for receiving the frame and sending the protocol replies itself.
|
||||
By default, the server replies to WebSocket control frames automatically. A PING frame gets a PONG, and a CLOSE frame gets a CLOSE. The application is not involved.
|
||||
|
||||
The ``ws_control_handler`` callback provides a middle ground: when it is set (and ``handle_ws_control_frames`` is true), control frames (PING, PONG, CLOSE) are delivered to this dedicated handler instead of the data handler, while the server still receives the frame body and performs the protocol replies itself after the handler returns. This is useful for observing heartbeats (tracking PONG responses) or logging the close reason without re-implementing the reply logic.
|
||||
Set ``handle_ws_control_frames`` to true in :cpp:type:`httpd_uri_t` to turn off the automatic reply. Control frames then go to the data handler. That handler must receive each frame and send the protocol reply itself.
|
||||
|
||||
The frame passed to the handler is read-only and owned by the server; it is only valid for the duration of the call, so the handler must not free or retain it. If the handler returns an error, the server still sends the protocol reply and then closes the connection.
|
||||
The ``ws_control_handler`` callback keeps control frames out of the data handler. Set it together with ``handle_ws_control_frames``. Control frames (PING, PONG, CLOSE) then go to this dedicated handler, and the data handler only sees data frames.
|
||||
|
||||
The server receives the frame body for the handler, so no allocation and no :cpp:func:`httpd_ws_recv_frame` call is needed. The server does not reply. The handler owns the protocol reply. Answer a PING with a PONG that echoes the payload. Answer a CLOSE with a CLOSE. A PONG needs no reply. To send the reply, overwrite ``frame->type`` and pass the frame to :cpp:func:`httpd_ws_send_frame`.
|
||||
|
||||
The server owns the frame and its payload. Both are valid only for the duration of the call. The handler must not free them and must not retain them. The handler must not increase ``frame->len`` above the received length, because the payload buffer holds a control frame only. If the handler returns an error, the server sends no reply and closes the connection.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
static esp_err_t ws_control_frame_handler(httpd_req_t *req, const httpd_ws_frame_t *frame)
|
||||
static esp_err_t ws_control_frame_handler(httpd_req_t *req, httpd_ws_frame_t *frame)
|
||||
{
|
||||
// Observe PING/PONG/CLOSE here (e.g. heartbeat tracking, logging).
|
||||
// The server sends the protocol reply itself after this returns.
|
||||
return ESP_OK;
|
||||
switch (frame->type) {
|
||||
case HTTPD_WS_TYPE_PING:
|
||||
frame->type = HTTPD_WS_TYPE_PONG; // reuse the frame for the reply
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
case HTTPD_WS_TYPE_CLOSE:
|
||||
frame->len = 0; // an empty CLOSE is a valid reply
|
||||
frame->payload = NULL;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
default:
|
||||
return ESP_OK; // a PONG needs no reply
|
||||
}
|
||||
}
|
||||
|
||||
// Registering a WebSocket URI handler with a dedicated 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).
|
||||
@@ -84,10 +96,10 @@ This example registers a dedicated control-frame handler on the `/ws` endpoint (
|
||||
|
||||
```c
|
||||
.handle_ws_control_frames = true,
|
||||
.ws_control_handler = ws_control_frame_handler, // observes PING/PONG/CLOSE
|
||||
.ws_control_handler = ws_control_frame_handler, // handles 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.
|
||||
Control frames then go to `ws_control_frame_handler()` instead of the data handler, so `echo_handler()` only deals with TEXT/BINARY frames. The server receives the control frame body for the handler but does not reply, so the handler sends the protocol reply itself (a PONG echoing the payload for a PING, an empty CLOSE for a CLOSE). 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
|
||||
|
||||
@@ -25,9 +25,9 @@ menu "Example Configuration"
|
||||
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.
|
||||
control frames (PING, PONG, CLOSE) on the /ws endpoint, keeping them
|
||||
out of the data handler. The server receives the frame body for the
|
||||
handler, which then sends the protocol replies itself (PONG for
|
||||
PING, CLOSE for CLOSE).
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -141,31 +141,39 @@ static esp_err_t ws_post_handshake_cb(httpd_req_t *req)
|
||||
|
||||
#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.
|
||||
* Dedicated control-frame handler: keeps PING/PONG/CLOSE out of the data handler
|
||||
* below, which therefore only deals with TEXT/BINARY frames.
|
||||
*
|
||||
* The server has already received the frame body (no allocation needed here) but
|
||||
* does not reply, so this handler owns the protocol reply: a PONG echoing the
|
||||
* payload for a PING, an empty CLOSE for a CLOSE, nothing for a PONG. The frame
|
||||
* is owned by the server and is reused in place for the reply; it must not be
|
||||
* freed, retained, or grown past the received length.
|
||||
*
|
||||
* 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)
|
||||
static esp_err_t ws_control_frame_handler(httpd_req_t *req, 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;
|
||||
ESP_LOGI(TAG, "Control frame: PING (len %d), replying PONG", frame->len);
|
||||
frame->type = HTTPD_WS_TYPE_PONG;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
case HTTPD_WS_TYPE_PONG:
|
||||
/* Reply to our own PING; nothing to send back (RFC 6455, section 5.5.3) */
|
||||
ESP_LOGI(TAG, "Control frame: PONG, heartbeat alive");
|
||||
break;
|
||||
return ESP_OK;
|
||||
case HTTPD_WS_TYPE_CLOSE:
|
||||
ESP_LOGI(TAG, "Control frame: CLOSE (len %d), server replies CLOSE", frame->len);
|
||||
break;
|
||||
ESP_LOGI(TAG, "Control frame: CLOSE (len %d), replying CLOSE", frame->len);
|
||||
frame->len = 0;
|
||||
frame->payload = NULL;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
default:
|
||||
ESP_LOGI(TAG, "Control frame: type %d", frame->type);
|
||||
break;
|
||||
/* Reserved control opcode: fail the connection (RFC 6455, section 5.2) */
|
||||
ESP_LOGW(TAG, "Control frame: reserved opcode %d, closing connection", frame->type);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
|
||||
|
||||
@@ -215,9 +223,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;
|
||||
@@ -310,8 +320,8 @@ static const httpd_uri_t ws = {
|
||||
.user_ctx = NULL,
|
||||
.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. */
|
||||
/* Route control frames to the dedicated handler, which owns the
|
||||
* protocol replies; echo_handler() then only sees data frames. */
|
||||
.handle_ws_control_frames = true,
|
||||
.ws_control_handler = ws_control_frame_handler,
|
||||
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
|
||||
|
||||
Reference in New Issue
Block a user