Merge branch 'fix/esp_http_server-ws-frame-header-strictness_v6.0' into 'release/v6.0'

Fix(esp http server): ws frame header strictness (v6.0)

See merge request espressif/esp-idf!51857
This commit is contained in:
Mahavir Jain
2026-09-06 20:12:59 +05:30
4 changed files with 505 additions and 82 deletions
+2 -3
View File
@@ -60,9 +60,8 @@ menu "HTTP Server"
Sec-WebSocket-Key), rejects frames with reserved RSV bits, reserved or Sec-WebSocket-Key), rejects frames with reserved RSV bits, reserved or
fragmented control opcodes, non-minimal payload length encodings, and fragmented control opcodes, non-minimal payload length encodings, and
frames whose 64-bit length has the MSB set; sends a CLOSE frame on frames whose 64-bit length has the MSB set; sends a CLOSE frame on
protocol errors; validates CLOSE frame status codes and UTF-8 reason; protocol errors; and blocks outbound data frames once a CLOSE has been
validates UTF-8 text payloads; and blocks outbound data frames once a sent or received.
CLOSE has been sent or received.
This option defaults to off in this release cycle so existing deployments This option defaults to off in this release cycle so existing deployments
see no behavior change. Enable to opt into stricter enforcement; lenient see no behavior change. Enable to opt into stricter enforcement; lenient
@@ -1754,12 +1754,12 @@ 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 * @note Please refer to RFC6455 Section 5.4 for more details
*/ */
typedef enum { typedef enum {
HTTPD_WS_TYPE_CONTINUE = 0x0, HTTPD_WS_TYPE_CONTINUE = 0x0,
HTTPD_WS_TYPE_TEXT = 0x1, HTTPD_WS_TYPE_TEXT = 0x1,
HTTPD_WS_TYPE_BINARY = 0x2, HTTPD_WS_TYPE_BINARY = 0x2,
HTTPD_WS_TYPE_CLOSE = 0x8, HTTPD_WS_TYPE_CLOSE = 0x8,
HTTPD_WS_TYPE_PING = 0x9, HTTPD_WS_TYPE_PING = 0x9,
HTTPD_WS_TYPE_PONG = 0xA HTTPD_WS_TYPE_PONG = 0xA,
} httpd_ws_type_t; } httpd_ws_type_t;
/** /**
+167 -18
View File
@@ -47,10 +47,17 @@ static const char *TAG="httpd_ws";
*/ */
#define HTTPD_WS_CONTINUE 0x00U #define HTTPD_WS_CONTINUE 0x00U
#define HTTPD_WS_FIN_BIT 0x80U #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_OPCODE_BITS 0x0fU
#define HTTPD_WS_MASK_BIT 0x80U #define HTTPD_WS_MASK_BIT 0x80U
#define HTTPD_WS_LENGTH_BITS 0x7fU #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_TOO_BIG 1009U
/* /*
* The magic GUID string used for handshake * The magic GUID string used for handshake
* Please refer to RFC6455 Section 1.3 for more details. * Please refer to RFC6455 Section 1.3 for more details.
@@ -345,6 +352,66 @@ static esp_err_t httpd_ws_check_req(httpd_req_t *req)
return ESP_OK; return ESP_OK;
} }
/* RFC 6455 §5.5: opcodes 0x8-0xA (CLOSE, PING, PONG) are control frames.
* Reserved control opcodes 0xB-0xF are rejected earlier in httpd_ws_get_frame_type,
* so an explicit whitelist is used here instead of a `>= CLOSE` range compare. */
static inline bool httpd_ws_is_control_opcode(httpd_ws_type_t type)
{
return type == HTTPD_WS_TYPE_CLOSE ||
type == HTTPD_WS_TYPE_PING ||
type == HTTPD_WS_TYPE_PONG;
}
/* Mark the session's pending frame as a CLOSE so the request layer tears the
* socket down after the current request completes. */
static inline void httpd_ws_mark_closing(struct httpd_req_aux *aux)
{
aux->ws_final = true;
aux->ws_type = HTTPD_WS_TYPE_CLOSE;
}
/* 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)
{
esp_err_t ret = ESP_FAIL;
if (!req || !req->aux) {
return ret;
}
struct httpd_req_aux *aux = req->aux;
if (!aux->sd) {
return ret;
}
httpd_ws_mark_closing(aux);
bool already_closing = aux->sd->ws_close;
aux->sd->ws_close = true;
if (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 ret;
}
static esp_err_t httpd_ws_unmask_payload(uint8_t *payload, size_t len, const uint8_t *mask_key) static esp_err_t httpd_ws_unmask_payload(uint8_t *payload, size_t len, const uint8_t *mask_key)
{ {
if (len < 1 || !payload) { if (len < 1 || !payload) {
@@ -396,6 +463,17 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *frame, size_t
/* Interpret length */ /* Interpret length */
uint8_t init_len = second_byte & HTTPD_WS_LENGTH_BITS; uint8_t init_len = second_byte & HTTPD_WS_LENGTH_BITS;
#if CONFIG_HTTPD_WS_STRICTER_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 (httpd_ws_is_control_opcode(frame->type) && 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);
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
if (init_len < 126) { if (init_len < 126) {
/* Case 1: If length is 0-125, then this length bit is 7 bits */ /* Case 1: If length is 0-125, then this length bit is 7 bits */
frame->len = init_len; frame->len = init_len;
@@ -408,7 +486,15 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *frame, size_t
return ESP_FAIL; return ESP_FAIL;
} }
frame->len = ((uint32_t)(length_bytes[0] << 8U) | (length_bytes[1])); uint16_t length = ((uint16_t)(length_bytes[0] << 8U) | (length_bytes[1]));
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
/* RFC 6455 §5.2: encodings must be minimal; a value < 126 MUST use 7-bit form. */
if (length < 126) {
ESP_LOGE(TAG, LOG_FMT("Invalid WS frame length: non-minimal 16-bit encoding"));
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
frame->len = length;
} else if (init_len == 127) { } else if (init_len == 127) {
/* Case 3: If length is byte 127, then this frame's length bit is 64 bits */ /* Case 3: If length is byte 127, then this frame's length bit is 64 bits */
uint8_t length_bytes[8] = { 0 }; uint8_t length_bytes[8] = { 0 };
@@ -418,14 +504,34 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *frame, size_t
return ESP_FAIL; return ESP_FAIL;
} }
frame->len = (((uint64_t)length_bytes[0] << 56U) | #if CONFIG_HTTPD_WS_STRICTER_RFC6455
((uint64_t)length_bytes[1] << 48U) | /* RFC 6455 §5.2: MSB must be 0 */
((uint64_t)length_bytes[2] << 40U) | if (length_bytes[0] & 0x80) {
((uint64_t)length_bytes[3] << 32U) | ESP_LOGE(TAG, LOG_FMT("Invalid WS frame length: MSB must be 0"));
((uint64_t)length_bytes[4] << 24U) | return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
((uint64_t)length_bytes[5] << 16U) | }
((uint64_t)length_bytes[6] << 8U) | #endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
((uint64_t)length_bytes[7]));
uint64_t length = (uint64_t)length_bytes[0] << 56U |
(uint64_t)length_bytes[1] << 48U |
(uint64_t)length_bytes[2] << 40U |
(uint64_t)length_bytes[3] << 32U |
(uint64_t)length_bytes[4] << 24U |
(uint64_t)length_bytes[5] << 16U |
(uint64_t)length_bytes[6] << 8U |
(uint64_t)length_bytes[7];
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
/* Encoding must be minimal; values <= UINT16_MAX MUST use 16-bit form */
if (length <= UINT16_MAX) {
ESP_LOGE(TAG, LOG_FMT("Invalid WS frame length: non-minimal 64-bit encoding"));
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
if (length > SIZE_MAX) {
ESP_LOGE(TAG, LOG_FMT("Invalid WS frame length: too large for platform"));
return httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_TOO_BIG);
}
frame->len = (size_t)length;
} }
/* If this frame is masked, dump the mask as well */ /* If this frame is masked, dump the mask as well */
if (masked) { if (masked) {
@@ -437,7 +543,10 @@ esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *frame, size_t
} else { } else {
/* If the WS frame from client to server is not masked, it should be rejected. /* 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. */ * 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_STRICTER_RFC6455
httpd_ws_fail_connection(req, HTTPD_WS_CLOSE_CODE_PROTOCOL_ERROR);
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
} }
@@ -500,6 +609,22 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
struct sock_db *sess = httpd_sess_get(hd, fd);
if (!sess) {
return ESP_ERR_INVALID_ARG;
}
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
/* RFC 6455 §1.4 / §5.5.1: once a CLOSE has been sent or received the
* endpoint MUST NOT transmit any further data frames. Only the CLOSE
* frame itself is permitted so that an in-progress fail-the-connection
* or close-handshake response can still be emitted. */
if (sess->ws_close && frame->type != HTTPD_WS_TYPE_CLOSE) {
ESP_LOGW(TAG, LOG_FMT("Session is closing; refusing non-CLOSE frame (type=0x%02X)"), frame->type);
return ESP_ERR_INVALID_STATE;
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
/* Prepare Tx buffer - maximum length is 14, which includes 2 bytes header, 8 bytes length, 4 bytes mask key */ /* Prepare Tx buffer - maximum length is 14, which includes 2 bytes header, 8 bytes length, 4 bytes mask key */
uint8_t tx_len = 0; uint8_t tx_len = 0;
uint8_t header_buf[10] = {0 }; uint8_t header_buf[10] = {0 };
@@ -510,7 +635,7 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t
if (frame->len <= 125) { if (frame->len <= 125) {
header_buf[1] = frame->len & 0x7fU; /* Length for 7 bits */ header_buf[1] = frame->len & 0x7fU; /* Length for 7 bits */
tx_len = 2; tx_len = 2;
} else if (frame->len > 125 && frame->len < UINT16_MAX) { } else if (frame->len > 125 && frame->len <= UINT16_MAX) {
header_buf[1] = 126; /* Length for 16 bits */ header_buf[1] = 126; /* Length for 16 bits */
header_buf[2] = (frame->len >> 8U) & 0xffU; header_buf[2] = (frame->len >> 8U) & 0xffU;
header_buf[3] = frame->len & 0xffU; header_buf[3] = frame->len & 0xffU;
@@ -531,11 +656,6 @@ 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. */ /* WebSocket server does not required to mask response payload, so leave the MASK bit as 0. */
header_buf[1] &= (~HTTPD_WS_MASK_BIT); header_buf[1] &= (~HTTPD_WS_MASK_BIT);
struct sock_db *sess = httpd_sess_get(hd, fd);
if (!sess) {
return ESP_ERR_INVALID_ARG;
}
/* Send off header */ /* Send off header */
if (sess->send_fn(hd, fd, (const char *)header_buf, tx_len, 0) < 0) { if (sess->send_fn(hd, fd, (const char *)header_buf, tx_len, 0) < 0) {
ESP_LOGW(TAG, LOG_FMT("Failed to send WS header")); ESP_LOGW(TAG, LOG_FMT("Failed to send WS header"));
@@ -580,8 +700,7 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
/* If we fail to read exactly one byte, this socket FD is invalid or the frame header is incomplete. */ /* If we fail to read exactly one byte, this socket FD is invalid or the frame header is incomplete. */
/* Here we mark it as a Close message and close it later. */ /* Here we mark it as a Close message and close it later. */
ESP_LOGW(TAG, LOG_FMT("Failed to read header byte (socket FD invalid), closing socket now")); ESP_LOGW(TAG, LOG_FMT("Failed to read header byte (socket FD invalid), closing socket now"));
aux->ws_final = true; httpd_ws_mark_closing(aux);
aux->ws_type = HTTPD_WS_TYPE_CLOSE;
return ESP_OK; return ESP_OK;
} }
@@ -591,6 +710,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_final = (first_byte & HTTPD_WS_FIN_BIT) != 0;
aux->ws_type = (first_byte & HTTPD_WS_OPCODE_BITS); aux->ws_type = (first_byte & HTTPD_WS_OPCODE_BITS);
#if CONFIG_HTTPD_WS_STRICTER_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);
}
/* 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);
}
/* RFC 6455 §5.5: control frames MUST NOT be fragmented (FIN bit must be set). */
if (httpd_ws_is_control_opcode(aux->ws_type) && !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);
}
#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 */
if (!sd->ws_control_frames) { if (!sd->ws_control_frames) {
ESP_LOGD(TAG, LOG_FMT("Handler not requests control frames")); ESP_LOGD(TAG, LOG_FMT("Handler not requests control frames"));
@@ -32,6 +32,17 @@ esp_err_t null_func(httpd_req_t *req)
return ESP_OK; return ESP_OK;
} }
httpd_uri_t handler_limit_uri (char* path)
{
httpd_uri_t uri = {
.uri = path,
.method = HTTP_GET,
.handler = null_func,
.user_ctx = NULL,
};
return uri;
};
#ifdef CONFIG_HTTPD_WS_SUPPORT #ifdef CONFIG_HTTPD_WS_SUPPORT
static httpd_handle_t start_test_ws_server(uint16_t server_port, uint16_t ctrl_port) static httpd_handle_t start_test_ws_server(uint16_t server_port, uint16_t ctrl_port)
{ {
@@ -54,20 +65,7 @@ static httpd_handle_t start_test_ws_server(uint16_t server_port, uint16_t ctrl_p
TEST_ASSERT_EQUAL(ESP_OK, httpd_register_uri_handler(hd, &ws_uri)); TEST_ASSERT_EQUAL(ESP_OK, httpd_register_uri_handler(hd, &ws_uri));
return hd; return hd;
} }
#endif /* CONFIG_HTTPD_WS_SUPPORT */
httpd_uri_t handler_limit_uri (char* path)
{
httpd_uri_t uri = {
.uri = path,
.method = HTTP_GET,
.handler = null_func,
.user_ctx = NULL,
};
return uri;
};
#ifdef CONFIG_HTTPD_WS_SUPPORT
static httpd_uri_t handler_limit_ws_uri(char *path, const char *subprotocol) static httpd_uri_t handler_limit_ws_uri(char *path, const char *subprotocol)
{ {
httpd_uri_t uri = { httpd_uri_t uri = {
@@ -83,6 +81,20 @@ static httpd_uri_t handler_limit_ws_uri(char *path, const char *subprotocol)
static int ws_recv_fail_handler_calls; 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) static int ws_recv_fail_override(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags)
{ {
(void)hd; (void)hd;
@@ -93,6 +105,38 @@ static int ws_recv_fail_override(httpd_handle_t hd, int sockfd, char *buf, size_
return HTTPD_SOCK_ERR_FAIL; 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) static esp_err_t ws_counting_handler(httpd_req_t *req)
{ {
(void)req; (void)req;
@@ -472,28 +516,6 @@ TEST_CASE("WS recv failure marks close without dispatching handler", "[HTTP SERV
free(hd.hd_req_aux.resp_hdrs); free(hd.hd_req_aux.resp_hdrs);
} }
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
TEST_CASE("WS HTTP/1.0 upgrade request returns 400", "[HTTP SERVER][websocket]")
{
test_case_uses_tcpip();
httpd_handle_t hd = start_test_ws_server(8091, ESP_HTTPD_DEF_CTRL_PORT + 10);
mock_server_request_t req = {
.data = "GET /ws HTTP/1.0\r\n"
"Host: localhost\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n",
};
mock_server_response_t *resp = mock_server_send_request(8091, &req);
TEST_ASSERT_NOT_NULL(resp);
mock_server_assert_status(resp, 400);
mock_server_response_free(resp);
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
/* Regression test: enabling CONFIG_HTTPD_WS_SUPPORT must not reject HTTP/1.0 /* Regression test: enabling CONFIG_HTTPD_WS_SUPPORT must not reject HTTP/1.0
* traffic on non-WS endpoints. The HTTP/1.1 requirement only applies once * traffic on non-WS endpoints. The HTTP/1.1 requirement only applies once
* an "Upgrade: websocket" header confirms the request is a WS handshake. */ * an "Upgrade: websocket" header confirms the request is a WS handshake. */
@@ -515,26 +537,6 @@ TEST_CASE("Non-WS HTTP/1.0 request is not rejected by WS version check", "[HTTP
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd)); TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
} }
#if CONFIG_HTTPD_WS_STRICTER_RFC6455
TEST_CASE("WS handshake missing Host returns 400", "[HTTP SERVER][websocket]")
{
test_case_uses_tcpip();
httpd_handle_t hd = start_test_ws_server(8090, ESP_HTTPD_DEF_CTRL_PORT + 9);
mock_server_request_t req = {
.data = "GET /ws HTTP/1.1\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n",
};
mock_server_response_t *resp = mock_server_send_request(8090, &req);
TEST_ASSERT_NOT_NULL(resp);
mock_server_assert_status(resp, 400);
mock_server_response_free(resp);
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
TEST_CASE("WS handshake missing Sec-WebSocket-Version returns 400", "[HTTP SERVER][websocket]") TEST_CASE("WS handshake missing Sec-WebSocket-Version returns 400", "[HTTP SERVER][websocket]")
{ {
test_case_uses_tcpip(); test_case_uses_tcpip();
@@ -574,6 +576,44 @@ TEST_CASE("WS handshake unsupported version returns 426 with Sec-WebSocket-Versi
} }
#if CONFIG_HTTPD_WS_STRICTER_RFC6455 #if CONFIG_HTTPD_WS_STRICTER_RFC6455
TEST_CASE("WS HTTP/1.0 upgrade request returns 400", "[HTTP SERVER][websocket]")
{
test_case_uses_tcpip();
httpd_handle_t hd = start_test_ws_server(8091, ESP_HTTPD_DEF_CTRL_PORT + 10);
mock_server_request_t req = {
.data = "GET /ws HTTP/1.0\r\n"
"Host: localhost\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n",
};
mock_server_response_t *resp = mock_server_send_request(8091, &req);
TEST_ASSERT_NOT_NULL(resp);
mock_server_assert_status(resp, 400);
mock_server_response_free(resp);
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
}
TEST_CASE("WS handshake missing Host returns 400", "[HTTP SERVER][websocket]")
{
test_case_uses_tcpip();
httpd_handle_t hd = start_test_ws_server(8090, ESP_HTTPD_DEF_CTRL_PORT + 9);
mock_server_request_t req = {
.data = "GET /ws HTTP/1.1\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n",
};
mock_server_response_t *resp = mock_server_send_request(8090, &req);
TEST_ASSERT_NOT_NULL(resp);
mock_server_assert_status(resp, 400);
mock_server_response_free(resp);
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
}
TEST_CASE("WS handshake invalid Sec-WebSocket-Key returns 400", "[HTTP SERVER][websocket]") TEST_CASE("WS handshake invalid Sec-WebSocket-Key returns 400", "[HTTP SERVER][websocket]")
{ {
test_case_uses_tcpip(); test_case_uses_tcpip();
@@ -592,8 +632,243 @@ TEST_CASE("WS handshake invalid Sec-WebSocket-Key returns 400", "[HTTP SERVER][w
mock_server_response_free(resp); mock_server_response_free(resp);
TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd)); TEST_ASSERT_EQUAL(ESP_OK, httpd_stop(hd));
} }
/* Common fake-session wiring for the strict recv-path tests: a single-socket
* server whose scripted recv feeds `frame` and whose send is captured. */
static void ws_setup_recv_fixture(struct httpd_data *hd, httpd_req_t *req,
struct httpd_req_aux *aux, struct sock_db *session,
const uint8_t *frame, size_t frame_len)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
ws_scripted_recv_ctx = (ws_scripted_recv_ctx_t){ .data = frame, .len = frame_len };
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;
}
/* 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)
{
static const uint8_t expected_reply[] = { 0x88, 0x02, 0x03, 0xEA };
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 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 */
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_FAIL, httpd_ws_get_frame_type(&req));
ws_assert_close_1002_sent(&session);
}
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) */
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_FAIL, httpd_ws_get_frame_type(&req));
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
ws_assert_close_1002_sent(&session);
}
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) */
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_FAIL, httpd_ws_get_frame_type(&req));
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
ws_assert_close_1002_sent(&session);
}
TEST_CASE("WS recv fragmented control frame sends CLOSE 1002", "[HTTP SERVER][websocket]")
{
static const uint8_t ws_frame[] = { 0x09 }; /* FIN=0, opcode=PING */
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_FAIL, httpd_ws_get_frame_type(&req));
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
ws_assert_close_1002_sent(&session);
}
TEST_CASE("WS recv unmasked frame sends CLOSE 1002 and marks close", "[HTTP SERVER][websocket]")
{
/* Length byte 0x02: MASK=0, payload len=2. recv_frame reads this as its
* first byte (the opcode byte is taken from aux), so it exercises the
* unmasked-client-frame rejection rather than the mask-key path. */
static const uint8_t ws_frame[] = { 0x02 };
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_ERR_INVALID_STATE, httpd_ws_recv_frame(&req, &frame, 0));
ws_assert_close_1002_sent(&session);
}
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 };
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));
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);
ws_assert_close_1002_sent(&session);
}
TEST_CASE("WS recv rejects non-minimal 16-bit payload length encoding", "[HTTP SERVER][websocket]")
{
/* 0xFE = MASK=1 len=126; 0x00 0x7D = 125, which must use 7-bit form */
static const uint8_t ws_frame[] = { 0xFE, 0x00, 0x7D };
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_FAIL, httpd_ws_recv_frame(&req, &frame, 0));
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
ws_assert_close_1002_sent(&session);
}
TEST_CASE("WS recv rejects non-minimal 64-bit payload length encoding", "[HTTP SERVER][websocket]")
{
/* 0xFF = MASK=1 len=127; value 65535 must use 16-bit form */
static const uint8_t ws_frame[] = { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF };
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_FAIL, httpd_ws_recv_frame(&req, &frame, 0));
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, aux.ws_type);
ws_assert_close_1002_sent(&session);
}
TEST_CASE("WS send refuses non-CLOSE frame once session is closing", "[HTTP SERVER][websocket]")
{
/* RFC 6455 §5.5.1: after a CLOSE is sent/received, only a CLOSE may follow. */
static const uint8_t payload[] = { 0x41, 0x42 };
static const uint8_t expected_close[] = { 0x88, 0x00 }; /* CLOSE, zero-length */
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
struct httpd_data hd = {0};
struct sock_db session = {0};
httpd_ws_frame_t data_frame = {
.type = HTTPD_WS_TYPE_TEXT,
.payload = (uint8_t *)payload,
.len = sizeof(payload),
};
httpd_ws_frame_t close_frame = {
.type = HTTPD_WS_TYPE_CLOSE,
.payload = NULL,
.len = 0,
};
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;
session.ws_close = true; /* a CLOSE has already been sent/received */
/* A data frame must be refused and nothing may go on the wire. */
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, httpd_ws_send_frame_async(&hd, session.fd, &data_frame));
TEST_ASSERT_EQUAL(0, ws_send_capture_ctx.len);
/* The CLOSE frame itself is still permitted. */
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_send_frame_async(&hd, session.fd, &close_frame));
TEST_ASSERT_EQUAL(sizeof(expected_close), ws_send_capture_ctx.len);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_close, ws_send_capture_ctx.data, sizeof(expected_close));
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */ #endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload", "[HTTP SERVER][websocket]")
{
static const uint8_t expected_header[] = { 0x82, 0x7E, 0xFF, 0xFF };
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
struct httpd_data hd = {0};
struct sock_db session = {0};
httpd_ws_frame_t frame = {
.type = HTTPD_WS_TYPE_BINARY,
.payload = NULL,
.len = UINT16_MAX,
};
memset(&ws_send_capture_ctx, 0, sizeof(ws_send_capture_ctx));
hd.config = config;
hd.config.max_open_sockets = 1;
hd.hd_sd = &session;
session.fd = 123;
session.handle = (httpd_handle_t)&hd;
session.send_fn = ws_scripted_send_override;
TEST_ASSERT_EQUAL(ESP_OK, httpd_ws_send_frame_async(&hd, session.fd, &frame));
TEST_ASSERT_EQUAL(sizeof(expected_header), ws_send_capture_ctx.len);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_header, ws_send_capture_ctx.data, sizeof(expected_header));
}
#endif /* CONFIG_HTTPD_WS_SUPPORT */ #endif /* CONFIG_HTTPD_WS_SUPPORT */
/********* URL query / header pointer-accessor tests ********* /********* URL query / header pointer-accessor tests *********