http-over-capnp-test.c++ (17921B)
1 // Copyright (c) 2019 Cloudflare, Inc. and contributors 2 // Licensed under the MIT License: 3 // 4 // Permission is hereby granted, free of charge, to any person obtaining a copy 5 // of this software and associated documentation files (the "Software"), to deal 6 // in the Software without restriction, including without limitation the rights 7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 // copies of the Software, and to permit persons to whom the Software is 9 // furnished to do so, subject to the following conditions: 10 // 11 // The above copyright notice and this permission notice shall be included in 12 // all copies or substantial portions of the Software. 13 // 14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 // THE SOFTWARE. 21 22 #include "http-over-capnp.h" 23 #include <kj/test.h> 24 25 namespace capnp { 26 namespace { 27 28 KJ_TEST("KJ and RPC HTTP method enums match") { 29 #define EXPECT_MATCH(METHOD) \ 30 KJ_EXPECT(static_cast<uint>(kj::HttpMethod::METHOD) == \ 31 static_cast<uint>(capnp::HttpMethod::METHOD)); 32 33 KJ_HTTP_FOR_EACH_METHOD(EXPECT_MATCH); 34 #undef EXPECT_MATCH 35 } 36 37 // ======================================================================================= 38 39 kj::Promise<void> expectRead(kj::AsyncInputStream& in, kj::StringPtr expected) { 40 if (expected.size() == 0) return kj::READY_NOW; 41 42 auto buffer = kj::heapArray<char>(expected.size()); 43 44 auto promise = in.tryRead(buffer.begin(), 1, buffer.size()); 45 return promise.then(kj::mvCapture(buffer, [&in,expected](kj::Array<char> buffer, size_t amount) { 46 if (amount == 0) { 47 KJ_FAIL_ASSERT("expected data never sent", expected); 48 } 49 50 auto actual = buffer.slice(0, amount); 51 if (memcmp(actual.begin(), expected.begin(), actual.size()) != 0) { 52 KJ_FAIL_ASSERT("data from stream doesn't match expected", expected, actual); 53 } 54 55 return expectRead(in, expected.slice(amount)); 56 })); 57 } 58 59 enum Direction { 60 CLIENT_TO_SERVER, 61 SERVER_TO_CLIENT 62 }; 63 64 struct TestStep { 65 Direction direction; 66 kj::StringPtr send; 67 kj::StringPtr receive; 68 69 constexpr TestStep(Direction direction, kj::StringPtr send, kj::StringPtr receive) 70 : direction(direction), send(send), receive(receive) {} 71 constexpr TestStep(Direction direction, kj::StringPtr data) 72 : direction(direction), send(data), receive(data) {} 73 }; 74 75 constexpr TestStep TEST_STEPS[] = { 76 // Test basic request. 77 { 78 CLIENT_TO_SERVER, 79 80 "GET / HTTP/1.1\r\n" 81 "Host: example.com\r\n" 82 "\r\n"_kj, 83 }, 84 { 85 SERVER_TO_CLIENT, 86 87 "HTTP/1.1 200 OK\r\n" 88 "Content-Length: 3\r\n" 89 "\r\n" 90 "foo"_kj 91 }, 92 93 // Try PUT, vary path, vary status 94 { 95 CLIENT_TO_SERVER, 96 97 "PUT /foo/bar HTTP/1.1\r\n" 98 "Content-Length: 5\r\n" 99 "Host: example.com\r\n" 100 "\r\n" 101 "corge"_kj, 102 }, 103 { 104 SERVER_TO_CLIENT, 105 106 "HTTP/1.1 403 Unauthorized\r\n" 107 "Content-Length: 4\r\n" 108 "\r\n" 109 "nope"_kj 110 }, 111 112 // HEAD request 113 { 114 CLIENT_TO_SERVER, 115 116 "HEAD /foo/bar HTTP/1.1\r\n" 117 "Host: example.com\r\n" 118 "\r\n"_kj, 119 }, 120 { 121 SERVER_TO_CLIENT, 122 123 "HTTP/1.1 200 OK\r\n" 124 "Content-Length: 4\r\n" 125 "\r\n"_kj 126 }, 127 128 // Empty-body response 129 { 130 CLIENT_TO_SERVER, 131 132 "GET /foo/bar HTTP/1.1\r\n" 133 "Host: example.com\r\n" 134 "\r\n"_kj, 135 }, 136 { 137 SERVER_TO_CLIENT, 138 139 "HTTP/1.1 304 Not Modified\r\n" 140 "Server: foo\r\n" 141 "\r\n"_kj 142 }, 143 144 // Chonky body 145 { 146 CLIENT_TO_SERVER, 147 148 "POST / HTTP/1.1\r\n" 149 "Transfer-Encoding: chunked\r\n" 150 "Host: example.com\r\n" 151 "\r\n" 152 "3\r\n" 153 "foo\r\n" 154 "5\r\n" 155 "corge\r\n" 156 "0\r\n" 157 "\r\n"_kj, 158 }, 159 { 160 SERVER_TO_CLIENT, 161 162 "HTTP/1.1 200 OK\r\n" 163 "Transfer-Encoding: chunked\r\n" 164 "\r\n" 165 "6\r\n" 166 "barbaz\r\n" 167 "6\r\n" 168 "garply\r\n" 169 "0\r\n" 170 "\r\n"_kj 171 }, 172 173 // Streaming 174 { 175 CLIENT_TO_SERVER, 176 177 "POST / HTTP/1.1\r\n" 178 "Content-Length: 9\r\n" 179 "Host: example.com\r\n" 180 "\r\n"_kj, 181 }, 182 { 183 CLIENT_TO_SERVER, 184 185 "foo"_kj, 186 }, 187 { 188 CLIENT_TO_SERVER, 189 190 "bar"_kj, 191 }, 192 { 193 CLIENT_TO_SERVER, 194 195 "baz"_kj, 196 }, 197 { 198 SERVER_TO_CLIENT, 199 200 "HTTP/1.1 200 OK\r\n" 201 "Transfer-Encoding: chunked\r\n" 202 "\r\n"_kj, 203 }, 204 { 205 SERVER_TO_CLIENT, 206 207 "6\r\n" 208 "barbaz\r\n"_kj, 209 }, 210 { 211 SERVER_TO_CLIENT, 212 213 "6\r\n" 214 "garply\r\n"_kj, 215 }, 216 { 217 SERVER_TO_CLIENT, 218 219 "0\r\n" 220 "\r\n"_kj 221 }, 222 223 // Bidirectional. 224 { 225 CLIENT_TO_SERVER, 226 227 "POST / HTTP/1.1\r\n" 228 "Content-Length: 9\r\n" 229 "Host: example.com\r\n" 230 "\r\n"_kj, 231 }, 232 { 233 SERVER_TO_CLIENT, 234 235 "HTTP/1.1 200 OK\r\n" 236 "Transfer-Encoding: chunked\r\n" 237 "\r\n"_kj, 238 }, 239 { 240 CLIENT_TO_SERVER, 241 242 "foo"_kj, 243 }, 244 { 245 SERVER_TO_CLIENT, 246 247 "6\r\n" 248 "barbaz\r\n"_kj, 249 }, 250 { 251 CLIENT_TO_SERVER, 252 253 "bar"_kj, 254 }, 255 { 256 SERVER_TO_CLIENT, 257 258 "6\r\n" 259 "garply\r\n"_kj, 260 }, 261 { 262 CLIENT_TO_SERVER, 263 264 "baz"_kj, 265 }, 266 { 267 SERVER_TO_CLIENT, 268 269 "0\r\n" 270 "\r\n"_kj 271 }, 272 273 // Test headers being re-ordered by KJ. This isn't necessary behavior, but it does prove that 274 // we're not testing a pure streaming pass-through... 275 { 276 CLIENT_TO_SERVER, 277 278 "GET / HTTP/1.1\r\n" 279 "Host: example.com\r\n" 280 "Accept: text/html\r\n" 281 "Foo-Header: 123\r\n" 282 "User-Agent: kj\r\n" 283 "Accept-Language: en\r\n" 284 "\r\n"_kj, 285 286 "GET / HTTP/1.1\r\n" 287 "Host: example.com\r\n" 288 "Accept-Language: en\r\n" 289 "Accept: text/html\r\n" 290 "User-Agent: kj\r\n" 291 "Foo-Header: 123\r\n" 292 "\r\n"_kj 293 }, 294 { 295 SERVER_TO_CLIENT, 296 297 "HTTP/1.1 200 OK\r\n" 298 "Server: kj\r\n" 299 "Bar: 321\r\n" 300 "Content-Length: 3\r\n" 301 "\r\n" 302 "foo"_kj, 303 304 "HTTP/1.1 200 OK\r\n" 305 "Content-Length: 3\r\n" 306 "Server: kj\r\n" 307 "Bar: 321\r\n" 308 "\r\n" 309 "foo"_kj 310 }, 311 312 // We finish up a request with no response, to test cancellation. 313 { 314 CLIENT_TO_SERVER, 315 316 "GET / HTTP/1.1\r\n" 317 "Host: example.com\r\n" 318 "\r\n"_kj, 319 }, 320 }; 321 322 class OneConnectNetworkAddress final: public kj::NetworkAddress { 323 public: 324 OneConnectNetworkAddress(kj::Own<kj::AsyncIoStream> stream) 325 : stream(kj::mv(stream)) {} 326 327 kj::Promise<kj::Own<kj::AsyncIoStream>> connect() override { 328 auto result = KJ_ASSERT_NONNULL(kj::mv(stream)); 329 stream = nullptr; 330 return kj::mv(result); 331 } 332 333 kj::Own<kj::ConnectionReceiver> listen() override { KJ_UNIMPLEMENTED("test"); } 334 kj::Own<kj::NetworkAddress> clone() override { KJ_UNIMPLEMENTED("test"); } 335 kj::String toString() override { KJ_UNIMPLEMENTED("test"); } 336 337 private: 338 kj::Maybe<kj::Own<kj::AsyncIoStream>> stream; 339 }; 340 341 void runEndToEndTests(kj::Timer& timer, kj::HttpHeaderTable& headerTable, 342 HttpOverCapnpFactory& clientFactory, HttpOverCapnpFactory& serverFactory, 343 kj::WaitScope& waitScope) { 344 auto clientPipe = kj::newTwoWayPipe(); 345 auto serverPipe = kj::newTwoWayPipe(); 346 347 OneConnectNetworkAddress oneConnectAddr(kj::mv(serverPipe.ends[0])); 348 349 auto backHttp = kj::newHttpClient(timer, headerTable, oneConnectAddr); 350 auto backCapnp = serverFactory.kjToCapnp(kj::newHttpService(*backHttp)); 351 auto frontCapnp = clientFactory.capnpToKj(backCapnp); 352 kj::HttpServer frontKj(timer, headerTable, *frontCapnp); 353 auto listenTask = frontKj.listenHttp(kj::mv(clientPipe.ends[1])) 354 .eagerlyEvaluate([](kj::Exception&& e) { KJ_LOG(ERROR, e); }); 355 356 for (auto& step: TEST_STEPS) { 357 KJ_CONTEXT(step.send); 358 359 kj::AsyncOutputStream* out; 360 kj::AsyncInputStream* in; 361 362 switch (step.direction) { 363 case CLIENT_TO_SERVER: 364 out = clientPipe.ends[0]; 365 in = serverPipe.ends[1]; 366 break; 367 case SERVER_TO_CLIENT: 368 out = serverPipe.ends[1]; 369 in = clientPipe.ends[0]; 370 break; 371 } 372 373 auto writePromise = out->write(step.send.begin(), step.send.size()); 374 auto readPromise = expectRead(*in, step.receive); 375 if (!writePromise.poll(waitScope)) { 376 if (readPromise.poll(waitScope)) { 377 readPromise.wait(waitScope); 378 KJ_FAIL_ASSERT("write hung, read worked fine"); 379 } else { 380 KJ_FAIL_ASSERT("write and read both hung"); 381 } 382 } 383 384 writePromise.wait(waitScope); 385 KJ_ASSERT(readPromise.poll(waitScope), "read hung"); 386 readPromise.wait(waitScope); 387 } 388 389 // The last test message was a request with no response. If we now close the client end, this 390 // should propagate all the way through to close the server end! 391 clientPipe.ends[0] = nullptr; 392 auto lastRead = serverPipe.ends[1]->readAllText(); 393 KJ_ASSERT(lastRead.poll(waitScope), "last read hung"); 394 KJ_EXPECT(lastRead.wait(waitScope) == nullptr); 395 } 396 397 KJ_TEST("HTTP-over-Cap'n-Proto E2E, no path shortening") { 398 kj::EventLoop eventLoop; 399 kj::WaitScope waitScope(eventLoop); 400 kj::TimerImpl timer(kj::origin<kj::TimePoint>()); 401 402 ByteStreamFactory streamFactory1; 403 ByteStreamFactory streamFactory2; 404 kj::HttpHeaderTable::Builder tableBuilder; 405 HttpOverCapnpFactory factory1(streamFactory1, tableBuilder); 406 HttpOverCapnpFactory factory2(streamFactory2, tableBuilder); 407 auto headerTable = tableBuilder.build(); 408 409 runEndToEndTests(timer, *headerTable, factory1, factory2, waitScope); 410 } 411 412 KJ_TEST("HTTP-over-Cap'n-Proto E2E, with path shortening") { 413 kj::EventLoop eventLoop; 414 kj::WaitScope waitScope(eventLoop); 415 kj::TimerImpl timer(kj::origin<kj::TimePoint>()); 416 417 ByteStreamFactory streamFactory; 418 kj::HttpHeaderTable::Builder tableBuilder; 419 HttpOverCapnpFactory factory(streamFactory, tableBuilder); 420 auto headerTable = tableBuilder.build(); 421 422 runEndToEndTests(timer, *headerTable, factory, factory, waitScope); 423 } 424 425 KJ_TEST("HTTP-over-Cap'n-Proto 205 bug with HttpClientAdapter") { 426 // Test that a 205 with a hanging body doesn't prevent headers from being delivered. (This was 427 // a bug at one point. See, 205 responses are supposed to have empty bodies. But they must 428 // explicitly indicate an empty body. http-over-capnp, though, *assumed* an empty body when it 429 // saw a 205. But, on the client side, when HttpClientAdapter sees an empty body, it blocks 430 // delivery of the *headers* until the service promise resolves, in order to avoid prematurely 431 // cancelling the service. But on the server side, the service method is left hanging because 432 // it's waiting for the 205 to actually produce its empty body. If that didn't make any sense, 433 // consider yourself lucky.) 434 435 kj::EventLoop eventLoop; 436 kj::WaitScope waitScope(eventLoop); 437 kj::TimerImpl timer(kj::origin<kj::TimePoint>()); 438 439 ByteStreamFactory streamFactory; 440 kj::HttpHeaderTable::Builder tableBuilder; 441 HttpOverCapnpFactory factory(streamFactory, tableBuilder); 442 auto headerTable = tableBuilder.build(); 443 444 auto pipe = kj::newTwoWayPipe(); 445 446 OneConnectNetworkAddress oneConnectAddr(kj::mv(pipe.ends[0])); 447 448 auto backHttp = kj::newHttpClient(timer, *headerTable, oneConnectAddr); 449 auto backCapnp = factory.kjToCapnp(kj::newHttpService(*backHttp)); 450 auto frontCapnp = factory.capnpToKj(backCapnp); 451 452 auto frontClient = kj::newHttpClient(*frontCapnp); 453 454 auto req = frontClient->request(kj::HttpMethod::GET, "/", kj::HttpHeaders(*headerTable)); 455 456 { 457 auto readPromise = expectRead(*pipe.ends[1], "GET / HTTP/1.1\r\n\r\n"); 458 KJ_ASSERT(readPromise.poll(waitScope)); 459 readPromise.wait(waitScope); 460 } 461 462 KJ_EXPECT(!req.response.poll(waitScope)); 463 464 { 465 // A 205 response with no content-length or transfer-encoding is terminated by EOF (but also 466 // the body is required to be empty). We don't send the EOF yet, just the response line and 467 // empty headers. 468 kj::StringPtr resp = "HTTP/1.1 205 Reset Content\r\n\r\n"; 469 pipe.ends[1]->write(resp.begin(), resp.size()).wait(waitScope); 470 } 471 472 // On the client end, we should get a response now! 473 KJ_ASSERT(req.response.poll(waitScope)); 474 475 auto resp = req.response.wait(waitScope); 476 KJ_EXPECT(resp.statusCode == 205); 477 478 // But the body is still blocked. 479 auto promise = resp.body->readAllText(); 480 KJ_EXPECT(!promise.poll(waitScope)); 481 482 // OK now send the EOF it's waiting for. 483 pipe.ends[1]->shutdownWrite(); 484 485 // And now the body is unblocked. 486 KJ_ASSERT(promise.poll(waitScope)); 487 KJ_EXPECT(promise.wait(waitScope) == ""); 488 } 489 490 // ======================================================================================= 491 492 class WebSocketAccepter final: public kj::HttpService { 493 public: 494 WebSocketAccepter(kj::HttpHeaderTable& headerTable, 495 kj::Own<kj::PromiseFulfiller<kj::Own<kj::WebSocket>>> fulfiller, 496 kj::Promise<void> done) 497 : headerTable(headerTable), fulfiller(kj::mv(fulfiller)), done(kj::mv(done)) {} 498 499 kj::Promise<void> request( 500 kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers, 501 kj::AsyncInputStream& requestBody, Response& response) { 502 kj::HttpHeaders respHeaders(headerTable); 503 respHeaders.add("X-Foo", "bar"); 504 fulfiller->fulfill(response.acceptWebSocket(respHeaders)); 505 return kj::mv(done); 506 } 507 508 private: 509 kj::HttpHeaderTable& headerTable; 510 kj::Own<kj::PromiseFulfiller<kj::Own<kj::WebSocket>>> fulfiller; 511 kj::Promise<void> done; 512 }; 513 514 void runWebSocketTests(kj::HttpHeaderTable& headerTable, 515 HttpOverCapnpFactory& clientFactory, HttpOverCapnpFactory& serverFactory, 516 kj::WaitScope& waitScope) { 517 // We take a different approach here, because writing out raw WebSocket frames is a pain. 518 // It's easier to test WebSockets at the KJ API level. 519 520 auto wsPaf = kj::newPromiseAndFulfiller<kj::Own<kj::WebSocket>>(); 521 auto donePaf = kj::newPromiseAndFulfiller<void>(); 522 523 auto back = serverFactory.kjToCapnp(kj::heap<WebSocketAccepter>( 524 headerTable, kj::mv(wsPaf.fulfiller), kj::mv(donePaf.promise))); 525 auto front = clientFactory.capnpToKj(back); 526 auto client = kj::newHttpClient(*front); 527 528 auto resp = client->openWebSocket("/ws", kj::HttpHeaders(headerTable)).wait(waitScope); 529 KJ_ASSERT(resp.webSocketOrBody.is<kj::Own<kj::WebSocket>>()); 530 531 auto clientWs = kj::mv(resp.webSocketOrBody.get<kj::Own<kj::WebSocket>>()); 532 auto serverWs = wsPaf.promise.wait(waitScope); 533 534 { 535 auto promise = clientWs->send("foo"_kj); 536 auto message = serverWs->receive().wait(waitScope); 537 promise.wait(waitScope); 538 KJ_ASSERT(message.is<kj::String>()); 539 KJ_EXPECT(message.get<kj::String>() == "foo"); 540 } 541 542 { 543 auto promise = serverWs->send("bar"_kj.asBytes()); 544 auto message = clientWs->receive().wait(waitScope); 545 promise.wait(waitScope); 546 KJ_ASSERT(message.is<kj::Array<kj::byte>>()); 547 KJ_EXPECT(kj::str(message.get<kj::Array<kj::byte>>().asChars()) == "bar"); 548 } 549 550 { 551 auto promise = clientWs->close(1234, "baz"_kj); 552 auto message = serverWs->receive().wait(waitScope); 553 promise.wait(waitScope); 554 KJ_ASSERT(message.is<kj::WebSocket::Close>()); 555 KJ_EXPECT(message.get<kj::WebSocket::Close>().code == 1234); 556 KJ_EXPECT(message.get<kj::WebSocket::Close>().reason == "baz"); 557 } 558 559 { 560 auto promise = serverWs->disconnect(); 561 auto receivePromise = clientWs->receive(); 562 KJ_EXPECT(receivePromise.poll(waitScope)); 563 KJ_EXPECT_THROW(DISCONNECTED, receivePromise.wait(waitScope)); 564 promise.wait(waitScope); 565 } 566 } 567 568 KJ_TEST("HTTP-over-Cap'n Proto WebSocket, no path shortening") { 569 kj::EventLoop eventLoop; 570 kj::WaitScope waitScope(eventLoop); 571 572 ByteStreamFactory streamFactory1; 573 ByteStreamFactory streamFactory2; 574 kj::HttpHeaderTable::Builder tableBuilder; 575 HttpOverCapnpFactory factory1(streamFactory1, tableBuilder); 576 HttpOverCapnpFactory factory2(streamFactory2, tableBuilder); 577 auto headerTable = tableBuilder.build(); 578 579 runWebSocketTests(*headerTable, factory1, factory2, waitScope); 580 } 581 582 KJ_TEST("HTTP-over-Cap'n Proto WebSocket, with path shortening") { 583 kj::EventLoop eventLoop; 584 kj::WaitScope waitScope(eventLoop); 585 586 ByteStreamFactory streamFactory; 587 kj::HttpHeaderTable::Builder tableBuilder; 588 HttpOverCapnpFactory factory(streamFactory, tableBuilder); 589 auto headerTable = tableBuilder.build(); 590 591 runWebSocketTests(*headerTable, factory, factory, waitScope); 592 } 593 594 // ======================================================================================= 595 // bug fixes 596 597 class HangingHttpService final: public kj::HttpService { 598 public: 599 HangingHttpService(bool& called, bool& destroyed) 600 : called(called), destroyed(destroyed) {} 601 ~HangingHttpService() noexcept(false) { 602 destroyed = true; 603 } 604 605 kj::Promise<void> request( 606 kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers, 607 kj::AsyncInputStream& requestBody, Response& response) { 608 called = true; 609 return kj::NEVER_DONE; 610 } 611 612 private: 613 bool& called; 614 bool& destroyed; 615 }; 616 617 KJ_TEST("HttpService isn't destroyed while call outstanding") { 618 kj::EventLoop eventLoop; 619 kj::WaitScope waitScope(eventLoop); 620 621 ByteStreamFactory streamFactory; 622 kj::HttpHeaderTable::Builder tableBuilder; 623 HttpOverCapnpFactory factory(streamFactory, tableBuilder); 624 auto headerTable = tableBuilder.build(); 625 626 bool called = false; 627 bool destroyed = false; 628 auto service = factory.kjToCapnp(kj::heap<HangingHttpService>(called, destroyed)); 629 630 KJ_EXPECT(!called); 631 KJ_EXPECT(!destroyed); 632 633 auto req = service.startRequestRequest(); 634 auto httpReq = req.initRequest(); 635 httpReq.setMethod(capnp::HttpMethod::GET); 636 httpReq.setUrl("/"); 637 auto serverContext = req.send().wait(waitScope).getContext(); 638 service = nullptr; 639 640 auto promise = serverContext.whenResolved(); 641 KJ_EXPECT(!promise.poll(waitScope)); 642 643 KJ_EXPECT(called); 644 KJ_EXPECT(!destroyed); 645 } 646 647 } // namespace 648 } // namespace capnp