diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 9ed1db4fc7f..1a5ed4aeba2 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -1786,12 +1786,16 @@ esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *ar * @note Please refer to RFC6455 Section 5.4 for more details */ typedef enum { - HTTPD_WS_TYPE_CONTINUE = 0x0, - HTTPD_WS_TYPE_TEXT = 0x1, - HTTPD_WS_TYPE_BINARY = 0x2, - HTTPD_WS_TYPE_CLOSE = 0x8, - HTTPD_WS_TYPE_PING = 0x9, - HTTPD_WS_TYPE_PONG = 0xA + HTTPD_WS_TYPE_CONTINUE = 0x0, + HTTPD_WS_TYPE_TEXT = 0x1, + HTTPD_WS_TYPE_BINARY = 0x2, + HTTPD_WS_TYPE_NON_CTRL_RES = 0x3, /*!< Reserved non-control opcode range start */ + HTTPD_WS_TYPE_NON_CTRL_RES_END = 0x7, /*!< Reserved non-control opcode range end */ + HTTPD_WS_TYPE_CLOSE = 0x8, + HTTPD_WS_TYPE_PING = 0x9, + HTTPD_WS_TYPE_PONG = 0xA, + HTTPD_WS_TYPE_CTRL_RES = 0xB, /*!< Reserved control opcode range start */ + HTTPD_WS_TYPE_CTRL_RES_END = 0xF, /*!< Reserved control opcode range end */ } httpd_ws_type_t; /** diff --git a/components/esp_http_server/src/httpd_ws.c b/components/esp_http_server/src/httpd_ws.c index 13cc94c464e..e08b36e3264 100644 --- a/components/esp_http_server/src/httpd_ws.c +++ b/components/esp_http_server/src/httpd_ws.c @@ -47,10 +47,18 @@ static const char *TAG="httpd_ws"; */ #define HTTPD_WS_CONTINUE 0x00U #define HTTPD_WS_FIN_BIT 0x80U +#define HTTPD_WS_RSV1_BIT 0x40U +#define HTTPD_WS_RSV2_BIT 0x20U +#define HTTPD_WS_RSV3_BIT 0x10U #define HTTPD_WS_OPCODE_BITS 0x0fU #define HTTPD_WS_MASK_BIT 0x80U #define HTTPD_WS_LENGTH_BITS 0x7fU +/* 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 + /* * The magic GUID string used for handshake * Please refer to RFC6455 Section 1.3 for more details. @@ -345,6 +353,48 @@ static esp_err_t httpd_ws_check_req(httpd_req_t *req) return ESP_OK; } +/* 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 + */ +static esp_err_t httpd_ws_fail_connection(httpd_req_t *req, uint16_t close_code, bool send_close) +{ + if (!req || !req->aux) { + return ESP_FAIL; + } + + struct httpd_req_aux *aux = req->aux; + if (!aux->sd) { + return ESP_FAIL; + } + + aux->ws_final = true; + aux->ws_type = HTTPD_WS_TYPE_CLOSE; + + bool already_closing = aux->sd->ws_close; + aux->sd->ws_close = true; + + if (send_close && aux->sd->ws_handshake_done && !already_closing) { + uint8_t close_payload[2] = { + (uint8_t)(close_code >> 8U), + (uint8_t)(close_code & 0xffU), + }; + httpd_ws_frame_t close_frame = { + .final = true, + .fragmented = false, + .type = HTTPD_WS_TYPE_CLOSE, + .payload = close_payload, + .len = sizeof(close_payload), + }; + esp_err_t ret = httpd_ws_send_frame(req, &close_frame); + if (ret != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("Failed to send CLOSE frame with code %u"), close_code); + } + } + + return ESP_FAIL; +} + 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) { @@ -396,6 +446,17 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t /* Interpret length */ uint8_t init_len = second_byte & HTTPD_WS_LENGTH_BITS; + +#if CONFIG_HTTPD_WS_STRICT_RFC6455 + /* RFC 6455 §5.5: control frames MUST have payload <= 125 bytes and + * therefore MUST NOT use the 16- or 64-bit extended length encodings. + */ + if (frame->type >= HTTPD_WS_TYPE_CLOSE && init_len > 125) { + ESP_LOGE(TAG, LOG_FMT("Invalid control frame length encoding")); + return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR, true); + } +#endif /* CONFIG_HTTPD_WS_STRICT_RFC6455 */ + if (init_len < 126) { /* Case 1: If length is 0-125, then this length bit is 7 bits */ frame->len = init_len; @@ -439,7 +500,10 @@ static esp_err_t httpd_ws_recv_frame_internal(httpd_req_t *req, httpd_ws_frame_t } else { /* If the WS frame from client to server is not masked, it should be rejected. * Please refer to RFC6455 Section 5.2 for more details. */ - ESP_LOGW(TAG, LOG_FMT("WS frame is not properly masked.")); + ESP_LOGE(TAG, LOG_FMT("WS frame is not properly masked.")); +#if CONFIG_HTTPD_WS_STRICT_RFC6455 + httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR, true); +#endif /* CONFIG_HTTPD_WS_STRICT_RFC6455 */ return ESP_ERR_INVALID_STATE; } } @@ -607,6 +671,36 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req) aux->ws_final = (first_byte & HTTPD_WS_FIN_BIT) != 0; aux->ws_type = (first_byte & HTTPD_WS_OPCODE_BITS); +#if CONFIG_HTTPD_WS_STRICT_RFC6455 + /* RFC 6455 §5.2: RSV bits MUST be 0 unless an extension has been negotiated. + * This implementation does not support extensions, so any set RSV bit is an error. + */ + if (first_byte & (HTTPD_WS_RSV1_BIT | HTTPD_WS_RSV2_BIT | HTTPD_WS_RSV3_BIT)) { + ESP_LOGE(TAG, LOG_FMT("RSV1, RSV2 or RSV3 bits are set, closing connection")); + return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR, true); + } + + /* RFC 6455 §5.2: opcodes 0x3–0x7 and 0xB–0xF are reserved and must not be used. */ + switch (aux->ws_type) { + case HTTPD_WS_TYPE_CONTINUE: + case HTTPD_WS_TYPE_TEXT: + case HTTPD_WS_TYPE_BINARY: + case HTTPD_WS_TYPE_CLOSE: + case HTTPD_WS_TYPE_PING: + case HTTPD_WS_TYPE_PONG: + break; + default: + ESP_LOGE(TAG, LOG_FMT("Invalid WS frame type: 0x%02X"), aux->ws_type); + return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR, true); + } + + /* RFC 6455 §5.5: control frames MUST NOT be fragmented (FIN bit must be set). */ + if (aux->ws_type >= HTTPD_WS_TYPE_CLOSE && !aux->ws_final) { + ESP_LOGE(TAG, LOG_FMT("Invalid fragmented control frame: 0x%02X"), aux->ws_type); + return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR, true); + } +#endif /* CONFIG_HTTPD_WS_STRICT_RFC6455 */ + /* If userspace requests control frames, do not deal with the control frames */ if (!sd->ws_control_frames) { ESP_LOGD(TAG, LOG_FMT("Handler not requests control frames")); diff --git a/components/esp_http_server/test_apps/main/test_http_server.c b/components/esp_http_server/test_apps/main/test_http_server.c index f595842c512..885f51d9068 100644 --- a/components/esp_http_server/test_apps/main/test_http_server.c +++ b/components/esp_http_server/test_apps/main/test_http_server.c @@ -85,6 +85,20 @@ static httpd_uri_t handler_limit_ws_uri(char *path, const char *subprotocol) static int ws_recv_fail_handler_calls; +typedef struct { + const uint8_t *data; + size_t len; + size_t offset; +} ws_scripted_recv_ctx_t; + +typedef struct { + uint8_t data[32]; + size_t len; +} ws_send_capture_ctx_t; + +static ws_scripted_recv_ctx_t ws_scripted_recv_ctx; +static ws_send_capture_ctx_t ws_send_capture_ctx; + static int ws_recv_fail_override(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags) { (void)hd; @@ -95,6 +109,38 @@ static int ws_recv_fail_override(httpd_handle_t hd, int sockfd, char *buf, size_ 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; + (void)sockfd; + (void)flags; + + size_t remaining = ws_scripted_recv_ctx.len - ws_scripted_recv_ctx.offset; + if (remaining == 0) { + return HTTPD_SOCK_ERR_FAIL; + } + + size_t to_copy = remaining < buf_len ? remaining : buf_len; + memcpy(buf, ws_scripted_recv_ctx.data + ws_scripted_recv_ctx.offset, to_copy); + ws_scripted_recv_ctx.offset += to_copy; + return (int)to_copy; +} + +static int ws_scripted_send_override(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) +{ + (void)hd; + (void)sockfd; + (void)flags; + + if (ws_send_capture_ctx.len + buf_len > sizeof(ws_send_capture_ctx.data)) { + return HTTPD_SOCK_ERR_FAIL; + } + + memcpy(ws_send_capture_ctx.data + ws_send_capture_ctx.len, buf, buf_len); + ws_send_capture_ctx.len += buf_len; + return (int)buf_len; +} + static esp_err_t ws_counting_handler(httpd_req_t *req) { (void)req; @@ -669,6 +715,159 @@ TEST_CASE("WS handshake invalid Sec-WebSocket-Key returns 400", "[HTTP SERVER][w } #endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */ +#if CONFIG_HTTPD_WS_STRICT_RFC6455 +TEST_CASE("WS recv RSV bit set sends CLOSE 1002 and marks close", "[HTTP SERVER][websocket]") +{ + static const uint8_t ws_frame[] = { 0xC1 }; /* RSV1=1, FIN=1, opcode TEXT */ + static const uint8_t expected_reply[] = { 0x88, 0x02, 0x03, 0xEA }; + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + struct httpd_data hd = {0}; + httpd_req_t req = {0}; + struct httpd_req_aux aux = {0}; + struct sock_db session = {0}; + + ws_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = ws_frame, .len = sizeof(ws_frame) }; + memset(&ws_send_capture_ctx, 0, sizeof(ws_send_capture_ctx)); + + hd.config = config; + hd.config.max_open_sockets = 1; + hd.hd_sd = &session; + req.handle = &hd; + req.aux = &aux; + aux.sd = &session; + session.fd = 123; + session.handle = (httpd_handle_t)&hd; + session.recv_fn = ws_scripted_recv_override; + session.send_fn = ws_scripted_send_override; + session.ws_handshake_done = true; + + TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req)); + TEST_ASSERT_TRUE(session.ws_close); + 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 reserved non-control opcode sends CLOSE 1002", "[HTTP SERVER][websocket]") +{ + static const uint8_t ws_frame[] = { 0x83 }; /* FIN=1, opcode=0x3 (reserved) */ + + httpd_req_t req = {0}; + struct httpd_req_aux aux = {0}; + struct sock_db session = {0}; + + ws_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = ws_frame, .len = sizeof(ws_frame) }; + + req.aux = &aux; + aux.sd = &session; + session.fd = 123; + session.recv_fn = ws_scripted_recv_override; + session.ws_handshake_done = true; + + TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req)); + TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type); +} + +TEST_CASE("WS recv reserved control opcode sends CLOSE 1002", "[HTTP SERVER][websocket]") +{ + static const uint8_t ws_frame[] = { 0x8B }; /* FIN=1, opcode=0xB (reserved) */ + + httpd_req_t req = {0}; + struct httpd_req_aux aux = {0}; + struct sock_db session = {0}; + + ws_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = ws_frame, .len = sizeof(ws_frame) }; + + req.aux = &aux; + aux.sd = &session; + session.fd = 123; + session.recv_fn = ws_scripted_recv_override; + session.ws_handshake_done = true; + + TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req)); + TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type); +} + +TEST_CASE("WS recv fragmented control frame sends CLOSE 1002", "[HTTP SERVER][websocket]") +{ + static const uint8_t ws_frame[] = { 0x09 }; /* FIN=0, opcode=PING */ + + httpd_req_t req = {0}; + struct httpd_req_aux aux = {0}; + struct sock_db session = {0}; + + ws_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = ws_frame, .len = sizeof(ws_frame) }; + + req.aux = &aux; + aux.sd = &session; + session.fd = 123; + session.recv_fn = ws_scripted_recv_override; + session.ws_handshake_done = true; + + TEST_ASSERT_EQUAL(ESP_FAIL, httpd_ws_get_frame_type(&req)); + TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type); +} +#endif /* CONFIG_HTTPD_WS_STRICT_RFC6455 */ + +#if CONFIG_HTTPD_WS_STRICT_RFC6455 +TEST_CASE("WS recv unmasked frame sends CLOSE 1002 and marks close", "[HTTP SERVER][websocket]") +{ + /* Second byte 0x02: MASK=0, payload len=2 */ + static const uint8_t ws_frame[] = { 0x82, 0x02 }; + static const uint8_t expected_reply[] = { 0x88, 0x02, 0x03, 0xEA }; + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + 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_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = ws_frame, .len = sizeof(ws_frame) }; + memset(&ws_send_capture_ctx, 0, sizeof(ws_send_capture_ctx)); + + hd.config = config; + hd.config.max_open_sockets = 1; + hd.hd_sd = &session; + req.handle = &hd; + req.aux = &aux; + aux.sd = &session; + aux.ws_type = HTTPD_WS_TYPE_BINARY; + aux.ws_final = true; + session.fd = 123; + session.handle = (httpd_handle_t)&hd; + session.recv_fn = ws_scripted_recv_override; + session.send_fn = ws_scripted_send_override; + session.ws_handshake_done = true; + + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_recv_frame(&req, &frame, 0)); + TEST_ASSERT_TRUE(session.ws_close); + 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 control frame with payload > 125 sends CLOSE 1002", "[HTTP SERVER][websocket]") +{ + /* PING (0x89), MASK=1 (0x80), length=126 (0x7E) — invalid extended length for control */ + static const uint8_t ws_frame[] = { 0x89, 0xFE }; + + httpd_req_t req = {0}; + struct httpd_req_aux aux = {0}; + struct sock_db session = {0}; + + ws_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = ws_frame, .len = sizeof(ws_frame) }; + + req.aux = &aux; + aux.sd = &session; + session.fd = 123; + session.recv_fn = ws_scripted_recv_override; + session.ws_handshake_done = true; + session.ws_control_frames = false; + + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_get_frame_type(&req)); + TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type); +} +#endif /* CONFIG_HTTPD_WS_STRICT_RFC6455 */ #endif /* CONFIG_HTTPD_WS_SUPPORT */ /********* URL query / header pointer-accessor tests *********