diff --git a/wish/cpp/CMakeLists.txt b/wish/cpp/CMakeLists.txt index ca49a14..372d38c 100644 --- a/wish/cpp/CMakeLists.txt +++ b/wish/cpp/CMakeLists.txt @@ -24,6 +24,7 @@ FetchContent_Declare( wslay GIT_REPOSITORY https://github.com/tatsuhiro-t/wslay.git GIT_TAG master + PATCH_COMMAND git apply --reverse --check ${CMAKE_CURRENT_SOURCE_DIR}/patches/wslay-disable-mask.patch || git apply ${CMAKE_CURRENT_SOURCE_DIR}/patches/wslay-disable-mask.patch ) # Disable OpenSSL and Mbed TLS. We use BoringSSL instead. diff --git a/wish/cpp/patches/wslay-disable-mask.patch b/wish/cpp/patches/wslay-disable-mask.patch new file mode 100644 index 0000000..874e319 --- /dev/null +++ b/wish/cpp/patches/wslay-disable-mask.patch @@ -0,0 +1,49 @@ +diff --git a/lib/wslay_event.c b/lib/wslay_event.c +index 4c29fe4..93e9255 100644 +--- a/lib/wslay_event.c ++++ b/lib/wslay_event.c +@@ -509,7 +509,7 @@ int wslay_event_recv(wslay_event_context_ptr ctx) { + (wslay_get_rsv1(iocb.rsv) && + (wslay_is_ctrl_frame(iocb.opcode) || + iocb.opcode == WSLAY_CONTINUATION_FRAME)) || +- (ctx->server && !iocb.mask) || (!ctx->server && iocb.mask)) { ++ iocb.mask) { + if ((r = wslay_event_queue_close_wrapper(ctx, WSLAY_CODE_PROTOCOL_ERROR, + NULL, 0)) != 0) { + return (int)r; +@@ -775,7 +775,7 @@ int wslay_event_send(wslay_event_context_ptr ctx) { + iocb.fin = 1; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; +- iocb.mask = ctx->server ^ 1; ++ iocb.mask = 0; + iocb.data = ctx->omsg->data; + iocb.data_length = ctx->opayloadlen; + if (ctx->opayloadoff) { +@@ -835,7 +835,7 @@ int wslay_event_send(wslay_event_context_ptr ctx) { + iocb.fin = ctx->omsg->fin; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; +- iocb.mask = ctx->server ? 0 : 1; ++ iocb.mask = 0; + iocb.data = ctx->obufmark; + iocb.data_length = (size_t)(ctx->obuflimit - ctx->obufmark); + iocb.payload_length = ctx->opayloadlen; +@@ -908,7 +908,7 @@ ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf, + iocb.fin = 1; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; +- iocb.mask = ctx->server ^ 1; ++ iocb.mask = 0; + iocb.data = ctx->omsg->data; + iocb.data_length = ctx->opayloadlen; + if (ctx->opayloadoff) { +@@ -971,7 +971,7 @@ ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf, + iocb.fin = ctx->omsg->fin; + iocb.opcode = ctx->omsg->opcode; + iocb.rsv = ctx->omsg->rsv; +- iocb.mask = ctx->server ? 0 : 1; ++ iocb.mask = 0; + iocb.data = ctx->obufmark; + iocb.data_length = (size_t)(ctx->obuflimit - ctx->obufmark); + iocb.payload_length = ctx->opayloadlen; diff --git a/wish/cpp/src/wish_handler_test.cc b/wish/cpp/src/wish_handler_test.cc index dc50566..a11d8b4 100644 --- a/wish/cpp/src/wish_handler_test.cc +++ b/wish/cpp/src/wish_handler_test.cc @@ -1,8 +1,11 @@ #include "wish_handler.h" -#include + +#include #include #include -#include +#include + +#include #include class WishHandlerTest : public ::testing::Test { @@ -10,13 +13,6 @@ class WishHandlerTest : public ::testing::Test { void SetUp() override { base_ = event_base_new(); ASSERT_NE(base_, nullptr); - - struct bufferevent* pair[2]; - int res = bufferevent_pair_new(base_, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS, pair); - ASSERT_EQ(res, 0); - - server_bev_ = pair[0]; - client_bev_ = pair[1]; } void TearDown() override { @@ -24,19 +20,19 @@ class WishHandlerTest : public ::testing::Test { } struct event_base* base_ = nullptr; - struct bufferevent* server_bev_ = nullptr; - struct bufferevent* client_bev_ = nullptr; }; TEST_F(WishHandlerTest, HandshakeAndSimpleExchange) { - // We allocate on heap because EventCallback might execute `delete handler`. - // To avoid this causing problems, we ensure we don't double delete. - WishHandler* server = new WishHandler(server_bev_, true /* is_server */); - WishHandler* client = new WishHandler(client_bev_, false /* is_server */); + struct bufferevent* pair[2]; + int res = bufferevent_pair_new( + base_, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS, pair); + ASSERT_EQ(res, 0); + + WishHandler* server = new WishHandler(pair[0], true /* is_server */); + WishHandler* client = new WishHandler(pair[1], false /* is_server */); bool server_opened = false; bool client_opened = false; - std::string received_from_client; std::string received_from_server; @@ -73,13 +69,112 @@ TEST_F(WishHandlerTest, HandshakeAndSimpleExchange) { EXPECT_TRUE(client_opened); EXPECT_TRUE(server_opened); - EXPECT_EQ(received_from_client, "Hello, Server!"); EXPECT_EQ(received_from_server, "Hello, Client!"); - // Clean up - // If connection is still alive, we can safely delete them. - // Their destructors will free the bufferevents and wslay contexts. delete server; delete client; } + +// Helper: drain all bytes from a bufferevent's input buffer. +static void DrainInput(struct bufferevent* bev) { + struct evbuffer* buf = bufferevent_get_input(bev); + evbuffer_drain(buf, evbuffer_get_length(buf)); +} + +// Tests that the client does NOT mask frames when sending. +// WiSH doesn't use masking (unlike WebSocket over TCP). +// +// Strategy: use a raw bufferevent on one end of a pair as a fake server. +// Inject a valid HTTP 200 response to move the WishHandler into OPEN state, +// then call SendText and inspect the raw bytes for the mask bit. +TEST_F(WishHandlerTest, ClientSendsUnmasked) { + // No DEFER_CALLBACKS: pair transfers data synchronously between loop ticks. + struct bufferevent* pair[2]; + int res = bufferevent_pair_new(base_, BEV_OPT_CLOSE_ON_FREE, pair); + ASSERT_EQ(res, 0); + // pair[0]: WishHandler client bev + // pair[1]: raw observer (fake server) + bufferevent_enable(pair[1], EV_READ | EV_WRITE); + + WishHandler* client = new WishHandler(pair[0], false /* is_server */); + // Start writes the HTTP POST request and enables read on pair[0]. + client->Start(); + + // Let client's HTTP request flow to pair[1]. + event_base_loop(base_, EVLOOP_NONBLOCK); + DrainInput(pair[1]); + + // Fake server sends HTTP 200 OK back through pair[1] -> pair[0]. + const char* resp = + "HTTP/1.1 200 OK\r\nContent-Type: application/web-stream\r\n\r\n"; + bufferevent_write(pair[1], resp, strlen(resp)); + + // Let the client process the response and transition to OPEN state. + event_base_loop(base_, EVLOOP_NONBLOCK); + DrainInput(pair[1]); + + // The client should now be OPEN. Send a text message. + client->SendText("Hello"); + event_base_loop(base_, EVLOOP_NONBLOCK); + + struct evbuffer* raw = bufferevent_get_input(pair[1]); + size_t len = evbuffer_get_length(raw); + ASSERT_GT(len, 0) << "No bytes received from client after handshake"; + + unsigned char* bytes = evbuffer_pullup(raw, len); + + // First byte: FIN=1, RSV=0, opcode=TEXT(0x1) => 0x81 + EXPECT_EQ(bytes[0], 0x81u); + + // Second byte: mask bit (0x80) + payload length. + // WiSH doesn't use masking. + bool is_masked = (bytes[1] & 0x80) != 0; + EXPECT_FALSE(is_masked) << "Client sent a masked frame! WiSH must not use masking."; + + // WishHandler destructor frees pair[0]. + delete client; + // free pair[1] manually. + bufferevent_free(pair[1]); +} + +// Tests that the server does NOT mask frames when sending. +TEST_F(WishHandlerTest, ServerSendsUnmasked) { + struct bufferevent* pair[2]; + int res = bufferevent_pair_new(base_, BEV_OPT_CLOSE_ON_FREE, pair); + ASSERT_EQ(res, 0); + // pair[0]: WishHandler server bev + // pair[1]: raw observer (fake client) + bufferevent_enable(pair[1], EV_READ | EV_WRITE); + + WishHandler* server = new WishHandler(pair[0], true /* is_server */); + server->Start(); + + // Fake client sends HTTP POST to open the connection. + const char* req = + "POST / HTTP/1.1\r\nHost: localhost\r\n" + "Content-Type: application/web-stream\r\n\r\n"; + bufferevent_write(pair[1], req, strlen(req)); + + // Let server process the request, transition to OPEN, and send 200 OK. + event_base_loop(base_, EVLOOP_NONBLOCK); + DrainInput(pair[1]); + + // Server is now OPEN. Send a text message. + server->SendText("Hello"); + event_base_loop(base_, EVLOOP_NONBLOCK); + + struct evbuffer* raw = bufferevent_get_input(pair[1]); + size_t len = evbuffer_get_length(raw); + ASSERT_GT(len, 0) << "No bytes received from server after handshake"; + + unsigned char* bytes = evbuffer_pullup(raw, len); + + EXPECT_EQ(bytes[0], 0x81u); + + bool is_masked = (bytes[1] & 0x80) != 0; + EXPECT_FALSE(is_masked) << "Server sent a masked frame! WiSH must not use masking."; + + delete server; + bufferevent_free(pair[1]); +}