Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 1 | // Copyright (c) FIRST and other WPILib contributors. |
| 2 | // Open Source Software; you can modify and/or share it under the terms of |
| 3 | // the WPILib BSD license file in the root directory of this project. |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 4 | |
| 5 | #include "wpi/WebSocket.h" // NOLINT(build/include_order) |
| 6 | |
| 7 | #include "WebSocketTest.h" |
| 8 | #include "wpi/Base64.h" |
| 9 | #include "wpi/HttpParser.h" |
| 10 | #include "wpi/SmallString.h" |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 11 | #include "wpi/StringExtras.h" |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 12 | #include "wpi/raw_uv_ostream.h" |
| 13 | #include "wpi/sha1.h" |
| 14 | |
| 15 | namespace wpi { |
| 16 | |
| 17 | class WebSocketClientTest : public WebSocketTest { |
| 18 | public: |
| 19 | WebSocketClientTest() { |
| 20 | // Bare bones server |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 21 | req.header.connect([this](std::string_view name, std::string_view value) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 22 | // save key (required for valid response) |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 23 | if (equals_lower(name, "sec-websocket-key")) { |
| 24 | clientKey = value; |
| 25 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 26 | }); |
| 27 | req.headersComplete.connect([this](bool) { |
| 28 | // send response |
| 29 | SmallVector<uv::Buffer, 4> bufs; |
| 30 | raw_uv_ostream os{bufs, 4096}; |
| 31 | os << "HTTP/1.1 101 Switching Protocols\r\n"; |
| 32 | os << "Upgrade: websocket\r\n"; |
| 33 | os << "Connection: Upgrade\r\n"; |
| 34 | |
| 35 | // accept hash |
| 36 | SHA1 hash; |
| 37 | hash.Update(clientKey); |
| 38 | hash.Update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 39 | if (mockBadAccept) { |
| 40 | hash.Update("1"); |
| 41 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 42 | SmallString<64> hashBuf; |
| 43 | SmallString<64> acceptBuf; |
| 44 | os << "Sec-WebSocket-Accept: " |
| 45 | << Base64Encode(hash.RawFinal(hashBuf), acceptBuf) << "\r\n"; |
| 46 | |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 47 | if (!mockProtocol.empty()) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 48 | os << "Sec-WebSocket-Protocol: " << mockProtocol << "\r\n"; |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 49 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 50 | |
| 51 | os << "\r\n"; |
| 52 | |
| 53 | conn->Write(bufs, [](auto bufs, uv::Error) { |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 54 | for (auto& buf : bufs) { |
| 55 | buf.Deallocate(); |
| 56 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 57 | }); |
| 58 | |
| 59 | serverHeadersDone = true; |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 60 | if (connected) { |
| 61 | connected(); |
| 62 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 63 | }); |
| 64 | |
| 65 | serverPipe->Listen([this] { |
| 66 | conn = serverPipe->Accept(); |
| 67 | conn->StartRead(); |
| 68 | conn->data.connect([this](uv::Buffer& buf, size_t size) { |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 69 | std::string_view data{buf.base, size}; |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 70 | if (!serverHeadersDone) { |
| 71 | data = req.Execute(data); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 72 | if (req.HasError()) { |
| 73 | Finish(); |
| 74 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 75 | ASSERT_EQ(req.GetError(), HPE_OK) << http_errno_name(req.GetError()); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 76 | if (data.empty()) { |
| 77 | return; |
| 78 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 79 | } |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 80 | wireData.insert(wireData.end(), data.begin(), data.end()); |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 81 | }); |
| 82 | conn->end.connect([this] { Finish(); }); |
| 83 | }); |
| 84 | } |
| 85 | |
| 86 | bool mockBadAccept = false; |
| 87 | std::vector<uint8_t> wireData; |
| 88 | std::shared_ptr<uv::Pipe> conn; |
| 89 | HttpParser req{HttpParser::kRequest}; |
| 90 | SmallString<64> clientKey; |
| 91 | std::string mockProtocol; |
| 92 | bool serverHeadersDone = false; |
| 93 | std::function<void()> connected; |
| 94 | }; |
| 95 | |
| 96 | TEST_F(WebSocketClientTest, Open) { |
| 97 | int gotOpen = 0; |
| 98 | |
| 99 | clientPipe->Connect(pipeName, [&] { |
| 100 | auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 101 | ws->closed.connect([&](uint16_t code, std::string_view reason) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 102 | Finish(); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 103 | if (code != 1005 && code != 1006) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 104 | FAIL() << "Code: " << code << " Reason: " << reason; |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 105 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 106 | }); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 107 | ws->open.connect([&](std::string_view protocol) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 108 | ++gotOpen; |
| 109 | Finish(); |
| 110 | ASSERT_TRUE(protocol.empty()); |
| 111 | }); |
| 112 | }); |
| 113 | |
| 114 | loop->Run(); |
| 115 | |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 116 | if (HasFatalFailure()) { |
| 117 | return; |
| 118 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 119 | ASSERT_EQ(gotOpen, 1); |
| 120 | } |
| 121 | |
| 122 | TEST_F(WebSocketClientTest, BadAccept) { |
| 123 | int gotClosed = 0; |
| 124 | |
| 125 | mockBadAccept = true; |
| 126 | |
| 127 | clientPipe->Connect(pipeName, [&] { |
| 128 | auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 129 | ws->closed.connect([&](uint16_t code, std::string_view msg) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 130 | Finish(); |
| 131 | ++gotClosed; |
| 132 | ASSERT_EQ(code, 1002) << "Message: " << msg; |
| 133 | }); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 134 | ws->open.connect([&](std::string_view protocol) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 135 | Finish(); |
| 136 | FAIL() << "Got open"; |
| 137 | }); |
| 138 | }); |
| 139 | |
| 140 | loop->Run(); |
| 141 | |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 142 | if (HasFatalFailure()) { |
| 143 | return; |
| 144 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 145 | ASSERT_EQ(gotClosed, 1); |
| 146 | } |
| 147 | |
| 148 | TEST_F(WebSocketClientTest, ProtocolGood) { |
| 149 | int gotOpen = 0; |
| 150 | |
| 151 | mockProtocol = "myProtocol"; |
| 152 | |
| 153 | clientPipe->Connect(pipeName, [&] { |
| 154 | auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName, |
| 155 | {"myProtocol", "myProtocol2"}); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 156 | ws->closed.connect([&](uint16_t code, std::string_view msg) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 157 | Finish(); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 158 | if (code != 1005 && code != 1006) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 159 | FAIL() << "Code: " << code << "Message: " << msg; |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 160 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 161 | }); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 162 | ws->open.connect([&](std::string_view protocol) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 163 | ++gotOpen; |
| 164 | Finish(); |
| 165 | ASSERT_EQ(protocol, "myProtocol"); |
| 166 | }); |
| 167 | }); |
| 168 | |
| 169 | loop->Run(); |
| 170 | |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 171 | if (HasFatalFailure()) { |
| 172 | return; |
| 173 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 174 | ASSERT_EQ(gotOpen, 1); |
| 175 | } |
| 176 | |
| 177 | TEST_F(WebSocketClientTest, ProtocolRespNotReq) { |
| 178 | int gotClosed = 0; |
| 179 | |
| 180 | mockProtocol = "myProtocol"; |
| 181 | |
| 182 | clientPipe->Connect(pipeName, [&] { |
| 183 | auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 184 | ws->closed.connect([&](uint16_t code, std::string_view msg) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 185 | Finish(); |
| 186 | ++gotClosed; |
| 187 | ASSERT_EQ(code, 1003) << "Message: " << msg; |
| 188 | }); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 189 | ws->open.connect([&](std::string_view protocol) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 190 | Finish(); |
| 191 | FAIL() << "Got open"; |
| 192 | }); |
| 193 | }); |
| 194 | |
| 195 | loop->Run(); |
| 196 | |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 197 | if (HasFatalFailure()) { |
| 198 | return; |
| 199 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 200 | ASSERT_EQ(gotClosed, 1); |
| 201 | } |
| 202 | |
| 203 | TEST_F(WebSocketClientTest, ProtocolReqNotResp) { |
| 204 | int gotClosed = 0; |
| 205 | |
| 206 | clientPipe->Connect(pipeName, [&] { |
| 207 | auto ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName, |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 208 | {{"myProtocol"}}); |
| 209 | ws->closed.connect([&](uint16_t code, std::string_view msg) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 210 | Finish(); |
| 211 | ++gotClosed; |
| 212 | ASSERT_EQ(code, 1002) << "Message: " << msg; |
| 213 | }); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 214 | ws->open.connect([&](std::string_view protocol) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 215 | Finish(); |
| 216 | FAIL() << "Got open"; |
| 217 | }); |
| 218 | }); |
| 219 | |
| 220 | loop->Run(); |
| 221 | |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 222 | if (HasFatalFailure()) { |
| 223 | return; |
| 224 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 225 | ASSERT_EQ(gotClosed, 1); |
| 226 | } |
| 227 | |
| 228 | // |
| 229 | // Send and receive data. Most of these cases are tested in |
| 230 | // WebSocketServerTest, so only spot check differences like masking. |
| 231 | // |
| 232 | |
| 233 | class WebSocketClientDataTest : public WebSocketClientTest, |
| 234 | public ::testing::WithParamInterface<size_t> { |
| 235 | public: |
| 236 | WebSocketClientDataTest() { |
| 237 | clientPipe->Connect(pipeName, [&] { |
| 238 | ws = WebSocket::CreateClient(*clientPipe, "/test", pipeName); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 239 | if (setupWebSocket) { |
| 240 | setupWebSocket(); |
| 241 | } |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 242 | }); |
| 243 | } |
| 244 | |
| 245 | std::function<void()> setupWebSocket; |
| 246 | std::shared_ptr<WebSocket> ws; |
| 247 | }; |
| 248 | |
| 249 | INSTANTIATE_TEST_SUITE_P(WebSocketClientDataTests, WebSocketClientDataTest, |
| 250 | ::testing::Values(0, 1, 125, 126, 65535, 65536)); |
| 251 | |
| 252 | TEST_P(WebSocketClientDataTest, SendBinary) { |
| 253 | int gotCallback = 0; |
| 254 | std::vector<uint8_t> data(GetParam(), 0x03u); |
| 255 | setupWebSocket = [&] { |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 256 | ws->open.connect([&](std::string_view) { |
| 257 | ws->SendBinary({{data}}, [&](auto bufs, uv::Error) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 258 | ++gotCallback; |
| 259 | ws->Terminate(); |
| 260 | ASSERT_FALSE(bufs.empty()); |
| 261 | ASSERT_EQ(bufs[0].base, reinterpret_cast<const char*>(data.data())); |
| 262 | }); |
| 263 | }); |
| 264 | }; |
| 265 | |
| 266 | loop->Run(); |
| 267 | |
| 268 | auto expectData = BuildMessage(0x02, true, true, data); |
| 269 | AdjustMasking(wireData); |
| 270 | ASSERT_EQ(wireData, expectData); |
| 271 | ASSERT_EQ(gotCallback, 1); |
| 272 | } |
| 273 | |
| 274 | TEST_P(WebSocketClientDataTest, ReceiveBinary) { |
| 275 | int gotCallback = 0; |
| 276 | std::vector<uint8_t> data(GetParam(), 0x03u); |
| 277 | setupWebSocket = [&] { |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 278 | ws->binary.connect([&](auto inData, bool fin) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 279 | ++gotCallback; |
| 280 | ws->Terminate(); |
| 281 | ASSERT_TRUE(fin); |
| 282 | std::vector<uint8_t> recvData{inData.begin(), inData.end()}; |
| 283 | ASSERT_EQ(data, recvData); |
| 284 | }); |
| 285 | }; |
| 286 | auto message = BuildMessage(0x02, true, false, data); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 287 | connected = [&] { conn->Write({{message}}, [&](auto bufs, uv::Error) {}); }; |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 288 | |
| 289 | loop->Run(); |
| 290 | |
| 291 | ASSERT_EQ(gotCallback, 1); |
| 292 | } |
| 293 | |
| 294 | // |
| 295 | // The client must close the connection if a masked frame is received. |
| 296 | // |
| 297 | |
| 298 | TEST_P(WebSocketClientDataTest, ReceiveMasked) { |
| 299 | int gotCallback = 0; |
| 300 | std::vector<uint8_t> data(GetParam(), ' '); |
| 301 | setupWebSocket = [&] { |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 302 | ws->text.connect([&](std::string_view, bool) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 303 | ws->Terminate(); |
| 304 | FAIL() << "Should not have gotten masked message"; |
| 305 | }); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 306 | ws->closed.connect([&](uint16_t code, std::string_view reason) { |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 307 | ++gotCallback; |
| 308 | ASSERT_EQ(code, 1002) << "reason: " << reason; |
| 309 | }); |
| 310 | }; |
| 311 | auto message = BuildMessage(0x01, true, true, data); |
Austin Schuh | 812d0d1 | 2021-11-04 20:16:48 -0700 | [diff] [blame^] | 312 | connected = [&] { conn->Write({{message}}, [&](auto bufs, uv::Error) {}); }; |
Brian Silverman | 8fce748 | 2020-01-05 13:18:21 -0800 | [diff] [blame] | 313 | |
| 314 | loop->Run(); |
| 315 | |
| 316 | ASSERT_EQ(gotCallback, 1); |
| 317 | } |
| 318 | |
| 319 | } // namespace wpi |