mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-23 01:21:42 +00:00
wpiutil: Add WebSocket implementation (#1186)
This is a RFC 6455 compliant implementation with both client and server support.
This commit is contained in:
299
wpiutil/src/test/native/cpp/WebSocketClientTest.cpp
Normal file
299
wpiutil/src/test/native/cpp/WebSocketClientTest.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "wpi/WebSocket.h" // NOLINT(build/include_order)
|
||||
|
||||
#include "WebSocketTest.h"
|
||||
#include "wpi/Base64.h"
|
||||
#include "wpi/HttpParser.h"
|
||||
#include "wpi/SmallString.h"
|
||||
#include "wpi/raw_uv_ostream.h"
|
||||
#include "wpi/sha1.h"
|
||||
|
||||
namespace wpi {
|
||||
|
||||
class WebSocketClientTest : public WebSocketTest {
|
||||
public:
|
||||
WebSocketClientTest() {
|
||||
// Bare bones server
|
||||
req.header.connect([this](StringRef name, StringRef value) {
|
||||
// save key (required for valid response)
|
||||
if (name.equals_lower("sec-websocket-key")) clientKey = value;
|
||||
});
|
||||
req.headersComplete.connect([this](bool) {
|
||||
// send response
|
||||
SmallVector<uv::Buffer, 4> bufs;
|
||||
raw_uv_ostream os{bufs, 4096};
|
||||
os << "HTTP/1.1 101 Switching Protocols\r\n";
|
||||
os << "Upgrade: websocket\r\n";
|
||||
os << "Connection: Upgrade\r\n";
|
||||
|
||||
// accept hash
|
||||
SHA1 hash;
|
||||
hash.Update(clientKey);
|
||||
hash.Update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
if (mockBadAccept) hash.Update("1");
|
||||
SmallString<64> hashBuf;
|
||||
SmallString<64> acceptBuf;
|
||||
os << "Sec-WebSocket-Accept: "
|
||||
<< Base64Encode(hash.Final(hashBuf), acceptBuf) << "\r\n";
|
||||
|
||||
if (!mockProtocol.empty())
|
||||
os << "Sec-WebSocket-Protocol: " << mockProtocol << "\r\n";
|
||||
|
||||
os << "\r\n";
|
||||
|
||||
conn->Write(bufs, [](auto bufs, uv::Error) {
|
||||
for (auto& buf : bufs) buf.Deallocate();
|
||||
});
|
||||
|
||||
serverHeadersDone = true;
|
||||
if (connected) connected();
|
||||
});
|
||||
|
||||
serverPipe->Listen([this] {
|
||||
conn = serverPipe->Accept();
|
||||
conn->StartRead();
|
||||
conn->data.connect([this](uv::Buffer& buf, size_t size) {
|
||||
StringRef data{buf.base, size};
|
||||
if (!serverHeadersDone) {
|
||||
data = req.Execute(data);
|
||||
if (req.HasError()) Finish();
|
||||
ASSERT_EQ(req.GetError(), HPE_OK) << http_errno_name(req.GetError());
|
||||
if (data.empty()) return;
|
||||
}
|
||||
wireData.insert(wireData.end(), data.bytes_begin(), data.bytes_end());
|
||||
});
|
||||
conn->end.connect([this] { Finish(); });
|
||||
});
|
||||
}
|
||||
|
||||
bool mockBadAccept = false;
|
||||
std::vector<uint8_t> wireData;
|
||||
std::shared_ptr<uv::Pipe> conn;
|
||||
HttpParser req{HttpParser::kRequest};
|
||||
SmallString<64> clientKey;
|
||||
std::string mockProtocol;
|
||||
bool serverHeadersDone = false;
|
||||
std::function<void()> connected;
|
||||
};
|
||||
|
||||
TEST_F(WebSocketClientTest, Open) {
|
||||
int gotOpen = 0;
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
Finish();
|
||||
if (code != 1005 && code != 1006)
|
||||
FAIL() << "Code: " << code << " Reason: " << reason;
|
||||
});
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
++gotOpen;
|
||||
Finish();
|
||||
ASSERT_TRUE(protocol.empty());
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotOpen, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketClientTest, BadAccept) {
|
||||
int gotClosed = 0;
|
||||
|
||||
mockBadAccept = true;
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
ws->closed.connect([&](uint16_t code, StringRef msg) {
|
||||
Finish();
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1002) << "Message: " << msg;
|
||||
});
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
Finish();
|
||||
FAIL() << "Got open";
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketClientTest, ProtocolGood) {
|
||||
int gotOpen = 0;
|
||||
|
||||
mockProtocol = "myProtocol";
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(
|
||||
*clientPipe, "/test", pipeName,
|
||||
ArrayRef<StringRef>{"myProtocol", "myProtocol2"});
|
||||
ws->closed.connect([&](uint16_t code, StringRef msg) {
|
||||
Finish();
|
||||
if (code != 1005 && code != 1006)
|
||||
FAIL() << "Code: " << code << "Message: " << msg;
|
||||
});
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
++gotOpen;
|
||||
Finish();
|
||||
ASSERT_EQ(protocol, "myProtocol");
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotOpen, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketClientTest, ProtocolRespNotReq) {
|
||||
int gotClosed = 0;
|
||||
|
||||
mockProtocol = "myProtocol";
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
ws->closed.connect([&](uint16_t code, StringRef msg) {
|
||||
Finish();
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1003) << "Message: " << msg;
|
||||
});
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
Finish();
|
||||
FAIL() << "Got open";
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketClientTest, ProtocolReqNotResp) {
|
||||
int gotClosed = 0;
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName,
|
||||
StringRef{"myProtocol"});
|
||||
ws->closed.connect([&](uint16_t code, StringRef msg) {
|
||||
Finish();
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1002) << "Message: " << msg;
|
||||
});
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
Finish();
|
||||
FAIL() << "Got open";
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// Send and receive data. Most of these cases are tested in
|
||||
// WebSocketServerTest, so only spot check differences like masking.
|
||||
//
|
||||
|
||||
class WebSocketClientDataTest : public WebSocketClientTest,
|
||||
public ::testing::WithParamInterface<size_t> {
|
||||
public:
|
||||
WebSocketClientDataTest() {
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
if (setupWebSocket) setupWebSocket();
|
||||
});
|
||||
}
|
||||
|
||||
std::function<void()> setupWebSocket;
|
||||
std::shared_ptr<WebSocket> ws;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(WebSocketClientDataTests, WebSocketClientDataTest,
|
||||
::testing::Values(0, 1, 125, 126, 65535, 65536), );
|
||||
|
||||
TEST_P(WebSocketClientDataTest, SendBinary) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) {
|
||||
ws->SendBinary(uv::Buffer(data), [&](auto bufs, uv::Error) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_FALSE(bufs.empty());
|
||||
ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data()));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x02, true, true, data);
|
||||
AdjustMasking(wireData);
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketClientDataTest, ReceiveBinary) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->binary.connect([&](ArrayRef<uint8_t> inData, bool fin) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_TRUE(fin);
|
||||
std::vector<uint8_t> recvData{inData.begin(), inData.end()};
|
||||
ASSERT_EQ(data, recvData);
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x02, true, false, data);
|
||||
connected = [&] {
|
||||
conn->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// The client must close the connection if a masked frame is received.
|
||||
//
|
||||
|
||||
TEST_P(WebSocketClientDataTest, ReceiveMasked) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), ' ');
|
||||
setupWebSocket = [&] {
|
||||
ws->text.connect([&](StringRef, bool) {
|
||||
ws->Terminate();
|
||||
FAIL() << "Should not have gotten masked message";
|
||||
});
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, true, true, data);
|
||||
connected = [&] {
|
||||
conn->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
} // namespace wpi
|
||||
148
wpiutil/src/test/native/cpp/WebSocketIntegrationTest.cpp
Normal file
148
wpiutil/src/test/native/cpp/WebSocketIntegrationTest.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "wpi/WebSocketServer.h" // NOLINT(build/include_order)
|
||||
|
||||
#include "WebSocketTest.h"
|
||||
#include "wpi/HttpParser.h"
|
||||
#include "wpi/SmallString.h"
|
||||
|
||||
namespace wpi {
|
||||
|
||||
class WebSocketIntegrationTest : public WebSocketTest {};
|
||||
|
||||
TEST_F(WebSocketIntegrationTest, Open) {
|
||||
int gotServerOpen = 0;
|
||||
int gotClientOpen = 0;
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto server = WebSocketServer::Create(*conn);
|
||||
server->connected.connect([&](StringRef url, WebSocket&) {
|
||||
++gotServerOpen;
|
||||
ASSERT_EQ(url, "/test");
|
||||
});
|
||||
});
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
Finish();
|
||||
if (code != 1005 && code != 1006)
|
||||
FAIL() << "Code: " << code << " Reason: " << reason;
|
||||
});
|
||||
ws->open.connect([&, s = ws.get() ](StringRef) {
|
||||
++gotClientOpen;
|
||||
s->Close();
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotServerOpen, 1);
|
||||
ASSERT_EQ(gotClientOpen, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketIntegrationTest, Protocol) {
|
||||
int gotServerOpen = 0;
|
||||
int gotClientOpen = 0;
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto server = WebSocketServer::Create(*conn, {"proto1", "proto2"});
|
||||
server->connected.connect([&](StringRef, WebSocket& ws) {
|
||||
++gotServerOpen;
|
||||
ASSERT_EQ(ws.GetProtocol(), "proto1");
|
||||
});
|
||||
});
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws =
|
||||
WebSocket::CreateClient(*clientPipe, "/test", pipeName, {"proto1"});
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
Finish();
|
||||
if (code != 1005 && code != 1006)
|
||||
FAIL() << "Code: " << code << " Reason: " << reason;
|
||||
});
|
||||
ws->open.connect([&, s = ws.get() ](StringRef protocol) {
|
||||
++gotClientOpen;
|
||||
s->Close();
|
||||
ASSERT_EQ(protocol, "proto1");
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotServerOpen, 1);
|
||||
ASSERT_EQ(gotClientOpen, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketIntegrationTest, ServerSendBinary) {
|
||||
int gotData = 0;
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto server = WebSocketServer::Create(*conn);
|
||||
server->connected.connect([&](StringRef, WebSocket& ws) {
|
||||
ws.SendBinary(uv::Buffer{"\x03\x04", 2}, [&](auto, uv::Error) {});
|
||||
ws.Close();
|
||||
});
|
||||
});
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
Finish();
|
||||
if (code != 1005 && code != 1006)
|
||||
FAIL() << "Code: " << code << " Reason: " << reason;
|
||||
});
|
||||
ws->binary.connect([&](ArrayRef<uint8_t> data, bool) {
|
||||
++gotData;
|
||||
std::vector<uint8_t> recvData{data.begin(), data.end()};
|
||||
std::vector<uint8_t> expectData{0x03, 0x04};
|
||||
ASSERT_EQ(recvData, expectData);
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotData, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketIntegrationTest, ClientSendText) {
|
||||
int gotData = 0;
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto server = WebSocketServer::Create(*conn);
|
||||
server->connected.connect([&](StringRef, WebSocket& ws) {
|
||||
ws.text.connect([&](StringRef data, bool) {
|
||||
++gotData;
|
||||
ASSERT_EQ(data, "hello");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
Finish();
|
||||
if (code != 1005 && code != 1006)
|
||||
FAIL() << "Code: " << code << " Reason: " << reason;
|
||||
});
|
||||
ws->open.connect([&, s = ws.get() ](StringRef) {
|
||||
s->SendText(uv::Buffer{"hello"}, [&](auto, uv::Error) {});
|
||||
s->Close();
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotData, 1);
|
||||
}
|
||||
|
||||
} // namespace wpi
|
||||
736
wpiutil/src/test/native/cpp/WebSocketServerTest.cpp
Normal file
736
wpiutil/src/test/native/cpp/WebSocketServerTest.cpp
Normal file
@@ -0,0 +1,736 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "wpi/WebSocket.h" // NOLINT(build/include_order)
|
||||
|
||||
#include "WebSocketTest.h"
|
||||
#include "wpi/Base64.h"
|
||||
#include "wpi/HttpParser.h"
|
||||
#include "wpi/SmallString.h"
|
||||
#include "wpi/raw_uv_ostream.h"
|
||||
#include "wpi/sha1.h"
|
||||
|
||||
namespace wpi {
|
||||
|
||||
class WebSocketServerTest : public WebSocketTest {
|
||||
public:
|
||||
WebSocketServerTest() {
|
||||
resp.headersComplete.connect([this](bool) { headersDone = true; });
|
||||
|
||||
serverPipe->Listen([this]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
ws = WebSocket::CreateServer(*conn, "foo", "13");
|
||||
if (setupWebSocket) setupWebSocket();
|
||||
});
|
||||
clientPipe->Connect(pipeName, [this]() {
|
||||
clientPipe->StartRead();
|
||||
clientPipe->data.connect([this](uv::Buffer& buf, size_t size) {
|
||||
StringRef data{buf.base, size};
|
||||
if (!headersDone) {
|
||||
data = resp.Execute(data);
|
||||
if (resp.HasError()) Finish();
|
||||
ASSERT_EQ(resp.GetError(), HPE_OK)
|
||||
<< http_errno_name(resp.GetError());
|
||||
if (data.empty()) return;
|
||||
}
|
||||
wireData.insert(wireData.end(), data.bytes_begin(), data.bytes_end());
|
||||
if (handleData) handleData(data);
|
||||
});
|
||||
clientPipe->end.connect([this]() { Finish(); });
|
||||
});
|
||||
}
|
||||
|
||||
std::function<void()> setupWebSocket;
|
||||
std::function<void(StringRef)> handleData;
|
||||
std::vector<uint8_t> wireData;
|
||||
std::shared_ptr<WebSocket> ws;
|
||||
HttpParser resp{HttpParser::kResponse};
|
||||
bool headersDone = false;
|
||||
};
|
||||
|
||||
//
|
||||
// Terminate closes the endpoint but doesn't send a close frame.
|
||||
//
|
||||
|
||||
TEST_F(WebSocketServerTest, Terminate) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) { ws->Terminate(); });
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1006) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_TRUE(wireData.empty()); // terminate doesn't send data
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketServerTest, TerminateCode) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) { ws->Terminate(1000); });
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1000) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_TRUE(wireData.empty()); // terminate doesn't send data
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketServerTest, TerminateReason) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) { ws->Terminate(1000, "reason"); });
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1000);
|
||||
ASSERT_EQ(reason, "reason");
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_TRUE(wireData.empty()); // terminate doesn't send data
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// Close() sends a close frame.
|
||||
//
|
||||
|
||||
TEST_F(WebSocketServerTest, CloseBasic) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) { ws->Close(); });
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1005) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
// need to respond with close for server to finish shutdown
|
||||
auto message = BuildMessage(0x08, true, true, {});
|
||||
handleData = [&](StringRef) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x08, true, false, {});
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketServerTest, CloseCode) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) { ws->Close(1000); });
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1000) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
// need to respond with close for server to finish shutdown
|
||||
auto message = BuildMessage(0x08, true, true, {0x03u, 0xe8u});
|
||||
handleData = [&](StringRef) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x08, true, false, {0x03u, 0xe8u});
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketServerTest, CloseReason) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) { ws->Close(1000, "hangup"); });
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1000);
|
||||
ASSERT_EQ(reason, "hangup");
|
||||
});
|
||||
};
|
||||
// need to respond with close for server to finish shutdown
|
||||
auto message = BuildMessage(0x08, true, true,
|
||||
{0x03u, 0xe8u, 'h', 'a', 'n', 'g', 'u', 'p'});
|
||||
handleData = [&](StringRef) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x08, true, false,
|
||||
{0x03u, 0xe8u, 'h', 'a', 'n', 'g', 'u', 'p'});
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// Receiving a close frame results in closure and echoing the close frame.
|
||||
//
|
||||
|
||||
TEST_F(WebSocketServerTest, ReceiveCloseBasic) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1005) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x08, true, true, {});
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
// the endpoint should echo the message
|
||||
auto expectData = BuildMessage(0x08, true, false, {});
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketServerTest, ReceiveCloseCode) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1000) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x08, true, true, {0x03u, 0xe8u});
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
// the endpoint should echo the message
|
||||
auto expectData = BuildMessage(0x08, true, false, {0x03u, 0xe8u});
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketServerTest, ReceiveCloseReason) {
|
||||
int gotClosed = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1000);
|
||||
ASSERT_EQ(reason, "hangup");
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x08, true, true,
|
||||
{0x03u, 0xe8u, 'h', 'a', 'n', 'g', 'u', 'p'});
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
// the endpoint should echo the message
|
||||
auto expectData = BuildMessage(0x08, true, false,
|
||||
{0x03u, 0xe8u, 'h', 'a', 'n', 'g', 'u', 'p'});
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// If an unknown opcode is received, the receiving endpoint MUST _Fail the
|
||||
// WebSocket Connection_.
|
||||
//
|
||||
|
||||
class WebSocketServerBadOpcodeTest
|
||||
: public WebSocketServerTest,
|
||||
public ::testing::WithParamInterface<uint8_t> {};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(WebSocketServerBadOpcodeTests,
|
||||
WebSocketServerBadOpcodeTest,
|
||||
::testing::Values(3, 4, 5, 6, 7, 0xb, 0xc, 0xd, 0xe,
|
||||
0xf), );
|
||||
|
||||
TEST_P(WebSocketServerBadOpcodeTest, Receive) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(4, 0x03);
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(GetParam(), true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// Control frames themselves MUST NOT be fragmented.
|
||||
//
|
||||
|
||||
class WebSocketServerControlFrameTest
|
||||
: public WebSocketServerTest,
|
||||
public ::testing::WithParamInterface<uint8_t> {};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(WebSocketServerControlFrameTests,
|
||||
WebSocketServerControlFrameTest,
|
||||
::testing::Values(0x8, 0x9, 0xa), );
|
||||
|
||||
TEST_P(WebSocketServerControlFrameTest, ReceiveFragment) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(4, 0x03);
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(GetParam(), false, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// A fragmented message consists of a single frame with the FIN bit
|
||||
// clear and an opcode other than 0, followed by zero or more frames
|
||||
// with the FIN bit clear and the opcode set to 0, and terminated by
|
||||
// a single frame with the FIN bit set and an opcode of 0.
|
||||
//
|
||||
|
||||
// No previous message
|
||||
TEST_F(WebSocketServerTest, ReceiveFragmentInvalidNoPrevFrame) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(4, 0x03);
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x00, false, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
// No previous message with FIN=1.
|
||||
TEST_F(WebSocketServerTest, ReceiveFragmentInvalidNoPrevFragment) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(4, 0x03);
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, true, true, {}); // FIN=1
|
||||
auto message2 = BuildMessage(0x00, false, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write({uv::Buffer(message), uv::Buffer(message2)},
|
||||
[&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
// Incomplete fragment
|
||||
TEST_F(WebSocketServerTest, ReceiveFragmentInvalidIncomplete) {
|
||||
int gotCallback = 0;
|
||||
setupWebSocket = [&] {
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, false, true, {});
|
||||
auto message2 = BuildMessage(0x00, false, true, {});
|
||||
auto message3 = BuildMessage(0x01, true, true, {});
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(
|
||||
{uv::Buffer(message), uv::Buffer(message2), uv::Buffer(message3)},
|
||||
[&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
// Normally fragments are combined into a single callback
|
||||
TEST_F(WebSocketServerTest, ReceiveFragment) {
|
||||
int gotCallback = 0;
|
||||
|
||||
std::vector<uint8_t> data(4, 0x03);
|
||||
std::vector<uint8_t> data2(4, 0x04);
|
||||
std::vector<uint8_t> data3(4, 0x05);
|
||||
std::vector<uint8_t> combData{data};
|
||||
combData.insert(combData.end(), data2.begin(), data2.end());
|
||||
combData.insert(combData.end(), data3.begin(), data3.end());
|
||||
|
||||
setupWebSocket = [&] {
|
||||
ws->binary.connect([&](ArrayRef<uint8_t> inData, bool fin) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_TRUE(fin);
|
||||
std::vector<uint8_t> recvData{inData.begin(), inData.end()};
|
||||
ASSERT_EQ(combData, recvData);
|
||||
});
|
||||
};
|
||||
|
||||
auto message = BuildMessage(0x02, false, true, data);
|
||||
auto message2 = BuildMessage(0x00, false, true, data2);
|
||||
auto message3 = BuildMessage(0x00, true, true, data3);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(
|
||||
{uv::Buffer(message), uv::Buffer(message2), uv::Buffer(message3)},
|
||||
[&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
// But can be configured for multiple callbacks
|
||||
TEST_F(WebSocketServerTest, ReceiveFragmentSeparate) {
|
||||
int gotCallback = 0;
|
||||
|
||||
std::vector<uint8_t> data(4, 0x03);
|
||||
std::vector<uint8_t> data2(4, 0x04);
|
||||
std::vector<uint8_t> data3(4, 0x05);
|
||||
std::vector<uint8_t> combData{data};
|
||||
combData.insert(combData.end(), data2.begin(), data2.end());
|
||||
combData.insert(combData.end(), data3.begin(), data3.end());
|
||||
|
||||
setupWebSocket = [&] {
|
||||
ws->SetCombineFragments(false);
|
||||
ws->binary.connect([&](ArrayRef<uint8_t> inData, bool fin) {
|
||||
std::vector<uint8_t> recvData{inData.begin(), inData.end()};
|
||||
switch (++gotCallback) {
|
||||
case 1:
|
||||
ASSERT_FALSE(fin);
|
||||
ASSERT_EQ(data, recvData);
|
||||
break;
|
||||
case 2:
|
||||
ASSERT_FALSE(fin);
|
||||
ASSERT_EQ(data2, recvData);
|
||||
break;
|
||||
case 3:
|
||||
ws->Terminate();
|
||||
ASSERT_TRUE(fin);
|
||||
ASSERT_EQ(data3, recvData);
|
||||
break;
|
||||
default:
|
||||
FAIL() << "too many callbacks";
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
auto message = BuildMessage(0x02, false, true, data);
|
||||
auto message2 = BuildMessage(0x00, false, true, data2);
|
||||
auto message3 = BuildMessage(0x00, true, true, data3);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(
|
||||
{uv::Buffer(message), uv::Buffer(message2), uv::Buffer(message3)},
|
||||
[&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 3);
|
||||
}
|
||||
|
||||
//
|
||||
// Maximum message size is limited.
|
||||
//
|
||||
|
||||
// Single message
|
||||
TEST_F(WebSocketServerTest, ReceiveTooLarge) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(2048, 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->SetMaxMessageSize(1024);
|
||||
ws->binary.connect([&](auto, bool) {
|
||||
ws->Terminate();
|
||||
FAIL() << "Should not have gotten unmasked message";
|
||||
});
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1009) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
// Applied across fragments if combining
|
||||
TEST_F(WebSocketServerTest, ReceiveTooLargeFragmented) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(768, 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->SetMaxMessageSize(1024);
|
||||
ws->binary.connect([&](auto, bool) {
|
||||
ws->Terminate();
|
||||
FAIL() << "Should not have gotten unmasked message";
|
||||
});
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1009) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, false, true, data);
|
||||
auto message2 = BuildMessage(0x00, true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write({uv::Buffer(message), uv::Buffer(message2)},
|
||||
[&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// Send and receive data.
|
||||
//
|
||||
|
||||
class WebSocketServerDataTest : public WebSocketServerTest,
|
||||
public ::testing::WithParamInterface<size_t> {};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(WebSocketServerDataTests, WebSocketServerDataTest,
|
||||
::testing::Values(0, 1, 125, 126, 65535, 65536), );
|
||||
|
||||
TEST_P(WebSocketServerDataTest, SendText) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), ' ');
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) {
|
||||
ws->SendText(uv::Buffer(data), [&](auto bufs, uv::Error) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_FALSE(bufs.empty());
|
||||
ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data()));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x01, true, false, data);
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, SendBinary) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) {
|
||||
ws->SendBinary(uv::Buffer(data), [&](auto bufs, uv::Error) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_FALSE(bufs.empty());
|
||||
ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data()));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x02, true, false, data);
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, SendPing) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) {
|
||||
ws->SendPing(uv::Buffer(data), [&](auto bufs, uv::Error) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_FALSE(bufs.empty());
|
||||
ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data()));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x09, true, false, data);
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, SendPong) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->open.connect([&](StringRef) {
|
||||
ws->SendPong(uv::Buffer(data), [&](auto bufs, uv::Error) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_FALSE(bufs.empty());
|
||||
ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data()));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loop->Run();
|
||||
|
||||
auto expectData = BuildMessage(0x0a, true, false, data);
|
||||
ASSERT_EQ(wireData, expectData);
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, ReceiveText) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), ' ');
|
||||
setupWebSocket = [&] {
|
||||
ws->text.connect([&](StringRef inData, bool fin) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_TRUE(fin);
|
||||
std::vector<uint8_t> recvData;
|
||||
recvData.insert(recvData.end(), inData.bytes_begin(), inData.bytes_end());
|
||||
ASSERT_EQ(data, recvData);
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, ReceiveBinary) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->binary.connect([&](ArrayRef<uint8_t> inData, bool fin) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
ASSERT_TRUE(fin);
|
||||
std::vector<uint8_t> recvData{inData.begin(), inData.end()};
|
||||
ASSERT_EQ(data, recvData);
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x02, true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, ReceivePing) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->ping.connect([&](ArrayRef<uint8_t> inData) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
std::vector<uint8_t> recvData{inData.begin(), inData.end()};
|
||||
ASSERT_EQ(data, recvData);
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x09, true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
TEST_P(WebSocketServerDataTest, ReceivePong) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), 0x03u);
|
||||
setupWebSocket = [&] {
|
||||
ws->pong.connect([&](ArrayRef<uint8_t> inData) {
|
||||
++gotCallback;
|
||||
ws->Terminate();
|
||||
std::vector<uint8_t> recvData{inData.begin(), inData.end()};
|
||||
ASSERT_EQ(data, recvData);
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x0a, true, true, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// The server must close the connection if an unmasked frame is received.
|
||||
//
|
||||
|
||||
TEST_P(WebSocketServerDataTest, ReceiveUnmasked) {
|
||||
int gotCallback = 0;
|
||||
std::vector<uint8_t> data(GetParam(), ' ');
|
||||
setupWebSocket = [&] {
|
||||
ws->text.connect([&](StringRef, bool) {
|
||||
ws->Terminate();
|
||||
FAIL() << "Should not have gotten unmasked message";
|
||||
});
|
||||
ws->closed.connect([&](uint16_t code, StringRef reason) {
|
||||
++gotCallback;
|
||||
ASSERT_EQ(code, 1002) << "reason: " << reason;
|
||||
});
|
||||
};
|
||||
auto message = BuildMessage(0x01, true, false, data);
|
||||
resp.headersComplete.connect([&](bool) {
|
||||
clientPipe->Write(uv::Buffer(message), [&](auto bufs, uv::Error) {});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
ASSERT_EQ(gotCallback, 1);
|
||||
}
|
||||
|
||||
} // namespace wpi
|
||||
345
wpiutil/src/test/native/cpp/WebSocketTest.cpp
Normal file
345
wpiutil/src/test/native/cpp/WebSocketTest.cpp
Normal file
@@ -0,0 +1,345 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "wpi/WebSocket.h" // NOLINT(build/include_order)
|
||||
|
||||
#include "WebSocketTest.h"
|
||||
|
||||
#include "wpi/HttpParser.h"
|
||||
|
||||
namespace wpi {
|
||||
|
||||
#ifdef _WIN32
|
||||
const char* WebSocketTest::pipeName = "\\\\.\\pipe\\websocket-unit-test";
|
||||
#else
|
||||
const char* WebSocketTest::pipeName = "/tmp/websocket-unit-test";
|
||||
#endif
|
||||
const uint8_t WebSocketTest::testMask[4] = {0x11, 0x22, 0x33, 0x44};
|
||||
|
||||
void WebSocketTest::SetUpTestCase() {
|
||||
#ifndef _WIN32
|
||||
unlink(pipeName);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<uint8_t> WebSocketTest::BuildHeader(uint8_t opcode, bool fin,
|
||||
bool masking, uint64_t len) {
|
||||
std::vector<uint8_t> data;
|
||||
data.push_back(opcode | (fin ? 0x80u : 0x00u));
|
||||
if (len < 126) {
|
||||
data.push_back(len | (masking ? 0x80 : 0x00u));
|
||||
} else if (len < 65536) {
|
||||
data.push_back(126u | (masking ? 0x80 : 0x00u));
|
||||
data.push_back(len >> 8);
|
||||
data.push_back(len & 0xff);
|
||||
} else {
|
||||
data.push_back(127u | (masking ? 0x80u : 0x00u));
|
||||
for (int i = 56; i >= 0; i -= 8) data.push_back((len >> i) & 0xff);
|
||||
}
|
||||
if (masking) data.insert(data.end(), &testMask[0], &testMask[4]);
|
||||
return data;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> WebSocketTest::BuildMessage(uint8_t opcode, bool fin,
|
||||
bool masking,
|
||||
ArrayRef<uint8_t> data) {
|
||||
auto finalData = BuildHeader(opcode, fin, masking, data.size());
|
||||
size_t headerSize = finalData.size();
|
||||
finalData.insert(finalData.end(), data.begin(), data.end());
|
||||
if (masking) {
|
||||
uint8_t mask[4] = {finalData[headerSize - 4], finalData[headerSize - 3],
|
||||
finalData[headerSize - 2], finalData[headerSize - 1]};
|
||||
int n = 0;
|
||||
for (size_t i = headerSize, end = finalData.size(); i < end; ++i) {
|
||||
finalData[i] ^= mask[n++];
|
||||
if (n >= 4) n = 0;
|
||||
}
|
||||
}
|
||||
return finalData;
|
||||
}
|
||||
|
||||
// If the message is masked, changes the mask to match the mask set by
|
||||
// BuildHeader() by unmasking and remasking.
|
||||
void WebSocketTest::AdjustMasking(MutableArrayRef<uint8_t> message) {
|
||||
if (message.size() < 2) return;
|
||||
if ((message[1] & 0x80) == 0) return; // not masked
|
||||
size_t maskPos;
|
||||
uint8_t len = message[1] & 0x7f;
|
||||
if (len == 126)
|
||||
maskPos = 4;
|
||||
else if (len == 127)
|
||||
maskPos = 10;
|
||||
else
|
||||
maskPos = 2;
|
||||
uint8_t mask[4] = {message[maskPos], message[maskPos + 1],
|
||||
message[maskPos + 2], message[maskPos + 3]};
|
||||
message[maskPos] = testMask[0];
|
||||
message[maskPos + 1] = testMask[1];
|
||||
message[maskPos + 2] = testMask[2];
|
||||
message[maskPos + 3] = testMask[3];
|
||||
int n = 0;
|
||||
for (auto& ch : message.slice(maskPos + 4)) {
|
||||
ch ^= mask[n] ^ testMask[n];
|
||||
if (++n >= 4) n = 0;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(WebSocketTest, CreateClientBasic) {
|
||||
int gotHost = 0;
|
||||
int gotUpgrade = 0;
|
||||
int gotConnection = 0;
|
||||
int gotKey = 0;
|
||||
int gotVersion = 0;
|
||||
|
||||
HttpParser req{HttpParser::kRequest};
|
||||
req.url.connect([](StringRef url) { ASSERT_EQ(url, "/test"); });
|
||||
req.header.connect([&](StringRef name, StringRef value) {
|
||||
if (name.equals_lower("host")) {
|
||||
ASSERT_EQ(value, pipeName);
|
||||
++gotHost;
|
||||
} else if (name.equals_lower("upgrade")) {
|
||||
ASSERT_EQ(value, "websocket");
|
||||
++gotUpgrade;
|
||||
} else if (name.equals_lower("connection")) {
|
||||
ASSERT_EQ(value, "Upgrade");
|
||||
++gotConnection;
|
||||
} else if (name.equals_lower("sec-websocket-key")) {
|
||||
++gotKey;
|
||||
} else if (name.equals_lower("sec-websocket-version")) {
|
||||
ASSERT_EQ(value, "13");
|
||||
++gotVersion;
|
||||
} else {
|
||||
FAIL() << "unexpected header " << name.str();
|
||||
}
|
||||
});
|
||||
req.headersComplete.connect([&](bool) { Finish(); });
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
conn->StartRead();
|
||||
conn->data.connect([&](uv::Buffer& buf, size_t size) {
|
||||
req.Execute(StringRef{buf.base, size});
|
||||
if (req.HasError()) Finish();
|
||||
ASSERT_EQ(req.GetError(), HPE_OK) << http_errno_name(req.GetError());
|
||||
});
|
||||
});
|
||||
clientPipe->Connect(pipeName, [&]() {
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName);
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotHost, 1);
|
||||
ASSERT_EQ(gotUpgrade, 1);
|
||||
ASSERT_EQ(gotConnection, 1);
|
||||
ASSERT_EQ(gotKey, 1);
|
||||
ASSERT_EQ(gotVersion, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketTest, CreateClientExtraHeaders) {
|
||||
int gotExtra1 = 0;
|
||||
int gotExtra2 = 0;
|
||||
HttpParser req{HttpParser::kRequest};
|
||||
req.header.connect([&](StringRef name, StringRef value) {
|
||||
if (name.equals("Extra1")) {
|
||||
ASSERT_EQ(value, "Data1");
|
||||
++gotExtra1;
|
||||
} else if (name.equals("Extra2")) {
|
||||
ASSERT_EQ(value, "Data2");
|
||||
++gotExtra2;
|
||||
}
|
||||
});
|
||||
req.headersComplete.connect([&](bool) { Finish(); });
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
conn->StartRead();
|
||||
conn->data.connect([&](uv::Buffer& buf, size_t size) {
|
||||
req.Execute(StringRef{buf.base, size});
|
||||
if (req.HasError()) Finish();
|
||||
ASSERT_EQ(req.GetError(), HPE_OK) << http_errno_name(req.GetError());
|
||||
});
|
||||
});
|
||||
clientPipe->Connect(pipeName, [&]() {
|
||||
WebSocket::ClientOptions options;
|
||||
SmallVector<std::pair<StringRef, StringRef>, 4> extraHeaders;
|
||||
extraHeaders.emplace_back("Extra1", "Data1");
|
||||
extraHeaders.emplace_back("Extra2", "Data2");
|
||||
options.extraHeaders = extraHeaders;
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName,
|
||||
ArrayRef<StringRef>{}, options);
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotExtra1, 1);
|
||||
ASSERT_EQ(gotExtra2, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketTest, CreateClientTimeout) {
|
||||
int gotClosed = 0;
|
||||
serverPipe->Listen([&]() { auto conn = serverPipe->Accept(); });
|
||||
clientPipe->Connect(pipeName, [&]() {
|
||||
WebSocket::ClientOptions options;
|
||||
options.handshakeTimeout = uv::Timer::Time{100};
|
||||
auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName,
|
||||
ArrayRef<StringRef>{}, options);
|
||||
ws->closed.connect([&](uint16_t code, StringRef) {
|
||||
Finish();
|
||||
++gotClosed;
|
||||
ASSERT_EQ(code, 1006);
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotClosed, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketTest, CreateServerBasic) {
|
||||
int gotStatus = 0;
|
||||
int gotUpgrade = 0;
|
||||
int gotConnection = 0;
|
||||
int gotAccept = 0;
|
||||
int gotOpen = 0;
|
||||
|
||||
HttpParser resp{HttpParser::kResponse};
|
||||
resp.status.connect([&](StringRef status) {
|
||||
++gotStatus;
|
||||
ASSERT_EQ(resp.GetStatusCode(), 101u) << "status: " << status;
|
||||
});
|
||||
resp.header.connect([&](StringRef name, StringRef value) {
|
||||
if (name.equals_lower("upgrade")) {
|
||||
ASSERT_EQ(value, "websocket");
|
||||
++gotUpgrade;
|
||||
} else if (name.equals_lower("connection")) {
|
||||
ASSERT_EQ(value, "Upgrade");
|
||||
++gotConnection;
|
||||
} else if (name.equals_lower("sec-websocket-accept")) {
|
||||
++gotAccept;
|
||||
} else {
|
||||
FAIL() << "unexpected header " << name.str();
|
||||
}
|
||||
});
|
||||
resp.headersComplete.connect([&](bool) { Finish(); });
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto ws = WebSocket::CreateServer(*conn, "foo", "13");
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
++gotOpen;
|
||||
ASSERT_TRUE(protocol.empty());
|
||||
});
|
||||
});
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
clientPipe->StartRead();
|
||||
clientPipe->data.connect([&](uv::Buffer& buf, size_t size) {
|
||||
resp.Execute(StringRef{buf.base, size});
|
||||
if (resp.HasError()) Finish();
|
||||
ASSERT_EQ(resp.GetError(), HPE_OK) << http_errno_name(resp.GetError());
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotStatus, 1);
|
||||
ASSERT_EQ(gotUpgrade, 1);
|
||||
ASSERT_EQ(gotConnection, 1);
|
||||
ASSERT_EQ(gotAccept, 1);
|
||||
ASSERT_EQ(gotOpen, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketTest, CreateServerProtocol) {
|
||||
int gotProtocol = 0;
|
||||
int gotOpen = 0;
|
||||
|
||||
HttpParser resp{HttpParser::kResponse};
|
||||
resp.header.connect([&](StringRef name, StringRef value) {
|
||||
if (name.equals_lower("sec-websocket-protocol")) {
|
||||
++gotProtocol;
|
||||
ASSERT_EQ(value, "myProtocol");
|
||||
}
|
||||
});
|
||||
resp.headersComplete.connect([&](bool) { Finish(); });
|
||||
|
||||
serverPipe->Listen([&]() {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto ws = WebSocket::CreateServer(*conn, "foo", "13", "myProtocol");
|
||||
ws->open.connect([&](StringRef protocol) {
|
||||
++gotOpen;
|
||||
ASSERT_EQ(protocol, "myProtocol");
|
||||
});
|
||||
});
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
clientPipe->StartRead();
|
||||
clientPipe->data.connect([&](uv::Buffer& buf, size_t size) {
|
||||
resp.Execute(StringRef{buf.base, size});
|
||||
if (resp.HasError()) Finish();
|
||||
ASSERT_EQ(resp.GetError(), HPE_OK) << http_errno_name(resp.GetError());
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotProtocol, 1);
|
||||
ASSERT_EQ(gotOpen, 1);
|
||||
}
|
||||
|
||||
TEST_F(WebSocketTest, CreateServerBadVersion) {
|
||||
int gotStatus = 0;
|
||||
int gotVersion = 0;
|
||||
int gotUpgrade = 0;
|
||||
|
||||
HttpParser resp{HttpParser::kResponse};
|
||||
resp.status.connect([&](StringRef status) {
|
||||
++gotStatus;
|
||||
ASSERT_EQ(resp.GetStatusCode(), 426u) << "status: " << status;
|
||||
});
|
||||
resp.header.connect([&](StringRef name, StringRef value) {
|
||||
if (name.equals_lower("sec-websocket-version")) {
|
||||
++gotVersion;
|
||||
ASSERT_EQ(value, "13");
|
||||
} else if (name.equals_lower("upgrade")) {
|
||||
++gotUpgrade;
|
||||
ASSERT_EQ(value, "WebSocket");
|
||||
} else {
|
||||
FAIL() << "unexpected header " << name.str();
|
||||
}
|
||||
});
|
||||
resp.headersComplete.connect([&](bool) { Finish(); });
|
||||
|
||||
serverPipe->Listen([&] {
|
||||
auto conn = serverPipe->Accept();
|
||||
auto ws = WebSocket::CreateServer(*conn, "foo", "14");
|
||||
ws->open.connect([&](StringRef) {
|
||||
Finish();
|
||||
FAIL();
|
||||
});
|
||||
});
|
||||
clientPipe->Connect(pipeName, [&] {
|
||||
clientPipe->StartRead();
|
||||
clientPipe->data.connect([&](uv::Buffer& buf, size_t size) {
|
||||
resp.Execute(StringRef{buf.base, size});
|
||||
if (resp.HasError()) Finish();
|
||||
ASSERT_EQ(resp.GetError(), HPE_OK) << http_errno_name(resp.GetError());
|
||||
});
|
||||
});
|
||||
|
||||
loop->Run();
|
||||
|
||||
if (HasFatalFailure()) return;
|
||||
ASSERT_EQ(gotStatus, 1);
|
||||
ASSERT_EQ(gotVersion, 1);
|
||||
ASSERT_EQ(gotUpgrade, 1);
|
||||
}
|
||||
|
||||
} // namespace wpi
|
||||
73
wpiutil/src/test/native/cpp/WebSocketTest.h
Normal file
73
wpiutil/src/test/native/cpp/WebSocketTest.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "wpi/ArrayRef.h"
|
||||
#include "wpi/uv/Loop.h"
|
||||
#include "wpi/uv/Pipe.h"
|
||||
#include "wpi/uv/Timer.h"
|
||||
|
||||
namespace wpi {
|
||||
|
||||
class WebSocketTest : public ::testing::Test {
|
||||
public:
|
||||
static const char* pipeName;
|
||||
|
||||
static void SetUpTestCase();
|
||||
|
||||
WebSocketTest() {
|
||||
loop = uv::Loop::Create();
|
||||
clientPipe = uv::Pipe::Create(loop);
|
||||
serverPipe = uv::Pipe::Create(loop);
|
||||
|
||||
serverPipe->Bind(pipeName);
|
||||
|
||||
#if 0
|
||||
auto debugTimer = uv::Timer::Create(loop);
|
||||
debugTimer->timeout.connect([this] {
|
||||
std::printf("Active handles:\n");
|
||||
uv_print_active_handles(loop->GetRaw(), stdout);
|
||||
});
|
||||
debugTimer->Start(uv::Timer::Time{100}, uv::Timer::Time{100});
|
||||
debugTimer->Unreference();
|
||||
#endif
|
||||
|
||||
auto failTimer = uv::Timer::Create(loop);
|
||||
failTimer->timeout.connect([this] {
|
||||
loop->Stop();
|
||||
FAIL() << "loop failed to terminate";
|
||||
});
|
||||
failTimer->Start(uv::Timer::Time{1000});
|
||||
failTimer->Unreference();
|
||||
}
|
||||
|
||||
~WebSocketTest() { Finish(); }
|
||||
|
||||
void Finish() {
|
||||
loop->Walk([](uv::Handle& it) { it.Close(); });
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> BuildHeader(uint8_t opcode, bool fin,
|
||||
bool masking, uint64_t len);
|
||||
static std::vector<uint8_t> BuildMessage(uint8_t opcode, bool fin,
|
||||
bool masking,
|
||||
ArrayRef<uint8_t> data);
|
||||
static void AdjustMasking(MutableArrayRef<uint8_t> message);
|
||||
static const uint8_t testMask[4];
|
||||
|
||||
std::shared_ptr<uv::Loop> loop;
|
||||
std::shared_ptr<uv::Pipe> clientPipe;
|
||||
std::shared_ptr<uv::Pipe> serverPipe;
|
||||
};
|
||||
|
||||
} // namespace wpi
|
||||
Reference in New Issue
Block a user