|
17 | 17 | #include <boost/corosio/tls/tls_stream.hpp> |
18 | 18 | #include <boost/corosio/test/socket_pair.hpp> |
19 | 19 | #include <boost/capy/buffers.hpp> |
| 20 | +#include <boost/capy/cond.hpp> |
20 | 21 | #include <boost/capy/ex/run_async.hpp> |
21 | 22 | #include <boost/capy/task.hpp> |
22 | 23 |
|
@@ -446,6 +447,218 @@ run_tls_test_fail( |
446 | 447 | s2.close(); |
447 | 448 | } |
448 | 449 |
|
| 450 | +/** Run a TLS shutdown test with graceful close_notify. |
| 451 | +
|
| 452 | + Tests that one side can initiate TLS shutdown (sends close_notify) |
| 453 | + and the other side receives EOF. Uses unidirectional shutdown to |
| 454 | + avoid deadlock in single-threaded io_context. |
| 455 | +
|
| 456 | + Note: TLS shutdown in a single-threaded context can deadlock when both |
| 457 | + sides wait for each other. We use a timeout to detect and recover from |
| 458 | + potential deadlocks. |
| 459 | + |
| 460 | + @param ioc The io_context to use |
| 461 | + @param client_ctx TLS context for the client |
| 462 | + @param server_ctx TLS context for the server |
| 463 | + @param make_client Factory: (io_stream&, context) -> TLS stream |
| 464 | + @param make_server Factory: (io_stream&, context) -> TLS stream |
| 465 | +*/ |
| 466 | +template<typename ClientStreamFactory, typename ServerStreamFactory> |
| 467 | +void |
| 468 | +run_tls_shutdown_test( |
| 469 | + io_context& ioc, |
| 470 | + context client_ctx, |
| 471 | + context server_ctx, |
| 472 | + ClientStreamFactory make_client, |
| 473 | + ServerStreamFactory make_server ) |
| 474 | +{ |
| 475 | + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); |
| 476 | + |
| 477 | + auto client = make_client( s1, client_ctx ); |
| 478 | + auto server = make_server( s2, server_ctx ); |
| 479 | + |
| 480 | + // Handshake phase |
| 481 | + auto client_hs = [&client]() -> capy::task<> |
| 482 | + { |
| 483 | + auto [ec] = co_await client.handshake( tls_stream::client ); |
| 484 | + BOOST_TEST( !ec ); |
| 485 | + }; |
| 486 | + |
| 487 | + auto server_hs = [&server]() -> capy::task<> |
| 488 | + { |
| 489 | + auto [ec] = co_await server.handshake( tls_stream::server ); |
| 490 | + BOOST_TEST( !ec ); |
| 491 | + }; |
| 492 | + |
| 493 | + capy::run_async( ioc.get_executor() )( client_hs() ); |
| 494 | + capy::run_async( ioc.get_executor() )( server_hs() ); |
| 495 | + |
| 496 | + ioc.run(); |
| 497 | + ioc.restart(); |
| 498 | + |
| 499 | + // Data transfer phase |
| 500 | + auto transfer_task = [&client, &server]() -> capy::task<> |
| 501 | + { |
| 502 | + co_await test_stream( client, server ); |
| 503 | + }; |
| 504 | + capy::run_async( ioc.get_executor() )( transfer_task() ); |
| 505 | + |
| 506 | + ioc.run(); |
| 507 | + ioc.restart(); |
| 508 | + |
| 509 | + // Shutdown phase with timeout protection |
| 510 | + bool shutdown_done = false; |
| 511 | + bool read_done = false; |
| 512 | + |
| 513 | + auto client_shutdown = [&client, &shutdown_done]() -> capy::task<> |
| 514 | + { |
| 515 | + auto [ec] = co_await client.shutdown(); |
| 516 | + shutdown_done = true; |
| 517 | + // Shutdown may return success, canceled, or stream_truncated |
| 518 | + BOOST_TEST( !ec || ec == capy::cond::stream_truncated || |
| 519 | + ec == capy::cond::canceled ); |
| 520 | + }; |
| 521 | + |
| 522 | + auto server_read_eof = [&server, &read_done]() -> capy::task<> |
| 523 | + { |
| 524 | + char buf[32]; |
| 525 | + auto [ec, n] = co_await server.read_some( |
| 526 | + capy::mutable_buffer( buf, sizeof( buf ) ) ); |
| 527 | + read_done = true; |
| 528 | + // Should get EOF, stream_truncated, or canceled |
| 529 | + BOOST_TEST( ec == capy::cond::eof || ec == capy::cond::stream_truncated || |
| 530 | + ec == capy::cond::canceled ); |
| 531 | + }; |
| 532 | + |
| 533 | + // Timeout to prevent deadlock |
| 534 | + timer timeout( ioc ); |
| 535 | + timeout.expires_after( std::chrono::milliseconds( 500 ) ); |
| 536 | + auto timeout_task = [&timeout, &s1, &s2, &shutdown_done, &read_done]() -> capy::task<> |
| 537 | + { |
| 538 | + (void)shutdown_done; |
| 539 | + (void)read_done; |
| 540 | + auto [ec] = co_await timeout.wait(); |
| 541 | + if( !ec ) |
| 542 | + { |
| 543 | + // Timer expired - cancel pending operations (check if still open) |
| 544 | + if( s1.is_open() ) { s1.cancel(); s1.close(); } |
| 545 | + if( s2.is_open() ) { s2.cancel(); s2.close(); } |
| 546 | + } |
| 547 | + }; |
| 548 | + |
| 549 | + capy::run_async( ioc.get_executor() )( client_shutdown() ); |
| 550 | + capy::run_async( ioc.get_executor() )( server_read_eof() ); |
| 551 | + capy::run_async( ioc.get_executor() )( timeout_task() ); |
| 552 | + |
| 553 | + ioc.run(); |
| 554 | + |
| 555 | + timeout.cancel(); |
| 556 | + if( s1.is_open() ) s1.close(); |
| 557 | + if( s2.is_open() ) s2.close(); |
| 558 | +} |
| 559 | + |
| 560 | +/** Run a test for stream truncation (socket close without TLS shutdown). |
| 561 | +
|
| 562 | + Tests that when one side closes the underlying socket without |
| 563 | + performing TLS shutdown, the other side receives stream_truncated. |
| 564 | + |
| 565 | + @param ioc The io_context to use |
| 566 | + @param client_ctx TLS context for the client |
| 567 | + @param server_ctx TLS context for the server |
| 568 | + @param make_client Factory: (io_stream&, context) -> TLS stream |
| 569 | + @param make_server Factory: (io_stream&, context) -> TLS stream |
| 570 | +*/ |
| 571 | +template<typename ClientStreamFactory, typename ServerStreamFactory> |
| 572 | +void |
| 573 | +run_tls_truncation_test( |
| 574 | + io_context& ioc, |
| 575 | + context client_ctx, |
| 576 | + context server_ctx, |
| 577 | + ClientStreamFactory make_client, |
| 578 | + ServerStreamFactory make_server ) |
| 579 | +{ |
| 580 | + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); |
| 581 | + |
| 582 | + auto client = make_client( s1, client_ctx ); |
| 583 | + auto server = make_server( s2, server_ctx ); |
| 584 | + |
| 585 | + // Handshake phase |
| 586 | + auto client_hs = [&client]() -> capy::task<> |
| 587 | + { |
| 588 | + auto [ec] = co_await client.handshake( tls_stream::client ); |
| 589 | + BOOST_TEST( !ec ); |
| 590 | + }; |
| 591 | + |
| 592 | + auto server_hs = [&server]() -> capy::task<> |
| 593 | + { |
| 594 | + auto [ec] = co_await server.handshake( tls_stream::server ); |
| 595 | + BOOST_TEST( !ec ); |
| 596 | + }; |
| 597 | + |
| 598 | + capy::run_async( ioc.get_executor() )( client_hs() ); |
| 599 | + capy::run_async( ioc.get_executor() )( server_hs() ); |
| 600 | + |
| 601 | + ioc.run(); |
| 602 | + ioc.restart(); |
| 603 | + |
| 604 | + // Data transfer phase |
| 605 | + auto transfer_task = [&client, &server]() -> capy::task<> |
| 606 | + { |
| 607 | + co_await test_stream( client, server ); |
| 608 | + }; |
| 609 | + capy::run_async( ioc.get_executor() )( transfer_task() ); |
| 610 | + |
| 611 | + ioc.run(); |
| 612 | + ioc.restart(); |
| 613 | + |
| 614 | + // Truncation test with timeout protection |
| 615 | + bool read_done = false; |
| 616 | + |
| 617 | + auto client_close = [&s1]() -> capy::task<> |
| 618 | + { |
| 619 | + // Close underlying socket without TLS shutdown |
| 620 | + s1.close(); |
| 621 | + co_return; |
| 622 | + }; |
| 623 | + |
| 624 | + auto server_read_truncated = [&server, &read_done]() -> capy::task<> |
| 625 | + { |
| 626 | + char buf[32]; |
| 627 | + auto [ec, n] = co_await server.read_some( |
| 628 | + capy::mutable_buffer( buf, sizeof( buf ) ) ); |
| 629 | + read_done = true; |
| 630 | + // Should get stream_truncated, eof, or canceled |
| 631 | + BOOST_TEST( ec == capy::cond::stream_truncated || |
| 632 | + ec == capy::cond::eof || |
| 633 | + ec == capy::cond::canceled ); |
| 634 | + }; |
| 635 | + |
| 636 | + // Timeout to prevent deadlock |
| 637 | + timer timeout( ioc ); |
| 638 | + timeout.expires_after( std::chrono::milliseconds( 500 ) ); |
| 639 | + auto timeout_task = [&timeout, &s1, &s2, &read_done]() -> capy::task<> |
| 640 | + { |
| 641 | + (void)read_done; |
| 642 | + auto [ec] = co_await timeout.wait(); |
| 643 | + if( !ec ) |
| 644 | + { |
| 645 | + // Timer expired - cancel pending operations (check if still open) |
| 646 | + if( s1.is_open() ) { s1.cancel(); s1.close(); } |
| 647 | + if( s2.is_open() ) { s2.cancel(); s2.close(); } |
| 648 | + } |
| 649 | + }; |
| 650 | + |
| 651 | + capy::run_async( ioc.get_executor() )( client_close() ); |
| 652 | + capy::run_async( ioc.get_executor() )( server_read_truncated() ); |
| 653 | + capy::run_async( ioc.get_executor() )( timeout_task() ); |
| 654 | + |
| 655 | + ioc.run(); |
| 656 | + |
| 657 | + timeout.cancel(); |
| 658 | + if( s1.is_open() ) s1.close(); |
| 659 | + if( s2.is_open() ) s2.close(); |
| 660 | +} |
| 661 | + |
449 | 662 | } // namespace test |
450 | 663 | } // namespace tls |
451 | 664 | } // namespace corosio |
|
0 commit comments