mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
feat(esp_http_server): add dedicated WebSocket control-frame handler
Closes https://github.com/espressif/esp-idf/issues/18448
This commit is contained in:
@@ -89,6 +89,7 @@ struct sock_db {
|
||||
bool ws_close; /*!< Set to true to close the socket later (when WS Close frame received) */
|
||||
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 */
|
||||
void *ws_user_ctx; /*!< Pointer to user context data which will be available to handler for websocket*/
|
||||
#endif
|
||||
};
|
||||
@@ -553,6 +554,61 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
|
||||
*/
|
||||
esp_err_t httpd_ws_get_frame_type(httpd_req_t *req);
|
||||
|
||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||
/* Control frames carry at most 125 bytes of payload (RFC 6455 §5.5). These values
|
||||
* match the historical auto-reply path: a 128-byte receive buffer, and a 126-byte
|
||||
* cap passed to httpd_ws_recv_frame(). */
|
||||
#define HTTPD_WS_CTRL_FRAME_BUF_LEN 128
|
||||
#define HTTPD_WS_CTRL_FRAME_MAX_LEN 126
|
||||
|
||||
/**
|
||||
* @brief Receive the body of a WebSocket control frame into a caller buffer.
|
||||
*
|
||||
* @note The opcode/FIN must already have been decoded by httpd_ws_get_frame_type().
|
||||
* On success @p frame describes the received (unmasked) control frame with
|
||||
* @p frame->payload pointing into @p buf.
|
||||
*
|
||||
* @param[in] req WebSocket request
|
||||
* @param[out] frame Frame descriptor to populate
|
||||
* @param[in] buf Caller-owned buffer of at least HTTPD_WS_CTRL_FRAME_BUF_LEN bytes
|
||||
* @param[in] max_len Maximum payload length to accept
|
||||
* @return
|
||||
* - ESP_OK : Frame received
|
||||
* - ESP_ERR_INVALID_STATE : Frame could not be fully received
|
||||
*/
|
||||
esp_err_t httpd_ws_recv_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame, uint8_t *buf, size_t max_len);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* @param[in] req WebSocket request
|
||||
* @param[in] frame Control frame previously received (modified in place)
|
||||
* @return
|
||||
* - ESP_OK : Reply sent (or none needed)
|
||||
* - others : Socket send failure
|
||||
*/
|
||||
esp_err_t httpd_ws_reply_to_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* @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_err_t httpd_ws_handle_control_frame(httpd_req_t *req);
|
||||
#endif /* CONFIG_HTTPD_WS_SUPPORT */
|
||||
|
||||
/**
|
||||
* @brief Trigger an httpd session close externally
|
||||
*
|
||||
|
||||
@@ -851,17 +851,21 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
|
||||
ESP_LOGD(TAG, LOG_FMT("Received PONG frame"));
|
||||
}
|
||||
|
||||
/* Call handler if it's a non-control frame, a PONG frame,
|
||||
* or if handler requests control frames as well.
|
||||
* PONG must be dispatched so that:
|
||||
* 1. User code that sends PINGs can track responses (heartbeat)
|
||||
* 2. The PONG frame bytes are consumed from the socket via
|
||||
* httpd_ws_recv_frame(), preventing TCP stream misalignment */
|
||||
if (ret == ESP_OK &&
|
||||
(ra->ws_type < HTTPD_WS_TYPE_CLOSE ||
|
||||
ra->ws_type == HTTPD_WS_TYPE_PONG ||
|
||||
sd->ws_control_frames)) {
|
||||
ret = sd->ws_handler(r);
|
||||
/* 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.
|
||||
* - 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
|
||||
* so its bytes are consumed from the socket (avoiding stream misalignment). */
|
||||
if (ret == ESP_OK) {
|
||||
if (ra->ws_type >= HTTPD_WS_TYPE_CLOSE && sd->ws_control_handler != NULL) {
|
||||
ret = httpd_ws_handle_control_frame(r);
|
||||
} else if (ra->ws_type < HTTPD_WS_TYPE_CLOSE ||
|
||||
ra->ws_type == HTTPD_WS_TYPE_PONG ||
|
||||
sd->ws_control_frames) {
|
||||
ret = sd->ws_handler(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
|
||||
@@ -176,6 +176,7 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle,
|
||||
#ifdef CONFIG_HTTPD_WS_SUPPORT
|
||||
hd->hd_calls[i]->is_websocket = uri_handler->is_websocket;
|
||||
hd->hd_calls[i]->handle_ws_control_frames = uri_handler->handle_ws_control_frames;
|
||||
hd->hd_calls[i]->ws_control_handler = uri_handler->ws_control_handler;
|
||||
if (uri_handler->supported_subprotocol) {
|
||||
hd->hd_calls[i]->supported_subprotocol = strdup(uri_handler->supported_subprotocol);
|
||||
if (hd->hd_calls[i]->supported_subprotocol == NULL) {
|
||||
@@ -353,6 +354,7 @@ esp_err_t httpd_uri(struct httpd_data *hd)
|
||||
aux->sd->ws_handshake_done = true;
|
||||
aux->sd->ws_handler = uri->handler;
|
||||
aux->sd->ws_control_frames = uri->handle_ws_control_frames;
|
||||
aux->sd->ws_control_handler = uri->handle_ws_control_frames ? uri->ws_control_handler : NULL;
|
||||
aux->sd->ws_user_ctx = uri->user_ctx;
|
||||
|
||||
#ifdef CONFIG_HTTPD_WS_POST_HANDSHAKE_CB_SUPPORT
|
||||
|
||||
@@ -689,6 +689,71 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_recv_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame,
|
||||
uint8_t *buf, size_t max_len)
|
||||
{
|
||||
/* The opcode/FIN were already decoded by httpd_ws_get_frame_type(); a zeroed
|
||||
* frame (len == 0) makes httpd_ws_recv_frame() read the remaining header and
|
||||
* payload. Control payloads are <= 125 bytes (RFC 6455 §5.5). */
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
frame->payload = buf;
|
||||
if (httpd_ws_recv_frame(req, frame, max_len) != ESP_OK) {
|
||||
ESP_LOGD(TAG, LOG_FMT("Cannot receive the full control frame"));
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_reply_to_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame)
|
||||
{
|
||||
switch (frame->type) {
|
||||
case HTTPD_WS_TYPE_PING:
|
||||
/* Reply to a PING with a PONG echoing the payload (RFC 6455 §5.5.2/5.5.3) */
|
||||
ESP_LOGD(TAG, LOG_FMT("Got a WS PING frame, Replying PONG..."));
|
||||
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) */
|
||||
ESP_LOGD(TAG, LOG_FMT("Got a WS CLOSE frame, Replying CLOSE..."));
|
||||
frame->len = 0;
|
||||
frame->payload = NULL;
|
||||
return httpd_ws_send_frame(req, frame);
|
||||
default:
|
||||
/* PONG and any other control frame require no reply */
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_handle_control_frame(httpd_req_t *req)
|
||||
{
|
||||
struct httpd_req_aux *aux = req->aux;
|
||||
if (aux == NULL || aux->sd == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
struct sock_db *sd = aux->sd;
|
||||
|
||||
/* The server receives the control-frame body itself. Oversized or malformed
|
||||
* frames are rejected by the max_len cap (zero-trust on client input). */
|
||||
httpd_ws_frame_t frame;
|
||||
uint8_t frame_buf[HTTPD_WS_CTRL_FRAME_BUF_LEN] = { 0 };
|
||||
esp_err_t ret = httpd_ws_recv_control_frame(req, &frame, frame_buf, HTTPD_WS_CTRL_FRAME_MAX_LEN);
|
||||
if (ret != ESP_OK) {
|
||||
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;
|
||||
}
|
||||
|
||||
esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
|
||||
{
|
||||
esp_err_t ret = httpd_ws_check_req(req);
|
||||
@@ -756,48 +821,21 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
|
||||
}
|
||||
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
|
||||
|
||||
/* If userspace requests control frames, do not deal with the control frames */
|
||||
/* If userspace requests control frames, do not deal with the control frames here */
|
||||
if (!sd->ws_control_frames) {
|
||||
ESP_LOGD(TAG, LOG_FMT("Handler not requests control frames"));
|
||||
|
||||
/* Reply to PING. For PONG and CLOSE, it will be handled elsewhere. */
|
||||
if (aux->ws_type == HTTPD_WS_TYPE_PING) {
|
||||
ESP_LOGD(TAG, LOG_FMT("Got a WS PING frame, Replying PONG..."));
|
||||
|
||||
/* Read the rest of the PING frame, for PONG to reply back. */
|
||||
/* Please refer to RFC6455 Section 5.5.2 for more details */
|
||||
/* Auto-handle PING and CLOSE: receive the body, then send the reply. PONG is
|
||||
* deliberately not consumed here; it is dispatched to the data handler elsewhere. */
|
||||
if (aux->ws_type == HTTPD_WS_TYPE_PING || aux->ws_type == HTTPD_WS_TYPE_CLOSE) {
|
||||
httpd_ws_frame_t frame;
|
||||
uint8_t frame_buf[128] = { 0 };
|
||||
memset(&frame, 0, sizeof(httpd_ws_frame_t));
|
||||
frame.payload = frame_buf;
|
||||
|
||||
if (httpd_ws_recv_frame(req, &frame, 126) != ESP_OK) {
|
||||
ESP_LOGD(TAG, LOG_FMT("Cannot receive the full PING frame"));
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
uint8_t frame_buf[HTTPD_WS_CTRL_FRAME_BUF_LEN] = { 0 };
|
||||
esp_err_t recv_frame_ret = httpd_ws_recv_control_frame(req, &frame, frame_buf,
|
||||
HTTPD_WS_CTRL_FRAME_MAX_LEN);
|
||||
if (recv_frame_ret != ESP_OK) {
|
||||
return recv_frame_ret;
|
||||
}
|
||||
|
||||
/* Now turn the frame to PONG */
|
||||
frame.type = HTTPD_WS_TYPE_PONG;
|
||||
return httpd_ws_send_frame(req, &frame);
|
||||
} else if (aux->ws_type == HTTPD_WS_TYPE_CLOSE) {
|
||||
ESP_LOGD(TAG, LOG_FMT("Got a WS CLOSE frame, Replying CLOSE..."));
|
||||
|
||||
/* Read the rest of the CLOSE frame and response */
|
||||
/* Please refer to RFC6455 Section 5.5.1 for more details */
|
||||
httpd_ws_frame_t frame;
|
||||
uint8_t frame_buf[128] = { 0 };
|
||||
memset(&frame, 0, sizeof(httpd_ws_frame_t));
|
||||
frame.payload = frame_buf;
|
||||
|
||||
if (httpd_ws_recv_frame(req, &frame, 126) != ESP_OK) {
|
||||
ESP_LOGD(TAG, LOG_FMT("Cannot receive the full CLOSE frame"));
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
frame.len = 0;
|
||||
frame.type = HTTPD_WS_TYPE_CLOSE;
|
||||
frame.payload = NULL;
|
||||
return httpd_ws_send_frame(req, &frame);
|
||||
return httpd_ws_reply_to_control_frame(req, &frame);
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
|
||||
Reference in New Issue
Block a user