From f861147d7da361ae42696cc45ae679e26f70f883 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 23 Jan 2026 01:06:02 +0000 Subject: [PATCH 1/9] Enhance TLS testing with SNI, mTLS, certificate chain, and cancellation tests API additions: - Add context::set_servername_callback() for server-side SNI handling - Server can now accept/reject connections based on client's requested hostname Bug fixes: - Fix WolfSSL shutdown: remove double wolfSSL_get_error() call that consumed error state, add socket read after flushing close_notify to match OpenSSL's bidirectional shutdown semantics - Fix stop token propagation in both OpenSSL and WolfSSL stream implementations New tests: - testStopTokenCancellation: cancel during handshake, read, and write operations - testSocketErrorPropagation: socket.cancel() and connection reset handling - testCertificateValidation: untrusted CA and expired certificate rejection - testSni: hostname verification with correct and incorrect hostnames - testSniCallback: server-side SNI callback accepting/rejecting connections - testMtls: mutual TLS success, missing client cert, and wrong CA scenarios - testCertificateChain: intermediate certificate chain handling Test infrastructure: - Add embedded test certificates for chain, mTLS, and expired cert scenarios - Add context factory functions for various test configurations - Add deterministic test helpers using socket I/O synchronization - Failsafe timeouts now report test failures via BOOST_TEST(!failsafe_hit) - Replace timer-based delays with socket read synchronization for determinism - Re-enable testFailureCases (was disabled due to cancellation issues) Test count increased from ~100 to 798 assertions. --- .github/workflows/ci.yml | 120 +- build/Jamfile | 19 +- include/boost/corosio/tls/context.hpp | 1783 ++++++++++--------- src/corosio/src/tls/context.cpp | 604 +++---- src/corosio/src/tls/detail/context_impl.hpp | 5 + src/openssl/src/openssl_stream.cpp | 185 +- src/wolfssl/src/wolfssl_stream.cpp | 176 +- test/unit/Jamfile | 29 +- test/unit/tls/cross_ssl_stream.cpp | 362 ++-- test/unit/tls/wolfssl_stream.cpp | 475 +++-- 10 files changed, 2198 insertions(+), 1560 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fefc2a82..f21ba94c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ env: UBSAN_OPTIONS: "print_stacktrace=1" DEBIAN_FRONTEND: "noninteractive" TZ: "Europe/London" + # Enable verbose TLS test logging for debugging CI timeout issues + COROSIO_TLS_TEST_VERBOSE: "1" + COROSIO_TLS_DEBUG: "1" jobs: # Self-hosted runner selection is disabled to allow re-running individual @@ -290,6 +293,8 @@ jobs: apt-get: >- ${{ matrix.install }} build-essential + libssl-dev + curl zip unzip tar pkg-config ${{ matrix.x86 && '' || '' }} - name: Clone Capy @@ -350,6 +355,103 @@ jobs: # Patch boost-root with capy dependency cp -r "$workspace_root"/capy-root "libs/capy" + # Use vcpkg for TLS libraries + # Windows: OpenSSL + WolfSSL from vcpkg + # Linux: system OpenSSL + vcpkg WolfSSL only (system wolfssl package lacks -fPIC) + - name: Create vcpkg.json (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + cat > corosio-root/vcpkg.json << 'EOF' + { + "name": "boost-corosio-deps", + "version": "1.0.0", + "dependencies": ["openssl", "wolfssl"] + } + EOF + + - name: Create vcpkg.json (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + cat > corosio-root/vcpkg.json << 'EOF' + { + "name": "boost-corosio-deps", + "version": "1.0.0", + "dependencies": ["wolfssl"] + } + EOF + + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: ${{ github.workspace }}/vcpkg + vcpkgGitCommitId: bd52fac7114fdaa2208de8dd1227212a6683e562 + vcpkgJsonGlob: '**/corosio-root/vcpkg.json' + runVcpkgInstall: true + + - name: Set vcpkg paths (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + vcpkg_installed="${{ github.workspace }}/vcpkg/vcpkg_installed/x64-windows" + + # Debug: show what was installed + echo "Checking vcpkg installed packages:" + ls -la "${vcpkg_installed}/" || true + ls -la "${vcpkg_installed}/include/" || true + ls -la "${vcpkg_installed}/lib/" || true + + # For CMake (vcpkg toolchain file handles finding packages) + echo "CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" >> $GITHUB_ENV + + # For B2 (uses explicit paths) + echo "WOLFSSL_INCLUDE=${vcpkg_installed}/include" >> $GITHUB_ENV + echo "WOLFSSL_LIBRARY_PATH=${vcpkg_installed}/lib" >> $GITHUB_ENV + echo "OPENSSL_INCLUDE=${vcpkg_installed}/include" >> $GITHUB_ENV + echo "OPENSSL_LIBRARY_PATH=${vcpkg_installed}/lib" >> $GITHUB_ENV + + - name: Set vcpkg paths (Linux) + if: runner.os == 'Linux' + id: vcpkg-paths-linux + shell: bash + run: | + # lukka/run-vcpkg sets VCPKG_INSTALLED_DIR with a UUID-based path + # Use that directly instead of trying to find it + echo "Debug: VCPKG_INSTALLED_DIR=${VCPKG_INSTALLED_DIR:-not set}" + + if [ -n "${VCPKG_INSTALLED_DIR}" ] && [ -d "${VCPKG_INSTALLED_DIR}/x64-linux" ]; then + vcpkg_installed="${VCPKG_INSTALLED_DIR}/x64-linux" + else + # Fallback: try to find it + vcpkg_installed=$(find "${GITHUB_WORKSPACE}" -type d -path "*/vcpkg_installed/x64-linux" 2>/dev/null | head -1) + if [ -z "${vcpkg_installed}" ]; then + vcpkg_installed=$(find "/__w" -type d -path "*/vcpkg_installed/x64-linux" 2>/dev/null | head -1) + fi + fi + + if [ -z "${vcpkg_installed}" ] || [ ! -d "${vcpkg_installed}" ]; then + echo "ERROR: Could not find vcpkg installed directory!" + echo "VCPKG_INSTALLED_DIR=${VCPKG_INSTALLED_DIR:-not set}" + echo "GITHUB_WORKSPACE=${GITHUB_WORKSPACE}" + find "${GITHUB_WORKSPACE}" -type d -name "vcpkg_installed" 2>/dev/null || true + find "/__w" -type d -name "vcpkg_installed" 2>/dev/null || true + exit 1 + fi + + echo "Using vcpkg_installed: ${vcpkg_installed}" + ls -la "${vcpkg_installed}/" || true + ls -la "${vcpkg_installed}/include/" || true + ls -la "${vcpkg_installed}/lib/" || true + + # Output for CMake steps (to pass WolfSSL paths explicitly) + echo "wolfssl_include=${vcpkg_installed}/include" >> $GITHUB_OUTPUT + echo "wolfssl_library=${vcpkg_installed}/lib/libwolfssl.a" >> $GITHUB_OUTPUT + + # For B2 - WolfSSL from vcpkg, OpenSSL from system + echo "WOLFSSL_INCLUDE=${vcpkg_installed}/include" >> $GITHUB_ENV + echo "WOLFSSL_LIBRARY_PATH=${vcpkg_installed}/lib" >> $GITHUB_ENV + - name: Boost B2 Workflow uses: alandefreitas/cpp-actions/b2-workflow@v1.9.0 if: ${{ !matrix.coverage }} @@ -392,6 +494,11 @@ jobs: extra-args: | -D Boost_VERBOSE=ON -D BOOST_INCLUDE_LIBRARIES="${{ steps.patch.outputs.module }}" + ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + # Windows: CMAKE_TOOLCHAIN_FILE is set via environment variable in "Set vcpkg paths" step + # The action picks it up automatically. Don't pass via extra-args (format() corrupts backslashes) + toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} package: false package-artifact: false ref-source-dir: boost-root/libs/corosio @@ -425,6 +532,9 @@ jobs: extra-args: | -D BOOST_CI_INSTALL_TEST=ON -D CMAKE_PREFIX_PATH=${{ steps.patch.outputs.workspace_root }}/.local + ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} ref-source-dir: boost-root/libs/corosio trace-commands: true @@ -445,7 +555,11 @@ jobs: shared: ${{ matrix.shared }} install: false cmake-version: '>=3.15' - extra-args: -D BOOST_CI_INSTALL_TEST=OFF + extra-args: | + -D BOOST_CI_INSTALL_TEST=OFF + ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} ref-source-dir: boost-root/libs/corosio/test/cmake_test - name: Root Project CMake Workflow @@ -467,6 +581,10 @@ jobs: cxxflags: ${{ matrix.cxxflags }} shared: ${{ matrix.shared }} cmake-version: '>=3.20' + extra-args: | + ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} package: false package-artifact: false ref-source-dir: boost-root diff --git a/build/Jamfile b/build/Jamfile index 4a337d28..ae2781e1 100644 --- a/build/Jamfile +++ b/build/Jamfile @@ -52,6 +52,22 @@ lib boost_corosio ../include ; +# OpenSSL +using openssl ; + +alias corosio_openssl_sources : [ glob-tree-ex src/openssl/src : *.cpp ] ; + +lib boost_corosio_openssl + : corosio_openssl_sources + : requirements + /boost/corosio//boost_corosio + ../src/corosio + [ ac.check-library /openssl//ssl : /openssl//ssl /openssl//crypto : no ] + : usage-requirements + /boost/corosio//boost_corosio + BOOST_COROSIO_HAS_OPENSSL + ; + # WolfSSL using wolfssl ; @@ -61,10 +77,11 @@ lib boost_corosio_wolfssl : corosio_wolfssl_sources : requirements /boost/corosio//boost_corosio + ../src/corosio [ ac.check-library /wolfssl//wolfssl : /wolfssl//wolfssl : no ] : usage-requirements /boost/corosio//boost_corosio BOOST_COROSIO_HAS_WOLFSSL ; -boost-install boost_corosio boost_corosio_wolfssl ; +boost-install boost_corosio boost_corosio_openssl boost_corosio_wolfssl ; diff --git a/include/boost/corosio/tls/context.hpp b/include/boost/corosio/tls/context.hpp index 2a1591e1..2e23b378 100644 --- a/include/boost/corosio/tls/context.hpp +++ b/include/boost/corosio/tls/context.hpp @@ -1,867 +1,916 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_TLS_CONTEXT_HPP -#define BOOST_COROSIO_TLS_CONTEXT_HPP - -#include -#include - -#include -#include -#include - -namespace boost { -namespace corosio { -namespace tls { - -//------------------------------------------------------------------------------ -// -// Enumerations -// -//------------------------------------------------------------------------------ - -/** TLS handshake role. - - Specifies whether to perform the TLS handshake as a client or server. - - @see stream::handshake -*/ -enum class role -{ - /// Perform handshake as the connecting client. - client, - - /// Perform handshake as the accepting server. - server -}; - -/** TLS protocol version. - - Specifies the minimum or maximum TLS protocol version to use - for connections. Only modern, secure versions are supported. - - @see context::set_min_protocol_version - @see context::set_max_protocol_version -*/ -enum class version -{ - /// TLS 1.2 (RFC 5246). - tls_1_2, - - /// TLS 1.3 (RFC 8446). - tls_1_3 -}; - -/** Certificate and key file format. - - Specifies the encoding format for certificate and key data. - - @see context::use_certificate - @see context::use_private_key -*/ -enum class file_format -{ - /// PEM format (Base64-encoded with header/footer lines). - pem, - - /// DER format (raw ASN.1 binary encoding). - der -}; - -/** Peer certificate verification mode. - - Controls how the TLS implementation verifies the peer's - certificate during the handshake. - - @see context::set_verify_mode -*/ -enum class verify_mode -{ - /// Do not request or verify the peer certificate. - none, - - /// Request and verify the peer certificate if presented. - peer, - - /// Require and verify the peer certificate (fail if not presented). - require_peer -}; - -/** Certificate revocation checking policy. - - Controls how certificate revocation status is checked during - verification. - - @see context::set_revocation_policy -*/ -enum class revocation_policy -{ - /// Do not check revocation status. - disabled, - - /// Check revocation but allow connection if status is unknown. - soft_fail, - - /// Require successful revocation check (fail if status is unknown). - hard_fail -}; - -/** Purpose for password callback invocation. - - Indicates whether the password is needed for reading (decrypting) - or writing (encrypting) key material. - - @see context::set_password_callback -*/ -enum class password_purpose -{ - /// Password needed to decrypt/read protected key material. - for_reading, - - /// Password needed to encrypt/write protected key material. - for_writing -}; - -class context; - -namespace detail { -struct context_data; -context_data const& -get_context_data( context const& ) noexcept; -} // namespace detail - -/** A portable TLS context for certificate and settings storage. - - The `context` class provides a backend-agnostic interface for - configuring TLS connections. It stores credentials (certificates and - private keys), trust anchors, protocol settings, and verification - options that are used when establishing TLS connections. - - This class is a shared handle to an opaque implementation. Copies - share the same underlying state. This allows contexts to be passed - by value and shared across multiple TLS streams. - - This class abstracts the configuration phase of TLS across multiple - backend implementations (OpenSSL, WolfSSL, mbedTLS, Schannel, etc.), - allowing portable code that works regardless of which TLS library - is linked. - - @par Modification After Stream Creation - - Modifying a context after a TLS stream has been created from it - results in undefined behavior. The context's configuration is - captured when the first stream is constructed, and subsequent - modifications are not reflected in existing or new streams - sharing the context. - - If different configurations are needed, create separate context - objects. - - @par Thread Safety - - Distinct objects: Safe. - - Shared objects: Unsafe. A context must not be modified while - any thread is creating streams from it. - - @par Example - @code - // Create a client context with system trust anchors - corosio::tls::context ctx; - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( corosio::tls::verify_mode::peer ).value(); - ctx.set_hostname( "example.com" ); - - // Use with a TLS stream - corosio::tls::stream secure( sock, ctx ); - co_await secure.handshake( corosio::tls::role::client ); - @endcode - - @see role -*/ -class BOOST_COROSIO_DECL context -{ - struct impl; - std::shared_ptr impl_; - - friend - detail::context_data const& - detail::get_context_data( context const& ) noexcept; - -public: - /** Construct a default TLS context. - - Creates a context with default settings suitable for TLS 1.2 - and TLS 1.3 connections. No certificates or trust anchors are - loaded; call the appropriate methods to configure credentials - and verification. - - @par Example - @code - corosio::tls::context ctx; - @endcode - */ - context(); - - /** Copy constructor. - - Creates a new handle that shares ownership of the underlying - TLS context state with `other`. - - @param other The context to copy from. - */ - context( context const& other ) = default; - - /** Copy assignment operator. - - Releases the current context's shared ownership and acquires - shared ownership of `other`'s underlying state. - - @param other The context to copy from. - - @return Reference to this context. - */ - context& operator=( context const& other ) = default; - - /** Move constructor. - - Transfers ownership of the TLS context from another instance. - After the move, `other` is in a valid but empty state. - - @param other The context to move from. - */ - context( context&& other ) noexcept = default; - - /** Move assignment operator. - - Releases the current context's shared ownership and transfers - ownership from another instance. After the move, `other` is - in a valid but empty state. - - @param other The context to move from. - - @return Reference to this context. - */ - context& operator=( context&& other ) noexcept = default; - - /** Destructor. - - Releases this handle's shared ownership of the underlying - context. The context state is destroyed when the last handle - is released. - */ - ~context() = default; - - //-------------------------------------------------------------------------- - // - // Credential Loading - // - //-------------------------------------------------------------------------- - - /** Load the entity certificate from a memory buffer. - - Sets the certificate that identifies this endpoint to the peer. - For servers, this is the server certificate. For clients using - mutual TLS, this is the client certificate. - - The certificate must match the private key loaded via - `use_private_key()` or `use_private_key_file()`. - - @param certificate The certificate data. - - @param format The encoding format of the certificate data. - - @return Success, or an error if the certificate could not be parsed - or is invalid. - - @see use_certificate_file - @see use_private_key - */ - system::result - use_certificate( - std::string_view certificate, - file_format format ); - - /** Load the entity certificate from a file. - - Sets the certificate that identifies this endpoint to the peer. - For servers, this is the server certificate. For clients using - mutual TLS, this is the client certificate. - - @param filename Path to the certificate file. - - @param format The encoding format of the file. - - @return Success, or an error if the file could not be read or the - certificate is invalid. - - @par Example - @code - ctx.use_certificate_file( "server.crt", tls::file_format::pem ).value(); - @endcode - - @see use_certificate - @see use_private_key_file - */ - system::result - use_certificate_file( - std::string_view filename, - file_format format ); - - /** Load a certificate chain from a memory buffer. - - Loads the entity certificate followed by intermediate CA certificates. - The chain should be ordered from leaf to root (excluding the root). - This is the typical format for PEM certificate bundles. - - @param chain The certificate chain data in PEM format (concatenated - certificates). - - @return Success, or an error if the chain could not be parsed. - - @see use_certificate_chain_file - */ - system::result - use_certificate_chain( std::string_view chain ); - - /** Load a certificate chain from a file. - - Loads the entity certificate followed by intermediate CA certificates - from a PEM file. The file should contain concatenated PEM certificates - ordered from leaf to root (excluding the root). - - @param filename Path to the certificate chain file. - - @return Success, or an error if the file could not be read or parsed. - - @par Example - @code - // Load certificate chain (cert + intermediates) - ctx.use_certificate_chain_file( "fullchain.pem" ).value(); - @endcode - - @see use_certificate_chain - */ - system::result - use_certificate_chain_file( std::string_view filename ); - - /** Load the private key from a memory buffer. - - Sets the private key corresponding to the entity certificate. - The key must match the certificate loaded via `use_certificate()` - or `use_certificate_chain()`. - - If the key is encrypted, set a password callback via - `set_password_callback()` before calling this function. - - @param private_key The private key data. - - @param format The encoding format of the key data. - - @return Success, or an error if the key could not be parsed, - is encrypted without a password callback, or doesn't match - the certificate. - - @see use_private_key_file - @see set_password_callback - */ - system::result - use_private_key( - std::string_view private_key, - file_format format ); - - /** Load the private key from a file. - - Sets the private key corresponding to the entity certificate. - The key must match the certificate loaded via `use_certificate_file()` - or `use_certificate_chain_file()`. - - If the key file is encrypted, set a password callback via - `set_password_callback()` before calling this function. - - @param filename Path to the private key file. - - @param format The encoding format of the file. - - @return Success, or an error if the file could not be read, - the key is invalid, or it doesn't match the certificate. - - @par Example - @code - ctx.use_private_key_file( "server.key", tls::file_format::pem ).value(); - @endcode - - @see use_private_key - @see set_password_callback - */ - system::result - use_private_key_file( - std::string_view filename, - file_format format ); - - /** Load credentials from a PKCS#12 bundle in memory. - - PKCS#12 (also known as PFX) is a binary format that bundles a - certificate, private key, and optionally intermediate certificates - into a single password-protected file. - - @param data The PKCS#12 bundle data. - - @param passphrase The password protecting the bundle. - - @return Success, or an error if the bundle could not be parsed - or the passphrase is incorrect. - - @see use_pkcs12_file - */ - system::result - use_pkcs12( - std::string_view data, - std::string_view passphrase ); - - /** Load credentials from a PKCS#12 file. - - PKCS#12 (also known as PFX) is a binary format that bundles a - certificate, private key, and optionally intermediate certificates - into a single password-protected file. This is common on Windows - and for certificates exported from browsers. - - @param filename Path to the PKCS#12 file. - - @param passphrase The password protecting the file. - - @return Success, or an error if the file could not be read, - parsed, or the passphrase is incorrect. - - @par Example - @code - ctx.use_pkcs12_file( "credentials.pfx", "secret" ).value(); - @endcode - - @see use_pkcs12 - */ - system::result - use_pkcs12_file( - std::string_view filename, - std::string_view passphrase ); - - //-------------------------------------------------------------------------- - // - // Trust Anchors - // - //-------------------------------------------------------------------------- - - /** Add a certificate authority for peer verification. - - Adds a single CA certificate to the trust store used for verifying - peer certificates. Call this multiple times to add multiple CAs, - or use `load_verify_file()` for a bundle. - - @param ca The CA certificate data in PEM format. - - @return Success, or an error if the certificate could not be parsed. - - @see load_verify_file - @see set_default_verify_paths - */ - system::result - add_certificate_authority( std::string_view ca ); - - /** Load CA certificates from a file. - - Loads one or more CA certificates from a PEM file. The file may - contain multiple concatenated PEM certificates. - - @param filename Path to a PEM file containing CA certificates. - - @return Success, or an error if the file could not be read or parsed. - - @par Example - @code - // Load a custom CA bundle - ctx.load_verify_file( "/etc/ssl/certs/ca-certificates.crt" ).value(); - @endcode - - @see add_certificate_authority - @see add_verify_path - */ - system::result - load_verify_file( std::string_view filename ); - - /** Add a directory of CA certificates for verification. - - Adds a directory containing CA certificate files. Each file must - contain a single certificate in PEM format, named using the - subject name hash (as generated by `openssl rehash` or - `c_rehash`). - - @param path Path to the directory containing hashed CA certificates. - - @return Success, or an error if the directory is invalid. - - @par Example - @code - ctx.add_verify_path( "/etc/ssl/certs" ).value(); - @endcode - - @see load_verify_file - @see set_default_verify_paths - */ - system::result - add_verify_path( std::string_view path ); - - /** Use the system default CA certificate store. - - Configures the context to use the operating system's default - trust store for peer certificate verification. This is the - recommended approach for HTTPS clients connecting to public - servers. - - On different platforms this uses: - - Linux: `/etc/ssl/certs` or distribution-specific paths - - macOS: System Keychain - - Windows: Windows Certificate Store - - @return Success, or an error if the system store could not be loaded. - - @par Example - @code - // Trust the same CAs as the system - ctx.set_default_verify_paths().value(); - @endcode - - @see load_verify_file - @see add_verify_path - */ - system::result - set_default_verify_paths(); - - //-------------------------------------------------------------------------- - // - // Protocol Configuration - // - //-------------------------------------------------------------------------- - - /** Set the minimum TLS protocol version. - - Connections will reject protocol versions older than this. - The default allows TLS 1.2 and newer. - - @param v The minimum protocol version to accept. - - @return Success, or an error if the version is not supported - by the backend. - - @par Example - @code - // Require TLS 1.3 minimum - ctx.set_min_protocol_version( tls::version::tls_1_3 ).value(); - @endcode - - @see set_max_protocol_version - */ - system::result - set_min_protocol_version( version v ); - - /** Set the maximum TLS protocol version. - - Connections will not negotiate protocol versions newer than this. - The default allows the newest supported version. - - @param v The maximum protocol version to accept. - - @return Success, or an error if the version is not supported - by the backend. - - @see set_min_protocol_version - */ - system::result - set_max_protocol_version( version v ); - - /** Set the allowed cipher suites. - - Configures which cipher suites may be used for connections. - The format is backend-specific but typically follows OpenSSL - cipher list syntax. - - @param ciphers The cipher suite specification string. - - @return Success, or an error if the cipher string is invalid. - - @par Example - @code - // TLS 1.2 cipher suites (OpenSSL format) - ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ).value(); - @endcode - - @note For TLS 1.3, use `set_ciphersuites_tls13()` on backends - that distinguish between TLS 1.2 and 1.3 cipher configuration. - */ - system::result - set_ciphersuites( std::string_view ciphers ); - - /** Set the ALPN protocol list. - - Configures Application-Layer Protocol Negotiation (ALPN) for - the connection. ALPN is used to negotiate which application - protocol to use over the TLS connection (e.g., "h2" for HTTP/2, - "http/1.1" for HTTP/1.1). - - The protocols are tried in preference order (first = highest). - - @param protocols Ordered list of protocol identifiers. - - @return Success, or an error if ALPN configuration fails. - - @par Example - @code - // Prefer HTTP/2, fall back to HTTP/1.1 - ctx.set_alpn( { "h2", "http/1.1" } ).value(); - @endcode - */ - system::result - set_alpn( std::initializer_list protocols ); - - //-------------------------------------------------------------------------- - // - // Certificate Verification - // - //-------------------------------------------------------------------------- - - /** Set the peer certificate verification mode. - - Controls whether and how peer certificates are verified during - the TLS handshake. - - @param mode The verification mode to use. - - @return Success, or an error if the mode could not be set. - - @par Example - @code - // Verify peer certificate (typical for clients) - ctx.set_verify_mode( tls::verify_mode::peer ).value(); - - // Require client certificate (server-side mTLS) - ctx.set_verify_mode( tls::verify_mode::require_peer ).value(); - @endcode - - @see verify_mode - */ - system::result - set_verify_mode( verify_mode mode ); - - /** Set the maximum certificate chain verification depth. - - Limits how many intermediate certificates can appear between - the peer certificate and a trusted root. The default is - typically 100, which is sufficient for most certificate chains. - - @param depth Maximum number of intermediate certificates allowed. - - @return Success, or an error if the depth is invalid. - */ - system::result - set_verify_depth( int depth ); - - /** Set a custom certificate verification callback. - - Installs a callback that is invoked during certificate chain - verification. The callback can perform additional validation - beyond the standard checks and can override verification - results. - - The callback receives the verification result so far and - information about the certificate being verified. Return - `true` to accept the certificate, `false` to reject. - - @tparam Callback A callable with signature - `bool( bool preverified, verify_context& ctx )`. - - @param callback The verification callback. - - @return Success, or an error if the callback could not be set. - - @note The `verify_context` type provides access to the - certificate and chain information. Its exact interface - depends on the TLS backend. - */ - template - system::result - set_verify_callback( Callback callback ); - - /** Set the expected server hostname for verification. - - For client connections, sets the hostname that the server - certificate must match. This enables: - - 1. SNI (Server Name Indication) — tells the server which - certificate to present (for virtual hosting) - 2. Hostname verification — validates the certificate's - Subject Alternative Name or Common Name matches - - @param hostname The expected server hostname. - - @par Example - @code - ctx.set_hostname( "api.example.com" ); - @endcode - - @note This is typically required for HTTPS clients to ensure - they're connecting to the intended server. - */ - void - set_hostname( std::string_view hostname ); - - //-------------------------------------------------------------------------- - // - // Revocation Checking - // - //-------------------------------------------------------------------------- - - /** Add a Certificate Revocation List from memory. - - Adds a CRL to the verification store for checking whether - certificates have been revoked. CRLs are typically fetched - from the URLs in a certificate's CRL Distribution Points - extension. - - @param crl The CRL data in DER or PEM format. - - @return Success, or an error if the CRL could not be parsed. - - @see add_crl_file - @see set_revocation_policy - */ - system::result - add_crl( std::string_view crl ); - - /** Add a Certificate Revocation List from a file. - - Adds a CRL to the verification store for checking whether - certificates have been revoked. - - @param filename Path to a CRL file (DER or PEM format). - - @return Success, or an error if the file could not be read - or the CRL is invalid. - - @par Example - @code - ctx.add_crl_file( "issuer.crl" ).value(); - @endcode - - @see add_crl - @see set_revocation_policy - */ - system::result - add_crl_file( std::string_view filename ); - - /** Set the OCSP staple response for server-side stapling. - - For servers, provides a pre-fetched OCSP response to send - to clients during the handshake. This proves the server's - certificate hasn't been revoked without requiring the client - to contact the OCSP responder. - - The OCSP response must be periodically refreshed (typically - every few hours to days) before it expires. - - @param response The DER-encoded OCSP response. - - @return Success, or an error if the response is invalid. - - @note This is a server-side operation. Clients use - `set_require_ocsp_staple()` to require stapled responses. - */ - system::result - set_ocsp_staple( std::string_view response ); - - /** Require OCSP stapling from the server. - - For clients, requires the server to provide a stapled OCSP - response proving its certificate hasn't been revoked. If - the server doesn't provide a stapled response, the handshake - fails. - - @param require Whether to require OCSP stapling. - - @note Not all servers support OCSP stapling. Enable this only - when connecting to servers known to support it. - */ - void - set_require_ocsp_staple( bool require ); - - /** Set the certificate revocation checking policy. - - Controls how certificate revocation status is checked during - verification. This affects both CRL and OCSP checking. - - @param policy The revocation checking policy. - - @par Example - @code - // Require successful revocation check - ctx.set_revocation_policy( tls::revocation_policy::hard_fail ); - - // Check but allow unknown status - ctx.set_revocation_policy( tls::revocation_policy::soft_fail ); - @endcode - - @see revocation_policy - @see add_crl - */ - void - set_revocation_policy( revocation_policy policy ); - - //-------------------------------------------------------------------------- - // - // Password Handling - // - //-------------------------------------------------------------------------- - - /** Set the password callback for encrypted keys. - - Installs a callback that provides passwords for encrypted - private keys and PKCS#12 files. The callback is invoked when - loading encrypted key material. - - @tparam Callback A callable with signature - `std::string( std::size_t max_length, password_purpose purpose )`. - - @param callback The password callback. It receives the maximum - password length and the purpose (reading or writing), and - returns the password string. - - @par Example - @code - ctx.set_password_callback( - []( std::size_t max_len, tls::password_purpose purpose ) - { - // In practice, prompt user or read from secure storage - return std::string( "my-key-password" ); - }); - - // Now load encrypted key - ctx.use_private_key_file( "encrypted.key", tls::file_format::pem ).value(); - @endcode - - @see password_purpose - */ - template - void - set_password_callback( Callback callback ); -}; - -} // namespace tls -} // namespace corosio -} // namespace boost - -#endif +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_TLS_CONTEXT_HPP +#define BOOST_COROSIO_TLS_CONTEXT_HPP + +#include +#include + +#include +#include +#include + +namespace boost { +namespace corosio { +namespace tls { + +//------------------------------------------------------------------------------ +// +// Enumerations +// +//------------------------------------------------------------------------------ + +/** TLS handshake role. + + Specifies whether to perform the TLS handshake as a client or server. + + @see stream::handshake +*/ +enum class role +{ + /// Perform handshake as the connecting client. + client, + + /// Perform handshake as the accepting server. + server +}; + +/** TLS protocol version. + + Specifies the minimum or maximum TLS protocol version to use + for connections. Only modern, secure versions are supported. + + @see context::set_min_protocol_version + @see context::set_max_protocol_version +*/ +enum class version +{ + /// TLS 1.2 (RFC 5246). + tls_1_2, + + /// TLS 1.3 (RFC 8446). + tls_1_3 +}; + +/** Certificate and key file format. + + Specifies the encoding format for certificate and key data. + + @see context::use_certificate + @see context::use_private_key +*/ +enum class file_format +{ + /// PEM format (Base64-encoded with header/footer lines). + pem, + + /// DER format (raw ASN.1 binary encoding). + der +}; + +/** Peer certificate verification mode. + + Controls how the TLS implementation verifies the peer's + certificate during the handshake. + + @see context::set_verify_mode +*/ +enum class verify_mode +{ + /// Do not request or verify the peer certificate. + none, + + /// Request and verify the peer certificate if presented. + peer, + + /// Require and verify the peer certificate (fail if not presented). + require_peer +}; + +/** Certificate revocation checking policy. + + Controls how certificate revocation status is checked during + verification. + + @see context::set_revocation_policy +*/ +enum class revocation_policy +{ + /// Do not check revocation status. + disabled, + + /// Check revocation but allow connection if status is unknown. + soft_fail, + + /// Require successful revocation check (fail if status is unknown). + hard_fail +}; + +/** Purpose for password callback invocation. + + Indicates whether the password is needed for reading (decrypting) + or writing (encrypting) key material. + + @see context::set_password_callback +*/ +enum class password_purpose +{ + /// Password needed to decrypt/read protected key material. + for_reading, + + /// Password needed to encrypt/write protected key material. + for_writing +}; + +class context; + +namespace detail { +struct context_data; +context_data const& +get_context_data( context const& ) noexcept; +} // namespace detail + +/** A portable TLS context for certificate and settings storage. + + The `context` class provides a backend-agnostic interface for + configuring TLS connections. It stores credentials (certificates and + private keys), trust anchors, protocol settings, and verification + options that are used when establishing TLS connections. + + This class is a shared handle to an opaque implementation. Copies + share the same underlying state. This allows contexts to be passed + by value and shared across multiple TLS streams. + + This class abstracts the configuration phase of TLS across multiple + backend implementations (OpenSSL, WolfSSL, mbedTLS, Schannel, etc.), + allowing portable code that works regardless of which TLS library + is linked. + + @par Modification After Stream Creation + + Modifying a context after a TLS stream has been created from it + results in undefined behavior. The context's configuration is + captured when the first stream is constructed, and subsequent + modifications are not reflected in existing or new streams + sharing the context. + + If different configurations are needed, create separate context + objects. + + @par Thread Safety + + Distinct objects: Safe. + + Shared objects: Unsafe. A context must not be modified while + any thread is creating streams from it. + + @par Example + @code + // Create a client context with system trust anchors + corosio::tls::context ctx; + ctx.set_default_verify_paths().value(); + ctx.set_verify_mode( corosio::tls::verify_mode::peer ).value(); + ctx.set_hostname( "example.com" ); + + // Use with a TLS stream + corosio::tls::stream secure( sock, ctx ); + co_await secure.handshake( corosio::tls::role::client ); + @endcode + + @see role +*/ +class BOOST_COROSIO_DECL context +{ + struct impl; + std::shared_ptr impl_; + + friend + detail::context_data const& + detail::get_context_data( context const& ) noexcept; + +public: + /** Construct a default TLS context. + + Creates a context with default settings suitable for TLS 1.2 + and TLS 1.3 connections. No certificates or trust anchors are + loaded; call the appropriate methods to configure credentials + and verification. + + @par Example + @code + corosio::tls::context ctx; + @endcode + */ + context(); + + /** Copy constructor. + + Creates a new handle that shares ownership of the underlying + TLS context state with `other`. + + @param other The context to copy from. + */ + context( context const& other ) = default; + + /** Copy assignment operator. + + Releases the current context's shared ownership and acquires + shared ownership of `other`'s underlying state. + + @param other The context to copy from. + + @return Reference to this context. + */ + context& operator=( context const& other ) = default; + + /** Move constructor. + + Transfers ownership of the TLS context from another instance. + After the move, `other` is in a valid but empty state. + + @param other The context to move from. + */ + context( context&& other ) noexcept = default; + + /** Move assignment operator. + + Releases the current context's shared ownership and transfers + ownership from another instance. After the move, `other` is + in a valid but empty state. + + @param other The context to move from. + + @return Reference to this context. + */ + context& operator=( context&& other ) noexcept = default; + + /** Destructor. + + Releases this handle's shared ownership of the underlying + context. The context state is destroyed when the last handle + is released. + */ + ~context() = default; + + //-------------------------------------------------------------------------- + // + // Credential Loading + // + //-------------------------------------------------------------------------- + + /** Load the entity certificate from a memory buffer. + + Sets the certificate that identifies this endpoint to the peer. + For servers, this is the server certificate. For clients using + mutual TLS, this is the client certificate. + + The certificate must match the private key loaded via + `use_private_key()` or `use_private_key_file()`. + + @param certificate The certificate data. + + @param format The encoding format of the certificate data. + + @return Success, or an error if the certificate could not be parsed + or is invalid. + + @see use_certificate_file + @see use_private_key + */ + system::result + use_certificate( + std::string_view certificate, + file_format format ); + + /** Load the entity certificate from a file. + + Sets the certificate that identifies this endpoint to the peer. + For servers, this is the server certificate. For clients using + mutual TLS, this is the client certificate. + + @param filename Path to the certificate file. + + @param format The encoding format of the file. + + @return Success, or an error if the file could not be read or the + certificate is invalid. + + @par Example + @code + ctx.use_certificate_file( "server.crt", tls::file_format::pem ).value(); + @endcode + + @see use_certificate + @see use_private_key_file + */ + system::result + use_certificate_file( + std::string_view filename, + file_format format ); + + /** Load a certificate chain from a memory buffer. + + Loads the entity certificate followed by intermediate CA certificates. + The chain should be ordered from leaf to root (excluding the root). + This is the typical format for PEM certificate bundles. + + @param chain The certificate chain data in PEM format (concatenated + certificates). + + @return Success, or an error if the chain could not be parsed. + + @see use_certificate_chain_file + */ + system::result + use_certificate_chain( std::string_view chain ); + + /** Load a certificate chain from a file. + + Loads the entity certificate followed by intermediate CA certificates + from a PEM file. The file should contain concatenated PEM certificates + ordered from leaf to root (excluding the root). + + @param filename Path to the certificate chain file. + + @return Success, or an error if the file could not be read or parsed. + + @par Example + @code + // Load certificate chain (cert + intermediates) + ctx.use_certificate_chain_file( "fullchain.pem" ).value(); + @endcode + + @see use_certificate_chain + */ + system::result + use_certificate_chain_file( std::string_view filename ); + + /** Load the private key from a memory buffer. + + Sets the private key corresponding to the entity certificate. + The key must match the certificate loaded via `use_certificate()` + or `use_certificate_chain()`. + + If the key is encrypted, set a password callback via + `set_password_callback()` before calling this function. + + @param private_key The private key data. + + @param format The encoding format of the key data. + + @return Success, or an error if the key could not be parsed, + is encrypted without a password callback, or doesn't match + the certificate. + + @see use_private_key_file + @see set_password_callback + */ + system::result + use_private_key( + std::string_view private_key, + file_format format ); + + /** Load the private key from a file. + + Sets the private key corresponding to the entity certificate. + The key must match the certificate loaded via `use_certificate_file()` + or `use_certificate_chain_file()`. + + If the key file is encrypted, set a password callback via + `set_password_callback()` before calling this function. + + @param filename Path to the private key file. + + @param format The encoding format of the file. + + @return Success, or an error if the file could not be read, + the key is invalid, or it doesn't match the certificate. + + @par Example + @code + ctx.use_private_key_file( "server.key", tls::file_format::pem ).value(); + @endcode + + @see use_private_key + @see set_password_callback + */ + system::result + use_private_key_file( + std::string_view filename, + file_format format ); + + /** Load credentials from a PKCS#12 bundle in memory. + + PKCS#12 (also known as PFX) is a binary format that bundles a + certificate, private key, and optionally intermediate certificates + into a single password-protected file. + + @param data The PKCS#12 bundle data. + + @param passphrase The password protecting the bundle. + + @return Success, or an error if the bundle could not be parsed + or the passphrase is incorrect. + + @see use_pkcs12_file + */ + system::result + use_pkcs12( + std::string_view data, + std::string_view passphrase ); + + /** Load credentials from a PKCS#12 file. + + PKCS#12 (also known as PFX) is a binary format that bundles a + certificate, private key, and optionally intermediate certificates + into a single password-protected file. This is common on Windows + and for certificates exported from browsers. + + @param filename Path to the PKCS#12 file. + + @param passphrase The password protecting the file. + + @return Success, or an error if the file could not be read, + parsed, or the passphrase is incorrect. + + @par Example + @code + ctx.use_pkcs12_file( "credentials.pfx", "secret" ).value(); + @endcode + + @see use_pkcs12 + */ + system::result + use_pkcs12_file( + std::string_view filename, + std::string_view passphrase ); + + //-------------------------------------------------------------------------- + // + // Trust Anchors + // + //-------------------------------------------------------------------------- + + /** Add a certificate authority for peer verification. + + Adds a single CA certificate to the trust store used for verifying + peer certificates. Call this multiple times to add multiple CAs, + or use `load_verify_file()` for a bundle. + + @param ca The CA certificate data in PEM format. + + @return Success, or an error if the certificate could not be parsed. + + @see load_verify_file + @see set_default_verify_paths + */ + system::result + add_certificate_authority( std::string_view ca ); + + /** Load CA certificates from a file. + + Loads one or more CA certificates from a PEM file. The file may + contain multiple concatenated PEM certificates. + + @param filename Path to a PEM file containing CA certificates. + + @return Success, or an error if the file could not be read or parsed. + + @par Example + @code + // Load a custom CA bundle + ctx.load_verify_file( "/etc/ssl/certs/ca-certificates.crt" ).value(); + @endcode + + @see add_certificate_authority + @see add_verify_path + */ + system::result + load_verify_file( std::string_view filename ); + + /** Add a directory of CA certificates for verification. + + Adds a directory containing CA certificate files. Each file must + contain a single certificate in PEM format, named using the + subject name hash (as generated by `openssl rehash` or + `c_rehash`). + + @param path Path to the directory containing hashed CA certificates. + + @return Success, or an error if the directory is invalid. + + @par Example + @code + ctx.add_verify_path( "/etc/ssl/certs" ).value(); + @endcode + + @see load_verify_file + @see set_default_verify_paths + */ + system::result + add_verify_path( std::string_view path ); + + /** Use the system default CA certificate store. + + Configures the context to use the operating system's default + trust store for peer certificate verification. This is the + recommended approach for HTTPS clients connecting to public + servers. + + On different platforms this uses: + - Linux: `/etc/ssl/certs` or distribution-specific paths + - macOS: System Keychain + - Windows: Windows Certificate Store + + @return Success, or an error if the system store could not be loaded. + + @par Example + @code + // Trust the same CAs as the system + ctx.set_default_verify_paths().value(); + @endcode + + @see load_verify_file + @see add_verify_path + */ + system::result + set_default_verify_paths(); + + //-------------------------------------------------------------------------- + // + // Protocol Configuration + // + //-------------------------------------------------------------------------- + + /** Set the minimum TLS protocol version. + + Connections will reject protocol versions older than this. + The default allows TLS 1.2 and newer. + + @param v The minimum protocol version to accept. + + @return Success, or an error if the version is not supported + by the backend. + + @par Example + @code + // Require TLS 1.3 minimum + ctx.set_min_protocol_version( tls::version::tls_1_3 ).value(); + @endcode + + @see set_max_protocol_version + */ + system::result + set_min_protocol_version( version v ); + + /** Set the maximum TLS protocol version. + + Connections will not negotiate protocol versions newer than this. + The default allows the newest supported version. + + @param v The maximum protocol version to accept. + + @return Success, or an error if the version is not supported + by the backend. + + @see set_min_protocol_version + */ + system::result + set_max_protocol_version( version v ); + + /** Set the allowed cipher suites. + + Configures which cipher suites may be used for connections. + The format is backend-specific but typically follows OpenSSL + cipher list syntax. + + @param ciphers The cipher suite specification string. + + @return Success, or an error if the cipher string is invalid. + + @par Example + @code + // TLS 1.2 cipher suites (OpenSSL format) + ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ).value(); + @endcode + + @note For TLS 1.3, use `set_ciphersuites_tls13()` on backends + that distinguish between TLS 1.2 and 1.3 cipher configuration. + */ + system::result + set_ciphersuites( std::string_view ciphers ); + + /** Set the ALPN protocol list. + + Configures Application-Layer Protocol Negotiation (ALPN) for + the connection. ALPN is used to negotiate which application + protocol to use over the TLS connection (e.g., "h2" for HTTP/2, + "http/1.1" for HTTP/1.1). + + The protocols are tried in preference order (first = highest). + + @param protocols Ordered list of protocol identifiers. + + @return Success, or an error if ALPN configuration fails. + + @par Example + @code + // Prefer HTTP/2, fall back to HTTP/1.1 + ctx.set_alpn( { "h2", "http/1.1" } ).value(); + @endcode + */ + system::result + set_alpn( std::initializer_list protocols ); + + //-------------------------------------------------------------------------- + // + // Certificate Verification + // + //-------------------------------------------------------------------------- + + /** Set the peer certificate verification mode. + + Controls whether and how peer certificates are verified during + the TLS handshake. + + @param mode The verification mode to use. + + @return Success, or an error if the mode could not be set. + + @par Example + @code + // Verify peer certificate (typical for clients) + ctx.set_verify_mode( tls::verify_mode::peer ).value(); + + // Require client certificate (server-side mTLS) + ctx.set_verify_mode( tls::verify_mode::require_peer ).value(); + @endcode + + @see verify_mode + */ + system::result + set_verify_mode( verify_mode mode ); + + /** Set the maximum certificate chain verification depth. + + Limits how many intermediate certificates can appear between + the peer certificate and a trusted root. The default is + typically 100, which is sufficient for most certificate chains. + + @param depth Maximum number of intermediate certificates allowed. + + @return Success, or an error if the depth is invalid. + */ + system::result + set_verify_depth( int depth ); + + /** Set a custom certificate verification callback. + + Installs a callback that is invoked during certificate chain + verification. The callback can perform additional validation + beyond the standard checks and can override verification + results. + + The callback receives the verification result so far and + information about the certificate being verified. Return + `true` to accept the certificate, `false` to reject. + + @tparam Callback A callable with signature + `bool( bool preverified, verify_context& ctx )`. + + @param callback The verification callback. + + @return Success, or an error if the callback could not be set. + + @note The `verify_context` type provides access to the + certificate and chain information. Its exact interface + depends on the TLS backend. + */ + template + system::result + set_verify_callback( Callback callback ); + + /** Set the expected server hostname for verification. + + For client connections, sets the hostname that the server + certificate must match. This enables: + + 1. SNI (Server Name Indication) — tells the server which + certificate to present (for virtual hosting) + 2. Hostname verification — validates the certificate's + Subject Alternative Name or Common Name matches + + @param hostname The expected server hostname. + + @par Example + @code + ctx.set_hostname( "api.example.com" ); + @endcode + + @note This is typically required for HTTPS clients to ensure + they're connecting to the intended server. + */ + void + set_hostname( std::string_view hostname ); + + /** Set a callback for Server Name Indication (SNI). + + For server connections, this callback is invoked during the TLS + handshake when a client sends an SNI extension. The callback + receives the requested hostname and can accept or reject the + connection. + + @tparam Callback A callable with signature + `bool( std::string_view hostname )`. + + @param callback The SNI callback. Return `true` to accept the + connection or `false` to reject it with an alert. + + @par Example + @code + // Accept connections for specific domains only + ctx.set_servername_callback( + []( std::string_view hostname ) -> bool + { + return hostname == "api.example.com" || + hostname == "www.example.com"; + }); + @endcode + + @note For virtual hosting with different certificates per hostname, + create separate contexts and select the appropriate one before + creating the TLS stream. + + @see set_hostname + */ + template + void + set_servername_callback( Callback callback ); + +private: + void + set_servername_callback_impl( + std::function callback ); + +public: + + //-------------------------------------------------------------------------- + // + // Revocation Checking + // + //-------------------------------------------------------------------------- + + /** Add a Certificate Revocation List from memory. + + Adds a CRL to the verification store for checking whether + certificates have been revoked. CRLs are typically fetched + from the URLs in a certificate's CRL Distribution Points + extension. + + @param crl The CRL data in DER or PEM format. + + @return Success, or an error if the CRL could not be parsed. + + @see add_crl_file + @see set_revocation_policy + */ + system::result + add_crl( std::string_view crl ); + + /** Add a Certificate Revocation List from a file. + + Adds a CRL to the verification store for checking whether + certificates have been revoked. + + @param filename Path to a CRL file (DER or PEM format). + + @return Success, or an error if the file could not be read + or the CRL is invalid. + + @par Example + @code + ctx.add_crl_file( "issuer.crl" ).value(); + @endcode + + @see add_crl + @see set_revocation_policy + */ + system::result + add_crl_file( std::string_view filename ); + + /** Set the OCSP staple response for server-side stapling. + + For servers, provides a pre-fetched OCSP response to send + to clients during the handshake. This proves the server's + certificate hasn't been revoked without requiring the client + to contact the OCSP responder. + + The OCSP response must be periodically refreshed (typically + every few hours to days) before it expires. + + @param response The DER-encoded OCSP response. + + @return Success, or an error if the response is invalid. + + @note This is a server-side operation. Clients use + `set_require_ocsp_staple()` to require stapled responses. + */ + system::result + set_ocsp_staple( std::string_view response ); + + /** Require OCSP stapling from the server. + + For clients, requires the server to provide a stapled OCSP + response proving its certificate hasn't been revoked. If + the server doesn't provide a stapled response, the handshake + fails. + + @param require Whether to require OCSP stapling. + + @note Not all servers support OCSP stapling. Enable this only + when connecting to servers known to support it. + */ + void + set_require_ocsp_staple( bool require ); + + /** Set the certificate revocation checking policy. + + Controls how certificate revocation status is checked during + verification. This affects both CRL and OCSP checking. + + @param policy The revocation checking policy. + + @par Example + @code + // Require successful revocation check + ctx.set_revocation_policy( tls::revocation_policy::hard_fail ); + + // Check but allow unknown status + ctx.set_revocation_policy( tls::revocation_policy::soft_fail ); + @endcode + + @see revocation_policy + @see add_crl + */ + void + set_revocation_policy( revocation_policy policy ); + + //-------------------------------------------------------------------------- + // + // Password Handling + // + //-------------------------------------------------------------------------- + + /** Set the password callback for encrypted keys. + + Installs a callback that provides passwords for encrypted + private keys and PKCS#12 files. The callback is invoked when + loading encrypted key material. + + @tparam Callback A callable with signature + `std::string( std::size_t max_length, password_purpose purpose )`. + + @param callback The password callback. It receives the maximum + password length and the purpose (reading or writing), and + returns the password string. + + @par Example + @code + ctx.set_password_callback( + []( std::size_t max_len, tls::password_purpose purpose ) + { + // In practice, prompt user or read from secure storage + return std::string( "my-key-password" ); + }); + + // Now load encrypted key + ctx.use_private_key_file( "encrypted.key", tls::file_format::pem ).value(); + @endcode + + @see password_purpose + */ + template + void + set_password_callback( Callback callback ); +}; + +template +void +context:: +set_servername_callback( Callback callback ) +{ + set_servername_callback_impl( std::move( callback ) ); +} + +} // namespace tls +} // namespace corosio +} // namespace boost + +#endif diff --git a/src/corosio/src/tls/context.cpp b/src/corosio/src/tls/context.cpp index 3803630d..a877a16e 100644 --- a/src/corosio/src/tls/context.cpp +++ b/src/corosio/src/tls/context.cpp @@ -1,298 +1,306 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include -#include "detail/context_impl.hpp" - -#include -#include -#include - -namespace boost { -namespace corosio { -namespace tls { - -//------------------------------------------------------------------------------ - -context:: -context() - : impl_( std::make_shared() ) -{ -} - -//------------------------------------------------------------------------------ -// -// Credential Loading -// -//------------------------------------------------------------------------------ - -system::result -context:: -use_certificate( - std::string_view certificate, - file_format format ) -{ - impl_->entity_certificate = std::string( certificate ); - impl_->entity_cert_format = format; - return {}; -} - -system::result -context:: -use_certificate_file( - std::string_view filename, - file_format format ) -{ - std::ifstream file( std::string( filename ), std::ios::binary ); - if( !file ) - return system::error_code( ENOENT, system::generic_category() ); - - std::ostringstream ss; - ss << file.rdbuf(); - impl_->entity_certificate = ss.str(); - impl_->entity_cert_format = format; - return {}; -} - -system::result -context:: -use_certificate_chain( std::string_view chain ) -{ - impl_->certificate_chain = std::string( chain ); - return {}; -} - -system::result -context:: -use_certificate_chain_file( std::string_view filename ) -{ - std::ifstream file( std::string( filename ), std::ios::binary ); - if( !file ) - return system::error_code( ENOENT, system::generic_category() ); - - std::ostringstream ss; - ss << file.rdbuf(); - impl_->certificate_chain = ss.str(); - return {}; -} - -system::result -context:: -use_private_key( - std::string_view private_key, - file_format format ) -{ - impl_->private_key = std::string( private_key ); - impl_->private_key_format = format; - return {}; -} - -system::result -context:: -use_private_key_file( - std::string_view filename, - file_format format ) -{ - std::ifstream file( std::string( filename ), std::ios::binary ); - if( !file ) - return system::error_code( ENOENT, system::generic_category() ); - - std::ostringstream ss; - ss << file.rdbuf(); - impl_->private_key = ss.str(); - impl_->private_key_format = format; - return {}; -} - -system::result -context:: -use_pkcs12( - std::string_view /*data*/, - std::string_view /*passphrase*/ ) -{ - // TODO: Implement PKCS#12 parsing - return system::error_code( ENOTSUP, system::generic_category() ); -} - -system::result -context:: -use_pkcs12_file( - std::string_view /*filename*/, - std::string_view /*passphrase*/ ) -{ - // TODO: Implement PKCS#12 file loading - return system::error_code( ENOTSUP, system::generic_category() ); -} - -//------------------------------------------------------------------------------ -// -// Trust Anchors -// -//------------------------------------------------------------------------------ - -system::result -context:: -add_certificate_authority( std::string_view ca ) -{ - impl_->ca_certificates.emplace_back( ca ); - return {}; -} - -system::result -context:: -load_verify_file( std::string_view filename ) -{ - std::ifstream file( std::string( filename ), std::ios::binary ); - if( !file ) - return system::error_code( ENOENT, system::generic_category() ); - - std::ostringstream ss; - ss << file.rdbuf(); - impl_->ca_certificates.push_back( ss.str() ); - return {}; -} - -system::result -context:: -add_verify_path( std::string_view path ) -{ - impl_->verify_paths.emplace_back( path ); - return {}; -} - -system::result -context:: -set_default_verify_paths() -{ - impl_->use_default_verify_paths = true; - return {}; -} - -//------------------------------------------------------------------------------ -// -// Protocol Configuration -// -//------------------------------------------------------------------------------ - -system::result -context:: -set_min_protocol_version( version v ) -{ - impl_->min_version = v; - return {}; -} - -system::result -context:: -set_max_protocol_version( version v ) -{ - impl_->max_version = v; - return {}; -} - -system::result -context:: -set_ciphersuites( std::string_view ciphers ) -{ - impl_->ciphersuites = std::string( ciphers ); - return {}; -} - -system::result -context:: -set_alpn( std::initializer_list protocols ) -{ - impl_->alpn_protocols.clear(); - for( auto const& p : protocols ) - impl_->alpn_protocols.emplace_back( p ); - return {}; -} - -//------------------------------------------------------------------------------ -// -// Certificate Verification -// -//------------------------------------------------------------------------------ - -system::result -context:: -set_verify_mode( verify_mode mode ) -{ - impl_->verification_mode = mode; - return {}; -} - -system::result -context:: -set_verify_depth( int depth ) -{ - impl_->verify_depth = depth; - return {}; -} - -void -context:: -set_hostname( std::string_view hostname ) -{ - impl_->hostname = std::string( hostname ); -} - -//------------------------------------------------------------------------------ -// -// Revocation Checking -// -//------------------------------------------------------------------------------ - -system::result -context:: -add_crl( std::string_view crl ) -{ - impl_->crls.emplace_back( crl ); - return {}; -} - -system::result -context:: -add_crl_file( std::string_view filename ) -{ - std::ifstream file( std::string( filename ), std::ios::binary ); - if( !file ) - return system::error_code( ENOENT, system::generic_category() ); - - std::ostringstream ss; - ss << file.rdbuf(); - impl_->crls.push_back( ss.str() ); - return {}; -} - -system::result -context:: -set_ocsp_staple( std::string_view response ) -{ - impl_->ocsp_staple = std::string( response ); - return {}; -} - -void -context:: -set_require_ocsp_staple( bool require ) -{ - impl_->require_ocsp_staple = require; -} - -void -context:: -set_revocation_policy( revocation_policy policy ) -{ - impl_->revocation = policy; -} - -} // namespace tls -} // namespace corosio -} // namespace boost +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include "detail/context_impl.hpp" + +#include +#include +#include + +namespace boost { +namespace corosio { +namespace tls { + +//------------------------------------------------------------------------------ + +context:: +context() + : impl_( std::make_shared() ) +{ +} + +//------------------------------------------------------------------------------ +// +// Credential Loading +// +//------------------------------------------------------------------------------ + +system::result +context:: +use_certificate( + std::string_view certificate, + file_format format ) +{ + impl_->entity_certificate = std::string( certificate ); + impl_->entity_cert_format = format; + return {}; +} + +system::result +context:: +use_certificate_file( + std::string_view filename, + file_format format ) +{ + std::ifstream file( std::string( filename ), std::ios::binary ); + if( !file ) + return system::error_code( ENOENT, system::generic_category() ); + + std::ostringstream ss; + ss << file.rdbuf(); + impl_->entity_certificate = ss.str(); + impl_->entity_cert_format = format; + return {}; +} + +system::result +context:: +use_certificate_chain( std::string_view chain ) +{ + impl_->certificate_chain = std::string( chain ); + return {}; +} + +system::result +context:: +use_certificate_chain_file( std::string_view filename ) +{ + std::ifstream file( std::string( filename ), std::ios::binary ); + if( !file ) + return system::error_code( ENOENT, system::generic_category() ); + + std::ostringstream ss; + ss << file.rdbuf(); + impl_->certificate_chain = ss.str(); + return {}; +} + +system::result +context:: +use_private_key( + std::string_view private_key, + file_format format ) +{ + impl_->private_key = std::string( private_key ); + impl_->private_key_format = format; + return {}; +} + +system::result +context:: +use_private_key_file( + std::string_view filename, + file_format format ) +{ + std::ifstream file( std::string( filename ), std::ios::binary ); + if( !file ) + return system::error_code( ENOENT, system::generic_category() ); + + std::ostringstream ss; + ss << file.rdbuf(); + impl_->private_key = ss.str(); + impl_->private_key_format = format; + return {}; +} + +system::result +context:: +use_pkcs12( + std::string_view /*data*/, + std::string_view /*passphrase*/ ) +{ + // TODO: Implement PKCS#12 parsing + return system::error_code( ENOTSUP, system::generic_category() ); +} + +system::result +context:: +use_pkcs12_file( + std::string_view /*filename*/, + std::string_view /*passphrase*/ ) +{ + // TODO: Implement PKCS#12 file loading + return system::error_code( ENOTSUP, system::generic_category() ); +} + +//------------------------------------------------------------------------------ +// +// Trust Anchors +// +//------------------------------------------------------------------------------ + +system::result +context:: +add_certificate_authority( std::string_view ca ) +{ + impl_->ca_certificates.emplace_back( ca ); + return {}; +} + +system::result +context:: +load_verify_file( std::string_view filename ) +{ + std::ifstream file( std::string( filename ), std::ios::binary ); + if( !file ) + return system::error_code( ENOENT, system::generic_category() ); + + std::ostringstream ss; + ss << file.rdbuf(); + impl_->ca_certificates.push_back( ss.str() ); + return {}; +} + +system::result +context:: +add_verify_path( std::string_view path ) +{ + impl_->verify_paths.emplace_back( path ); + return {}; +} + +system::result +context:: +set_default_verify_paths() +{ + impl_->use_default_verify_paths = true; + return {}; +} + +//------------------------------------------------------------------------------ +// +// Protocol Configuration +// +//------------------------------------------------------------------------------ + +system::result +context:: +set_min_protocol_version( version v ) +{ + impl_->min_version = v; + return {}; +} + +system::result +context:: +set_max_protocol_version( version v ) +{ + impl_->max_version = v; + return {}; +} + +system::result +context:: +set_ciphersuites( std::string_view ciphers ) +{ + impl_->ciphersuites = std::string( ciphers ); + return {}; +} + +system::result +context:: +set_alpn( std::initializer_list protocols ) +{ + impl_->alpn_protocols.clear(); + for( auto const& p : protocols ) + impl_->alpn_protocols.emplace_back( p ); + return {}; +} + +//------------------------------------------------------------------------------ +// +// Certificate Verification +// +//------------------------------------------------------------------------------ + +system::result +context:: +set_verify_mode( verify_mode mode ) +{ + impl_->verification_mode = mode; + return {}; +} + +system::result +context:: +set_verify_depth( int depth ) +{ + impl_->verify_depth = depth; + return {}; +} + +void +context:: +set_hostname( std::string_view hostname ) +{ + impl_->hostname = std::string( hostname ); +} + +void +context:: +set_servername_callback_impl( + std::function callback ) +{ + impl_->servername_callback = std::move( callback ); +} + +//------------------------------------------------------------------------------ +// +// Revocation Checking +// +//------------------------------------------------------------------------------ + +system::result +context:: +add_crl( std::string_view crl ) +{ + impl_->crls.emplace_back( crl ); + return {}; +} + +system::result +context:: +add_crl_file( std::string_view filename ) +{ + std::ifstream file( std::string( filename ), std::ios::binary ); + if( !file ) + return system::error_code( ENOENT, system::generic_category() ); + + std::ostringstream ss; + ss << file.rdbuf(); + impl_->crls.push_back( ss.str() ); + return {}; +} + +system::result +context:: +set_ocsp_staple( std::string_view response ) +{ + impl_->ocsp_staple = std::string( response ); + return {}; +} + +void +context:: +set_require_ocsp_staple( bool require ) +{ + impl_->require_ocsp_staple = require; +} + +void +context:: +set_revocation_policy( revocation_policy policy ) +{ + impl_->revocation = policy; +} + +} // namespace tls +} // namespace corosio +} // namespace boost diff --git a/src/corosio/src/tls/detail/context_impl.hpp b/src/corosio/src/tls/detail/context_impl.hpp index cdb0c3df..a67b5464 100644 --- a/src/corosio/src/tls/detail/context_impl.hpp +++ b/src/corosio/src/tls/detail/context_impl.hpp @@ -71,6 +71,11 @@ struct context_data std::string hostname; std::function verify_callback; + //-------------------------------------------- + // SNI (Server Name Indication) + + std::function servername_callback; + //-------------------------------------------- // Revocation diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index f48d2e8f..00bd8d37 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -23,9 +23,31 @@ #include #include +#include +#include #include +#include #include +// Debug logging for CI timeout investigation +// Set COROSIO_TLS_DEBUG=1 to enable detailed logging +namespace { +inline bool tls_debug_enabled() +{ + static bool enabled = std::getenv("COROSIO_TLS_DEBUG") != nullptr; + return enabled; +} + +inline auto now_ms() +{ + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); +} +} // anonymous namespace + +#define TLS_DEBUG(msg) \ + do { if (tls_debug_enabled()) std::cerr << "[OPENSSL " << now_ms() << "ms] " << msg << "\n"; } while(0) + /* openssl_stream Architecture =========================== @@ -84,6 +106,30 @@ using buffer_array = std::array; namespace tls { namespace detail { +// Ex data index for storing context_data pointer in SSL_CTX +static int sni_ctx_data_index = -1; + +// SNI callback invoked by OpenSSL during handshake +static int +sni_callback( SSL* ssl, int* /* alert */, void* /* arg */ ) +{ + char const* servername = SSL_get_servername( ssl, TLSEXT_NAMETYPE_host_name ); + if( !servername ) + return SSL_TLSEXT_ERR_NOACK; // No SNI sent, continue + + SSL_CTX* ctx = SSL_get_SSL_CTX( ssl ); + auto* cd = static_cast( + SSL_CTX_get_ex_data( ctx, sni_ctx_data_index ) ); + + if( cd && cd->servername_callback ) + { + if( !cd->servername_callback( servername ) ) + return SSL_TLSEXT_ERR_ALERT_FATAL; // Callback rejected hostname + } + + return SSL_TLSEXT_ERR_OK; +} + /** Cached OpenSSL context owning SSL_CTX. Created on first stream construction for a given tls::context, @@ -94,16 +140,29 @@ class openssl_native_context { public: SSL_CTX* ctx_; + context_data const* cd_; // For SNI callback access explicit openssl_native_context( context_data const& cd ) : ctx_( nullptr ) + , cd_( &cd ) { // Create SSL_CTX supporting both client and server ctx_ = SSL_CTX_new( TLS_method() ); if( !ctx_ ) return; + // Initialize ex_data index for SNI callback (once) + if( sni_ctx_data_index < 0 ) + sni_ctx_data_index = SSL_CTX_get_ex_new_index( 0, nullptr, nullptr, nullptr, nullptr ); + + // Store context_data pointer for SNI callback access + SSL_CTX_set_ex_data( ctx_, sni_ctx_data_index, const_cast( &cd ) ); + + // Set SNI callback if provided + if( cd.servername_callback ) + SSL_CTX_set_tlsext_servername_callback( ctx_, sni_callback ); + // Set modes for partial writes and moving buffers SSL_CTX_set_mode( ctx_, SSL_MODE_ENABLE_PARTIAL_WRITE ); SSL_CTX_set_mode( ctx_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER ); @@ -141,6 +200,35 @@ class openssl_native_context } } + // Apply certificate chain if provided (entity cert + intermediates) + // First cert is entity, rest are intermediates + if( !cd.certificate_chain.empty() ) + { + BIO* bio = BIO_new_mem_buf( + cd.certificate_chain.data(), + static_cast( cd.certificate_chain.size() ) ); + if( bio ) + { + // First cert is entity certificate + X509* entity = PEM_read_bio_X509( bio, nullptr, nullptr, nullptr ); + if( entity ) + { + SSL_CTX_use_certificate( ctx_, entity ); + X509_free( entity ); + } + + // Remaining certs are intermediates - add as extra chain certs + X509* cert; + while( ( cert = PEM_read_bio_X509( bio, nullptr, nullptr, nullptr ) ) != nullptr ) + { + // SSL_CTX_add_extra_chain_cert takes ownership - don't free + SSL_CTX_add_extra_chain_cert( ctx_, cert ); + } + ERR_clear_error(); // Clear expected EOF error from reading end of chain + BIO_free( bio ); + } + } + // Apply private key if provided if( !cd.private_key.empty() ) { @@ -259,38 +347,60 @@ struct openssl_stream_impl_ //-------------------------------------------------------------------------- capy::task - flush_output() + flush_output(std::stop_token token) { - while(BIO_ctrl_pending(ext_bio_) > 0) + TLS_DEBUG("flush_output: start, stop_requested=" << token.stop_requested()); + while(BIO_ctrl_pending(ext_bio_) > 0 && !token.stop_requested()) { int pending = static_cast(BIO_ctrl_pending(ext_bio_)); int to_read = (std::min)(pending, static_cast(out_buf_.size())); int n = BIO_read(ext_bio_, out_buf_.data(), to_read); + TLS_DEBUG("flush_output: BIO_read returned " << n << " bytes"); if(n <= 0) break; // Write to underlying stream + TLS_DEBUG("flush_output: acquiring mutex"); auto guard = co_await io_mutex_.scoped_lock(); + TLS_DEBUG("flush_output: calling s_.write_some(" << n << " bytes)"); auto [ec, written] = co_await s_.write_some( capy::mutable_buffer(out_buf_.data(), static_cast(n))); + TLS_DEBUG("flush_output: s_.write_some returned ec=" << ec.message() << " written=" << written); if(ec) co_return ec; } + if(token.stop_requested()) + { + TLS_DEBUG("flush_output: stop_requested, returning canceled"); + co_return make_error_code(system::errc::operation_canceled); + } + TLS_DEBUG("flush_output: done"); co_return system::error_code{}; } capy::task - read_input() + read_input(std::stop_token token) { + TLS_DEBUG("read_input: start, stop_requested=" << token.stop_requested()); + if(token.stop_requested()) + { + TLS_DEBUG("read_input: already stopped, returning canceled"); + co_return make_error_code(system::errc::operation_canceled); + } + + TLS_DEBUG("read_input: acquiring mutex"); auto guard = co_await io_mutex_.scoped_lock(); + TLS_DEBUG("read_input: calling s_.read_some"); auto [ec, n] = co_await s_.read_some( capy::mutable_buffer(in_buf_.data(), in_buf_.size())); + TLS_DEBUG("read_input: s_.read_some returned ec=" << ec.message() << " n=" << n); if(ec) co_return ec; // Feed data into OpenSSL int written = BIO_write(ext_bio_, in_buf_.data(), static_cast(n)); (void)written; + TLS_DEBUG("read_input: BIO_write returned " << written); co_return system::error_code{}; } @@ -340,19 +450,19 @@ struct openssl_stream_impl_ if(err == SSL_ERROR_WANT_WRITE) { // Flush pending output (renegotiation) - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) goto done; } else if(err == SSL_ERROR_WANT_READ) { // First flush any pending output - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) goto done; // Then read from network - ec = co_await read_input(); + ec = co_await read_input(token); if(ec) { if(ec == make_error_code(capy::error::eof)) @@ -436,7 +546,7 @@ struct openssl_stream_impl_ // For write_some semantics, flush and return after first successful write if(total_written > 0) { - ec = co_await flush_output(); + ec = co_await flush_output(token); goto done; } } @@ -446,18 +556,18 @@ struct openssl_stream_impl_ if(err == SSL_ERROR_WANT_WRITE) { - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) goto done; } else if(err == SSL_ERROR_WANT_READ) { // Renegotiation - flush then read - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) goto done; - ec = co_await read_input(); + ec = co_await read_input(token); if(ec) goto done; } @@ -491,10 +601,14 @@ struct openssl_stream_impl_ std::coroutine_handle<> continuation, capy::executor_ref d) { + TLS_DEBUG("do_handshake: start, type=" << (type == openssl_stream::client ? "client" : "server") << " stop_requested=" << token.stop_requested()); system::error_code ec; + int iteration = 0; while(!token.stop_requested()) { + ++iteration; + TLS_DEBUG("do_handshake: iteration " << iteration << " stop_requested=" << token.stop_requested()); ERR_clear_error(); int ret; if(type == openssl_stream::client) @@ -502,37 +616,49 @@ struct openssl_stream_impl_ else ret = SSL_accept(ssl_); + TLS_DEBUG("do_handshake: SSL_connect/accept returned " << ret); + if(ret == 1) { + TLS_DEBUG("do_handshake: handshake complete, flushing output"); // Handshake completed - flush any remaining output - ec = co_await flush_output(); + ec = co_await flush_output(token); + TLS_DEBUG("do_handshake: flush_output returned ec=" << ec.message()); break; } else { int err = SSL_get_error(ssl_, ret); + TLS_DEBUG("do_handshake: SSL_get_error=" << err); if(err == SSL_ERROR_WANT_WRITE) { - ec = co_await flush_output(); + TLS_DEBUG("do_handshake: WANT_WRITE, flushing"); + ec = co_await flush_output(token); + TLS_DEBUG("do_handshake: flush_output returned ec=" << ec.message()); if(ec) break; } else if(err == SSL_ERROR_WANT_READ) { + TLS_DEBUG("do_handshake: WANT_READ, flushing then reading"); // Flush output first (e.g., ClientHello) - ec = co_await flush_output(); + ec = co_await flush_output(token); + TLS_DEBUG("do_handshake: flush_output returned ec=" << ec.message()); if(ec) break; + TLS_DEBUG("do_handshake: calling read_input"); // Then read response - ec = co_await read_input(); + ec = co_await read_input(token); + TLS_DEBUG("do_handshake: read_input returned ec=" << ec.message()); if(ec) break; } else { unsigned long ssl_err = ERR_get_error(); + TLS_DEBUG("do_handshake: SSL error " << ssl_err); ec = system::error_code( static_cast(ssl_err), system::system_category()); break; @@ -541,8 +667,12 @@ struct openssl_stream_impl_ } if(token.stop_requested()) + { + TLS_DEBUG("do_handshake: stop_requested after loop, setting canceled"); ec = make_error_code(system::errc::operation_canceled); + } + TLS_DEBUG("do_handshake: done, ec=" << ec.message()); *ec_out = ec; d.dispatch(capy::coro{continuation}).resume(); @@ -566,18 +696,18 @@ struct openssl_stream_impl_ if(ret == 1) { // Bidirectional shutdown complete - ec = co_await flush_output(); + ec = co_await flush_output(token); break; } else if(ret == 0) { // Sent close_notify, need to receive peer's - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) break; // Continue to receive peer's close_notify - ec = co_await read_input(); + ec = co_await read_input(token); if(ec) { // EOF is expected during shutdown @@ -592,17 +722,17 @@ struct openssl_stream_impl_ if(err == SSL_ERROR_WANT_WRITE) { - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) break; } else if(err == SSL_ERROR_WANT_READ) { - ec = co_await flush_output(); + ec = co_await flush_output(token); if(ec) break; - ec = co_await read_input(); + ec = co_await read_input(token); if(ec) { if(ec == make_error_code(capy::error::eof)) @@ -657,7 +787,7 @@ struct openssl_stream_impl_ buffer_array bufs{}; std::size_t count = param.copy_to(bufs.data(), max_buffers); - capy::run_async(d)( + capy::run_async(d, token)( do_read_some(bufs, count, token, ec, bytes, h, d)); } @@ -672,7 +802,7 @@ struct openssl_stream_impl_ buffer_array bufs{}; std::size_t count = param.copy_to(bufs.data(), max_buffers); - capy::run_async(d)( + capy::run_async(d, token)( do_write_some(bufs, count, token, ec, bytes, h, d)); } @@ -683,7 +813,7 @@ struct openssl_stream_impl_ std::stop_token token, system::error_code* ec) override { - capy::run_async(d)( + capy::run_async(d, token)( do_handshake(type, token, ec, h, d)); } @@ -693,7 +823,7 @@ struct openssl_stream_impl_ std::stop_token token, system::error_code* ec) override { - capy::run_async(d)( + capy::run_async(d, token)( do_shutdown(token, ec, h, d)); } @@ -737,10 +867,15 @@ struct openssl_stream_impl_ // Attach internal BIO to SSL (SSL takes ownership) SSL_set_bio( ssl_, int_bio, int_bio ); - // Apply per-session config (SNI) from context + // Apply per-session config (SNI + hostname verification) from context if( !impl.hostname.empty() ) { + // Set SNI extension so server knows which cert to present SSL_set_tlsext_host_name( ssl_, impl.hostname.c_str() ); + + // Enable hostname verification (checks CN/SAN in peer cert) + // Available in OpenSSL 1.0.2+ + SSL_set1_host( ssl_, impl.hostname.c_str() ); } return {}; diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index dd0fa839..508a2cc4 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -108,6 +108,25 @@ using buffer_array = std::array; namespace tls { namespace detail { +// SNI callback invoked by WolfSSL during handshake (server-side) +// Returns SNICbReturn enum: 0 = OK, fatal_return (2) = abort +static int +wolfssl_sni_callback( WOLFSSL* ssl, int* /* alert */, void* arg ) +{ + char const* servername = wolfSSL_get_servername( ssl, WOLFSSL_SNI_HOST_NAME ); + if( !servername ) + return 0; // No SNI sent, continue + + auto* cd = static_cast( arg ); + if( cd && cd->servername_callback ) + { + if( !cd->servername_callback( servername ) ) + return fatal_return; // Callback rejected hostname + } + + return 0; // Accept +} + /** Cached WolfSSL contexts owning WOLFSSL_CTX for client and server. Created on first stream construction for a given tls::context, @@ -136,9 +155,17 @@ class wolfssl_native_context verify_mode_flag = WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT; wolfSSL_CTX_set_verify( ctx, verify_mode_flag, nullptr ); - // Apply certificates if provided - if( !cd.entity_certificate.empty() ) + // Apply certificate chain if provided (entity cert + intermediates) + // wolfSSL_CTX_use_certificate_chain_buffer loads entity as cert, rest as chain + if( !cd.certificate_chain.empty() ) { + wolfSSL_CTX_use_certificate_chain_buffer( ctx, + reinterpret_cast( cd.certificate_chain.data() ), + static_cast( cd.certificate_chain.size() ) ); + } + else if( !cd.entity_certificate.empty() ) + { + // Only use single certificate if no chain provided int format = ( cd.entity_cert_format == file_format::pem ) ? WOLFSSL_FILETYPE_PEM : WOLFSSL_FILETYPE_ASN1; wolfSSL_CTX_use_certificate_buffer( ctx, @@ -171,10 +198,13 @@ class wolfssl_native_context wolfSSL_CTX_set_verify_depth( ctx, cd.verify_depth ); } + context_data const* cd_; // For SNI callback access + explicit wolfssl_native_context( context_data const& cd ) : client_ctx_( nullptr ) , server_ctx_( nullptr ) + , cd_( &cd ) { // Create separate contexts for client and server client_ctx_ = wolfSSL_CTX_new( wolfTLS_client_method() ); @@ -182,6 +212,13 @@ class wolfssl_native_context apply_common_settings( client_ctx_, cd ); apply_common_settings( server_ctx_, cd ); + + // Set SNI callback on server context if provided + if( server_ctx_ && cd.servername_callback ) + { + wolfSSL_CTX_set_servername_callback( server_ctx_, wolfssl_sni_callback ); + wolfSSL_CTX_set_servername_arg( server_ctx_, const_cast( &cd ) ); + } } ~wolfssl_native_context() override @@ -346,15 +383,23 @@ struct wolfssl_stream_impl_ //-------------------------------------------------------------------------- capy::task> - do_underlying_read(capy::mutable_buffer buf) + do_underlying_read(capy::mutable_buffer buf, std::stop_token token) { + if(token.stop_requested()) + co_return capy::io_result{ + make_error_code(system::errc::operation_canceled), 0}; + auto guard = co_await io_mutex_.scoped_lock(); co_return co_await s_.read_some(buf); } capy::task> - do_underlying_write(capy::mutable_buffer buf) + do_underlying_write(capy::mutable_buffer buf, std::stop_token token) { + if(token.stop_requested()) + co_return capy::io_result{ + make_error_code(system::errc::operation_canceled), 0}; + auto guard = co_await io_mutex_.scoped_lock(); co_return co_await s_.write_some(buf); } @@ -421,7 +466,7 @@ struct wolfssl_stream_impl_ { if(read_in_pos_ == read_in_len_) { read_in_pos_ = 0; read_in_len_ = 0; } capy::mutable_buffer buf(read_in_buf_.data() + read_in_len_, read_in_buf_.size() - read_in_len_); - auto [rec, rn] = co_await do_underlying_read(buf); + auto [rec, rn] = co_await do_underlying_read(buf, token); if(rec) { if(rec == make_error_code(capy::error::eof)) @@ -446,7 +491,7 @@ struct wolfssl_stream_impl_ while(read_out_len_ > 0) { capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; goto done; } if(wn < read_out_len_) std::memmove(read_out_buf_.data(), read_out_buf_.data() + wn, read_out_len_ - wn); @@ -536,7 +581,7 @@ struct wolfssl_stream_impl_ while(write_out_len_ > 0) { capy::mutable_buffer buf(write_out_buf_.data(), write_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; goto done; } if(wn < write_out_len_) std::memmove(write_out_buf_.data(), write_out_buf_.data() + wn, write_out_len_ - wn); @@ -554,7 +599,7 @@ struct wolfssl_stream_impl_ while(write_out_len_ > 0) { capy::mutable_buffer buf(write_out_buf_.data(), write_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; goto done; } if(wn < write_out_len_) std::memmove(write_out_buf_.data(), write_out_buf_.data() + wn, write_out_len_ - wn); @@ -566,7 +611,7 @@ struct wolfssl_stream_impl_ // Renegotiation if(write_in_pos_ == write_in_len_) { write_in_pos_ = 0; write_in_len_ = 0; } capy::mutable_buffer buf(write_in_buf_.data() + write_in_len_, write_in_buf_.size() - write_in_len_); - auto [rec, rn] = co_await do_underlying_read(buf); + auto [rec, rn] = co_await do_underlying_read(buf, token); if(rec) { ec = rec; goto done; } write_in_len_ += rn; } @@ -646,7 +691,7 @@ struct wolfssl_stream_impl_ while(read_out_len_ > 0) { capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; @@ -668,7 +713,7 @@ struct wolfssl_stream_impl_ while(read_out_len_ > 0) { capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; @@ -687,7 +732,7 @@ struct wolfssl_stream_impl_ capy::mutable_buffer buf( read_in_buf_.data() + read_in_len_, read_in_buf_.size() - read_in_len_); - auto [rec, rn] = co_await do_underlying_read(buf); + auto [rec, rn] = co_await do_underlying_read(buf, token); if(rec) { ec = rec; @@ -700,7 +745,7 @@ struct wolfssl_stream_impl_ while(read_out_len_ > 0) { capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; @@ -755,21 +800,29 @@ struct wolfssl_stream_impl_ }; current_op_ = &op; + // TLS shutdown is bidirectional: + // 1. We send close_notify to peer + // 2. We receive peer's close_notify + // wolfSSL_shutdown returns WOLFSSL_SHUTDOWN_NOT_DONE until both complete. + // After sending our close_notify (via flush), we must read from the socket + // to receive the peer's close_notify, matching OpenSSL's behavior. + while(!token.stop_requested()) { op.want_read = false; op.want_write = false; int ret = wolfSSL_shutdown(ssl_); + // Only call wolfSSL_get_error once per iteration to avoid consuming error state + int err = (ret != WOLFSSL_SUCCESS) ? wolfSSL_get_error(ssl_, ret) : 0; if(ret == WOLFSSL_SUCCESS) { - // Shutdown completed successfully - // Flush any remaining output + // Bidirectional shutdown complete - flush any remaining output while(read_out_len_ > 0) { capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); + auto [wec, wn] = co_await do_underlying_write(buf, token); if(wec) { ec = wec; @@ -783,25 +836,29 @@ struct wolfssl_stream_impl_ } else if(ret == WOLFSSL_SHUTDOWN_NOT_DONE) { - int err = wolfSSL_get_error(ssl_, ret); - - if(err == WOLFSSL_ERROR_WANT_READ) + // Sent our close_notify (or need to), waiting for peer's close_notify + // This mirrors OpenSSL's SSL_shutdown() returning 0. + + // First, flush any pending output (sends our close_notify) + while(read_out_len_ > 0) { - // Flush any pending output first - while(read_out_len_ > 0) + capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); + auto [wec, wn] = co_await do_underlying_write(buf, token); + if(wec) { - capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); - if(wec) - { - ec = wec; - goto exit_shutdown; - } - if(wn < read_out_len_) - std::memmove(read_out_buf_.data(), read_out_buf_.data() + wn, read_out_len_ - wn); - read_out_len_ -= wn; + // Socket error during shutdown write - acceptable, we're done + goto exit_shutdown; } + if(wn < read_out_len_) + std::memmove(read_out_buf_.data(), read_out_buf_.data() + wn, read_out_len_ - wn); + read_out_len_ -= wn; + } + // Check what WolfSSL needs next + if(err == WOLFSSL_ERROR_WANT_READ || err == 0) + { + // Need to read peer's close_notify from the socket + // err==0 also needs a read - the close_notify was sent, now wait for peer if(read_in_pos_ == read_in_len_) { read_in_pos_ = 0; @@ -810,44 +867,35 @@ struct wolfssl_stream_impl_ capy::mutable_buffer buf( read_in_buf_.data() + read_in_len_, read_in_buf_.size() - read_in_len_); - auto [rec, rn] = co_await do_underlying_read(buf); + auto [rec, rn] = co_await do_underlying_read(buf, token); if(rec) { - // EOF from peer is expected during shutdown - if(rec == make_error_code(capy::error::eof)) - break; - ec = rec; - break; + // EOF or socket error during shutdown read - acceptable + // The peer may have just closed the socket without close_notify + goto exit_shutdown; } read_in_len_ += rn; + // Continue loop to process the received data with wolfSSL_shutdown } else if(err == WOLFSSL_ERROR_WANT_WRITE) { - while(read_out_len_ > 0) - { - capy::mutable_buffer buf(read_out_buf_.data(), read_out_len_); - auto [wec, wn] = co_await do_underlying_write(buf); - if(wec) - { - ec = wec; - goto exit_shutdown; - } - if(wn < read_out_len_) - std::memmove(read_out_buf_.data(), read_out_buf_.data() + wn, read_out_len_ - wn); - read_out_len_ -= wn; - } + // Just need to flush more - already done above, continue loop + } + else if(err == WOLFSSL_ERROR_SYSCALL || err == SSL_ERROR_ZERO_RETURN) + { + // Socket closed or peer sent close_notify - shutdown complete + break; } else { - // Other error + // Unexpected error ec = system::error_code(err, system::system_category()); break; } } else { - // SSL_FATAL_ERROR - int err = wolfSSL_get_error(ssl_, ret); + // SSL_FATAL_ERROR or negative return ec = system::error_code(err, system::system_category()); break; } @@ -888,8 +936,8 @@ struct wolfssl_stream_impl_ buffer_array bufs{}; std::size_t count = param.copy_to(bufs.data(), max_buffers); - // Launch inner coroutine via run_async - capy::run_async(d)( + // Launch inner coroutine via run_async with stop_token for cancellation + capy::run_async(d, token)( do_read_some(bufs, count, token, ec, bytes, h, d)); } @@ -906,8 +954,8 @@ struct wolfssl_stream_impl_ buffer_array bufs{}; std::size_t count = param.copy_to(bufs.data(), max_buffers); - // Launch inner coroutine via run_async - capy::run_async(d)( + // Launch inner coroutine via run_async with stop_token for cancellation + capy::run_async(d, token)( do_write_some(bufs, count, token, ec, bytes, h, d)); } @@ -918,8 +966,8 @@ struct wolfssl_stream_impl_ std::stop_token token, system::error_code* ec) override { - // Launch inner coroutine via run_async - capy::run_async(d)( + // Launch inner coroutine via run_async with stop_token for cancellation + capy::run_async(d, token)( do_handshake(type, token, ec, h, d)); } @@ -929,8 +977,8 @@ struct wolfssl_stream_impl_ std::stop_token token, system::error_code* ec) override { - // Launch inner coroutine via run_async - capy::run_async(d)( + // Launch inner coroutine via run_async with stop_token for cancellation + capy::run_async(d, token)( do_shutdown(token, ec, h, d)); } @@ -988,12 +1036,16 @@ struct wolfssl_stream_impl_ wolfSSL_SetIOReadCtx( ssl_, this ); wolfSSL_SetIOWriteCtx( ssl_, this ); - // Apply per-session config (SNI) from context (client only) + // Apply per-session config (SNI + hostname verification) from context if( type == wolfssl_stream::client && !impl.hostname.empty() ) { + // Set SNI extension so server knows which cert to present wolfSSL_UseSNI( ssl_, WOLFSSL_SNI_HOST_NAME, impl.hostname.data(), static_cast( impl.hostname.size() ) ); + + // Enable hostname verification (checks CN/SAN in peer cert) + wolfSSL_check_domain_name( ssl_, impl.hostname.c_str() ); } return {}; diff --git a/test/unit/Jamfile b/test/unit/Jamfile index 869edff8..08a3c12f 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -20,7 +20,34 @@ project boost/corosio/test/unit gcc:-fcoroutines ; -for local f in [ glob-tree-ex . : *.cpp ] +# Non-TLS tests +for local f in [ glob *.cpp ] [ glob test/*.cpp ] { run $(f) ; } + +# OpenSSL tests - always link against boost_corosio_openssl +# The library itself has no if OpenSSL is not found +# The test has static_assert to fail compilation if OpenSSL define is missing +run tls/openssl_stream.cpp + : : : + /boost/corosio//boost_corosio_openssl + ; + +# WolfSSL tests - always link against boost_corosio_wolfssl +# The library itself has no if WolfSSL is not found +# The test has static_assert to fail compilation if WolfSSL define is missing +run tls/wolfssl_stream.cpp + : : : + /boost/corosio//boost_corosio_wolfssl + ; + +# Cross-SSL tests - need both OpenSSL and WolfSSL +run tls/cross_ssl_stream.cpp + : : : + /boost/corosio//boost_corosio_openssl + /boost/corosio//boost_corosio_wolfssl + ; + +# TLS stream base tests (no specific TLS backend required) +run tls/tls_stream.cpp ; diff --git a/test/unit/tls/cross_ssl_stream.cpp b/test/unit/tls/cross_ssl_stream.cpp index 7eaf3d60..5543931f 100644 --- a/test/unit/tls/cross_ssl_stream.cpp +++ b/test/unit/tls/cross_ssl_stream.cpp @@ -1,177 +1,185 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Plan: c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md - -// Cross-Implementation Notes -// -------------------------- -// - Anonymous ciphers skipped: cipher string syntax differs between impls -// - TLS shutdown skipped: close_notify handling differs (see block comment below) -// - Failure tests disabled: socket.cancel() doesn't unblock TLS handshake -// - To enable failure tests: need TLS-aware cancellation that both impls respect - -#include -#include - -#include "test_utils.hpp" -#include "test_suite.hpp" - -/* Cross-Implementation TLS Tests - ================================ - These tests verify TLS interoperability between OpenSSL and WolfSSL. - - Certificate Validation Behavior - ------------------------------- - tls::context stores certificate data as raw bytes without validation. - The backend (OpenSSL/WolfSSL) parses certificates at stream construction. - Invalid certificates are silently ignored (stream has no cert). - Certificate trust verification happens at the RECEIVING peer during handshake. - - TLS Shutdown Interoperability - ----------------------------- - TLS shutdown has documented interoperability issues between implementations. - The close_notify protocol requires bidirectional exchange, but implementations - handle this inconsistently: - - - WolfSSL's wolfSSL_shutdown() does bidirectional shutdown by default - - OpenSSL's SSL_shutdown() requires two calls (send, then receive) - - Some implementations block waiting for peer's close_notify; others don't - - Strict implementations treat missing close_notify as truncation attack - - Cross-impl tests skip TLS shutdown to avoid these friction points. This - matches real-world practice where applications often: - - Just close the socket (HTTP/1.0 "connection: close" style) - - Use application-layer signaling (HTTP/2 GOAWAY, gRPC graceful close) - - Accept SSL_ERROR_ZERO_RETURN as success - - Handshake and data transfer prove interoperability; shutdown is orthogonal. - - Testing Methodology - ------------------- - Success cases (run_tls_test_no_shutdown): - - Shared context (both endpoints use same cert/CA) - - Separate contexts (server cert + client trusts CA) - - Anonymous ciphers skipped: syntax differs between implementations - - Failure cases (run_tls_test_fail): - - Peer requires verification, other side has no cert - - Peer requires verification, other side has cert from untrusted CA - - All combinations tested: OpenSSL client <-> WolfSSL server and vice versa. -*/ - -namespace boost { -namespace corosio { - -struct cross_ssl_stream_test -{ -#if defined(BOOST_COROSIO_HAS_OPENSSL) && defined(BOOST_COROSIO_HAS_WOLFSSL) - static auto - make_openssl( io_stream& s, tls::context ctx ) - { - return openssl_stream( s, ctx ); - } - - static auto - make_wolfssl( io_stream& s, tls::context ctx ) - { - return wolfssl_stream( s, ctx ); - } - - void - testCrossImplSuccess() - { - using namespace tls::test; - - // Skip anon mode for cross-impl: anonymous cipher syntax differs between - // OpenSSL and WolfSSL, and WolfSSL may not have anon ciphers compiled in. - // Certificate-based modes test the important interop scenarios. - for( auto mode : { context_mode::shared_cert, - context_mode::separate_cert } ) - { - io_context ioc; - auto [client_ctx, server_ctx] = make_contexts( mode ); - - // OpenSSL client -> WolfSSL server - run_tls_test_no_shutdown( ioc, client_ctx, server_ctx, - make_openssl, make_wolfssl ); - ioc.restart(); - - // WolfSSL client -> OpenSSL server - run_tls_test_no_shutdown( ioc, client_ctx, server_ctx, - make_wolfssl, make_openssl ); - } - } - - void - testCrossImplFailure() - { - using namespace tls::test; - - io_context ioc; - - // OpenSSL client trusts wrong CA, WolfSSL server has cert - { - auto client_ctx = make_wrong_ca_context(); - auto server_ctx = make_server_context(); - run_tls_test_fail( ioc, client_ctx, server_ctx, - make_openssl, make_wolfssl ); - ioc.restart(); - } - - // WolfSSL client trusts wrong CA, OpenSSL server has cert - { - auto client_ctx = make_wrong_ca_context(); - auto server_ctx = make_server_context(); - run_tls_test_fail( ioc, client_ctx, server_ctx, - make_wolfssl, make_openssl ); - ioc.restart(); - } - - // OpenSSL client verifies, WolfSSL server has no cert - { - auto client_ctx = make_client_context(); - auto server_ctx = make_anon_context(); - server_ctx.set_ciphersuites( "" ); - run_tls_test_fail( ioc, client_ctx, server_ctx, - make_openssl, make_wolfssl ); - ioc.restart(); - } - - // WolfSSL client verifies, OpenSSL server has no cert - { - auto client_ctx = make_client_context(); - auto server_ctx = make_anon_context(); - server_ctx.set_ciphersuites( "" ); - run_tls_test_fail( ioc, client_ctx, server_ctx, - make_wolfssl, make_openssl ); - } - } -#endif - - void - run() - { -#if defined(BOOST_COROSIO_HAS_OPENSSL) && defined(BOOST_COROSIO_HAS_WOLFSSL) - testCrossImplSuccess(); - // Failure tests disabled: cancelling the underlying socket doesn't - // propagate to TLS handshake operations - they have their own async - // state machines that don't respond to socket cancellation. When one - // side fails verification, the other side's handshake hangs forever. - // Certificate verification failures are tested in same-implementation - // tests where this issue doesn't occur. - // testCrossImplFailure(); -#endif - } -}; - -TEST_SUITE(cross_ssl_stream_test, "boost.corosio.cross_ssl_stream"); - -} // namespace corosio -} // namespace boost +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Plan: c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md + +// Cross-Implementation Notes +// -------------------------- +// - Anonymous ciphers skipped: cipher string syntax differs between impls +// - TLS shutdown skipped: close_notify handling differs (see block comment below) +// - Failure tests disabled: socket.cancel() doesn't unblock TLS handshake +// - To enable failure tests: need TLS-aware cancellation that both impls respect + +#include +#include + +#include "test_utils.hpp" +#include "test_suite.hpp" +#include + +/* Cross-Implementation TLS Tests + ================================ + These tests verify TLS interoperability between OpenSSL and WolfSSL. + + Certificate Validation Behavior + ------------------------------- + tls::context stores certificate data as raw bytes without validation. + The backend (OpenSSL/WolfSSL) parses certificates at stream construction. + Invalid certificates are silently ignored (stream has no cert). + Certificate trust verification happens at the RECEIVING peer during handshake. + + TLS Shutdown Interoperability + ----------------------------- + TLS shutdown has documented interoperability issues between implementations. + The close_notify protocol requires bidirectional exchange, but implementations + handle this inconsistently: + + - WolfSSL's wolfSSL_shutdown() does bidirectional shutdown by default + - OpenSSL's SSL_shutdown() requires two calls (send, then receive) + - Some implementations block waiting for peer's close_notify; others don't + - Strict implementations treat missing close_notify as truncation attack + + Cross-impl tests skip TLS shutdown to avoid these friction points. This + matches real-world practice where applications often: + - Just close the socket (HTTP/1.0 "connection: close" style) + - Use application-layer signaling (HTTP/2 GOAWAY, gRPC graceful close) + - Accept SSL_ERROR_ZERO_RETURN as success + + Handshake and data transfer prove interoperability; shutdown is orthogonal. + + Testing Methodology + ------------------- + Success cases (run_tls_test_no_shutdown): + - Shared context (both endpoints use same cert/CA) + - Separate contexts (server cert + client trusts CA) + - Anonymous ciphers skipped: syntax differs between implementations + + Failure cases (run_tls_test_fail): + - Peer requires verification, other side has no cert + - Peer requires verification, other side has cert from untrusted CA + + All combinations tested: OpenSSL client <-> WolfSSL server and vice versa. +*/ + +namespace boost { +namespace corosio { + +struct cross_ssl_stream_test +{ +#if defined(BOOST_COROSIO_HAS_OPENSSL) && defined(BOOST_COROSIO_HAS_WOLFSSL) + static auto + make_openssl( io_stream& s, tls::context ctx ) + { + return openssl_stream( s, ctx ); + } + + static auto + make_wolfssl( io_stream& s, tls::context ctx ) + { + return wolfssl_stream( s, ctx ); + } + + void + testCrossImplSuccess() + { + using namespace tls::test; + + // Skip anon mode for cross-impl: anonymous cipher syntax differs between + // OpenSSL and WolfSSL, and WolfSSL may not have anon ciphers compiled in. + // Certificate-based modes test the important interop scenarios. + for( auto mode : { context_mode::shared_cert, + context_mode::separate_cert } ) + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( mode ); + + // OpenSSL client -> WolfSSL server + run_tls_test_no_shutdown( ioc, client_ctx, server_ctx, + make_openssl, make_wolfssl ); + ioc.restart(); + + // WolfSSL client -> OpenSSL server + run_tls_test_no_shutdown( ioc, client_ctx, server_ctx, + make_wolfssl, make_openssl ); + } + } + + void + testCrossImplFailure() + { + using namespace tls::test; + + io_context ioc; + + // OpenSSL client trusts wrong CA, WolfSSL server has cert + { + auto client_ctx = make_wrong_ca_context(); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_openssl, make_wolfssl ); + ioc.restart(); + } + + // WolfSSL client trusts wrong CA, OpenSSL server has cert + { + auto client_ctx = make_wrong_ca_context(); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_wolfssl, make_openssl ); + ioc.restart(); + } + + // OpenSSL client verifies, WolfSSL server has no cert + { + auto client_ctx = make_client_context(); + auto server_ctx = make_anon_context(); + server_ctx.set_ciphersuites( "" ); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_openssl, make_wolfssl ); + ioc.restart(); + } + + // WolfSSL client verifies, OpenSSL server has no cert + { + auto client_ctx = make_client_context(); + auto server_ctx = make_anon_context(); + server_ctx.set_ciphersuites( "" ); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_wolfssl, make_openssl ); + } + } +#endif + + void + run() + { +#if defined(BOOST_COROSIO_HAS_OPENSSL) && defined(BOOST_COROSIO_HAS_WOLFSSL) + testCrossImplSuccess(); + // Failure tests disabled: cancelling the underlying socket doesn't + // propagate to TLS handshake operations - they have their own async + // state machines that don't respond to socket cancellation. When one + // side fails verification, the other side's handshake hangs forever. + // Certificate verification failures are tested in same-implementation + // tests where this issue doesn't occur. + // testCrossImplFailure(); +#else +# if !defined(BOOST_COROSIO_HAS_OPENSSL) + std::cerr << "cross_ssl_stream tests SKIPPED: OpenSSL not found\n"; +# endif +# if !defined(BOOST_COROSIO_HAS_WOLFSSL) + std::cerr << "cross_ssl_stream tests SKIPPED: WolfSSL not found\n"; +# endif +#endif + } +}; + +TEST_SUITE(cross_ssl_stream_test, "boost.corosio.cross_ssl_stream"); + +} // namespace corosio +} // namespace boost diff --git a/test/unit/tls/wolfssl_stream.cpp b/test/unit/tls/wolfssl_stream.cpp index c7e61574..0c1bfa4b 100644 --- a/test/unit/tls/wolfssl_stream.cpp +++ b/test/unit/tls/wolfssl_stream.cpp @@ -1,128 +1,347 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// WolfSSL Implementation Notes -// ---------------------------- -// - Anonymous ciphers: "aNULL:eNULL:@SECLEVEL=0" is OpenSSL syntax, doesn't work -// - WolfSSL anon ciphers require compile-time flags and different cipher string -// - context_mode::anon skipped; shared_cert and separate_cert modes work -// - Failure tests disabled: socket.cancel() doesn't propagate to TLS ops -// - To enable failure tests: need TLS-aware cancellation in wolfssl_stream - -// Test that header file is self-contained. -#include - -#include "test_utils.hpp" -#include "test_suite.hpp" - -namespace boost { -namespace corosio { - -struct wolfssl_stream_test -{ -#ifdef BOOST_COROSIO_HAS_WOLFSSL - static auto - make_stream( io_stream& s, tls::context ctx ) - { - return wolfssl_stream( s, ctx ); - } - - void - testSuccessCases() - { - using namespace tls::test; - - // Skip anon mode: anonymous cipher string "aNULL:eNULL:@SECLEVEL=0" - // is OpenSSL-specific and not supported by WolfSSL. - for( auto mode : { context_mode::shared_cert, - context_mode::separate_cert } ) - { - io_context ioc; - auto [client_ctx, server_ctx] = make_contexts( mode ); - run_tls_test( ioc, client_ctx, server_ctx, - make_stream, make_stream ); - } - } - - void - testFailureCases() - { - using namespace tls::test; - - io_context ioc; - - // Client verifies, server has no cert - { - auto client_ctx = make_client_context(); - auto server_ctx = make_anon_context(); - server_ctx.set_ciphersuites( "" ); // disable anon ciphers - run_tls_test_fail( ioc, client_ctx, server_ctx, - make_stream, make_stream ); - ioc.restart(); - } - - // Client trusts wrong CA - { - auto client_ctx = make_wrong_ca_context(); - auto server_ctx = make_server_context(); - run_tls_test_fail( ioc, client_ctx, server_ctx, - make_stream, make_stream ); - ioc.restart(); - } - } - - void - testTlsShutdown() - { - using namespace tls::test; - - for( auto mode : { context_mode::shared_cert, - context_mode::separate_cert } ) - { - io_context ioc; - auto [client_ctx, server_ctx] = make_contexts( mode ); - run_tls_shutdown_test( ioc, client_ctx, server_ctx, - make_stream, make_stream ); - } - } - - void - testStreamTruncated() - { - using namespace tls::test; - - for( auto mode : { context_mode::shared_cert, - context_mode::separate_cert } ) - { - io_context ioc; - auto [client_ctx, server_ctx] = make_contexts( mode ); - run_tls_truncation_test( ioc, client_ctx, server_ctx, - make_stream, make_stream ); - } - } -#endif - - void - run() - { -#ifdef BOOST_COROSIO_HAS_WOLFSSL - testSuccessCases(); - testTlsShutdown(); - testStreamTruncated(); - // Failure tests disabled: socket cancellation doesn't propagate to - // TLS handshake operations, causing hangs when one side fails. - // testFailureCases(); -#endif - } -}; - -TEST_SUITE(wolfssl_stream_test, "boost.corosio.wolfssl_stream"); - -} // namespace corosio -} // namespace boost +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// WolfSSL Implementation Notes +// ---------------------------- +// - Anonymous ciphers: "aNULL:eNULL:@SECLEVEL=0" is OpenSSL syntax, doesn't work +// - WolfSSL anon ciphers require compile-time flags and different cipher string +// - context_mode::anon skipped; shared_cert and separate_cert modes work +// - Failure tests disabled: socket.cancel() doesn't propagate to TLS ops +// - To enable failure tests: need TLS-aware cancellation in wolfssl_stream + +// Test that header file is self-contained. +#include + +#include "test_utils.hpp" +#include "test_suite.hpp" +#include + +namespace boost { +namespace corosio { + +struct wolfssl_stream_test +{ +#ifdef BOOST_COROSIO_HAS_WOLFSSL + static auto + make_stream( io_stream& s, tls::context ctx ) + { + return wolfssl_stream( s, ctx ); + } + + void + testSuccessCases() + { + using namespace tls::test; + + // Skip anon mode: anonymous cipher string "aNULL:eNULL:@SECLEVEL=0" + // is OpenSSL-specific and not supported by WolfSSL. + for( auto mode : { context_mode::shared_cert, + context_mode::separate_cert } ) + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( mode ); + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testFailureCases() + { + using namespace tls::test; + + io_context ioc; + + // Client verifies, server has no cert + { + auto client_ctx = make_client_context(); + auto server_ctx = make_anon_context(); + server_ctx.set_ciphersuites( "" ); // disable anon ciphers + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + ioc.restart(); + } + + // Client trusts wrong CA + { + auto client_ctx = make_wrong_ca_context(); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + ioc.restart(); + } + } + + void + testTlsShutdown() + { + using namespace tls::test; + + for( auto mode : { context_mode::shared_cert, + context_mode::separate_cert } ) + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( mode ); + run_tls_shutdown_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testStreamTruncated() + { + using namespace tls::test; + + for( auto mode : { context_mode::shared_cert, + context_mode::separate_cert } ) + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( mode ); + run_tls_truncation_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testStopTokenCancellation() + { + using namespace tls::test; + + // Cancel during handshake + { + io_context ioc; + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + run_stop_token_handshake_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Cancel during read + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( context_mode::separate_cert ); + run_stop_token_read_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Cancel during write + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( context_mode::separate_cert ); + run_stop_token_write_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testSocketErrorPropagation() + { + using namespace tls::test; + + // socket.cancel() while TLS blocked on socket I/O + { + io_context ioc; + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + run_socket_cancel_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Connection reset during handshake + { + io_context ioc; + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + run_connection_reset_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testCertificateValidation() + { + using namespace tls::test; + + // Untrusted CA - client trusts different CA than server's cert + // Should fail immediately during certificate verification + { + io_context ioc; + auto client_ctx = make_untrusted_ca_client_context(); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Expired certificate - server cert expired Jan 2, 2020 + // Client trusts the cert but should reject due to expiry + { + io_context ioc; + auto client_ctx = make_expired_client_context(); + auto server_ctx = make_expired_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testSni() + { + using namespace tls::test; + + // Test SNI + hostname verification - correct hostname succeeds + // Server cert has CN=www.example.com + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "www.example.com" ); + auto server_ctx = make_server_context(); + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Test hostname verification - wrong hostname fails + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "wrong.example.com" ); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testSniCallback() + { + using namespace tls::test; + + // SNI callback accepts the hostname - handshake succeeds + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "www.example.com" ); + + auto server_ctx = make_server_context(); + server_ctx.set_servername_callback( + []( std::string_view hostname ) -> bool + { + return hostname == "www.example.com"; + }); + + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // SNI callback rejects the hostname - handshake fails + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "www.example.com" ); + + auto server_ctx = make_server_context(); + server_ctx.set_servername_callback( + []( std::string_view hostname ) -> bool + { + return hostname == "api.example.com"; // Only accept api.* + }); + + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testMtls() + { + using namespace tls::test; + + // mTLS success - client provides valid cert + { + io_context ioc; + auto client_ctx = make_mtls_client_context(); + auto server_ctx = make_mtls_server_context(); + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // mTLS failure - client provides no cert but server requires it + { + io_context ioc; + auto client_ctx = make_chain_client_context(); + auto server_ctx = make_mtls_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // mTLS failure - client provides cert signed by WRONG CA + { + io_context ioc; + auto client_ctx = make_invalid_mtls_client_context(); + auto server_ctx = make_mtls_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testCertificateChain() + { + using namespace tls::test; + + // Basic chain test: client trusts both CAs, server sends entity cert only + { + io_context ioc; + auto client_ctx = make_chain_client_context(); // trusts root + intermediate + auto server_ctx = make_chain_server_context(); // entity cert only + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Server sends only entity cert - client trusts only root + // Should fail because client can't build chain to root + { + io_context ioc; + auto client_ctx = make_rootonly_client_context(); + auto server_ctx = make_chain_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Note: Fullchain test (server sends chain, client trusts only root) is + // disabled for WolfSSL due to wolfSSL_CTX_add_extra_chain_cert not properly + // sending intermediates during handshake. OpenSSL version works correctly. + } +#endif + + void + run() + { +#ifdef BOOST_COROSIO_HAS_WOLFSSL + testSuccessCases(); + testTlsShutdown(); + testStreamTruncated(); + testFailureCases(); + testStopTokenCancellation(); + testSocketErrorPropagation(); + testCertificateValidation(); + testSni(); + testSniCallback(); + testMtls(); + testCertificateChain(); +#else + std::cerr << "wolfssl_stream tests SKIPPED: WolfSSL not found\n"; + static_assert(false, "WolfSSL not found"); +#endif + } +}; + +TEST_SUITE(wolfssl_stream_test, "boost.corosio.wolfssl_stream"); + +} // namespace corosio +} // namespace boost From a7490ae265a0014d76734c6cd72f3da999513012 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 23 Jan 2026 15:53:43 +0000 Subject: [PATCH 2/9] fix: make_socket_pair port allocation for parallel test execution The previous implementation used a non-atomic counter with a narrow port range (100 ports), causing Address already in use errors when: - Tests run in parallel (ctest -j) - Ports remain in TCP TIME_WAIT from previous test runs Changes: - Use std::atomic for thread-safe port counter - Expand port range to full ephemeral range (49152-65535) - Add retry logic (up to 20 attempts) when bind fails This fixes intermittent CI failures where openssl_stream and other tests using make_socket_pair would abort with EADDRINUSE. --- src/corosio/src/test/socket_pair.cpp | 221 +++++++++++++++------------ 1 file changed, 121 insertions(+), 100 deletions(-) diff --git a/src/corosio/src/test/socket_pair.cpp b/src/corosio/src/test/socket_pair.cpp index 9072cf55..2f34c492 100644 --- a/src/corosio/src/test/socket_pair.cpp +++ b/src/corosio/src/test/socket_pair.cpp @@ -1,100 +1,121 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost { -namespace corosio { -namespace test { - -namespace { - -constexpr std::uint16_t test_port_base = 49300; -constexpr std::uint16_t test_port_range = 100; -std::uint16_t next_test_port = 0; - -std::uint16_t -get_test_port() noexcept -{ - auto port = test_port_base + (next_test_port % test_port_range); - ++next_test_port; - return static_cast(port); -} - -} // namespace - -std::pair -make_socket_pair(io_context& ioc) -{ - auto ex = ioc.get_executor(); - std::uint16_t port = get_test_port(); - - system::error_code accept_ec; - system::error_code connect_ec; - bool accept_done = false; - bool connect_done = false; - - acceptor acc(ioc); - acc.listen(endpoint(urls::ipv4_address::loopback(), port)); - - socket s1(ioc); - socket s2(ioc); - s2.open(); - - capy::run_async(ex)( - [](acceptor& a, socket& s, - system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await a.accept(s); - ec_out = ec; - done_out = true; - }(acc, s1, accept_ec, accept_done)); - - capy::run_async(ex)( - [](socket& s, endpoint ep, - system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await s.connect(ep); - ec_out = ec; - done_out = true; - }(s2, endpoint(urls::ipv4_address::loopback(), port), - connect_ec, connect_done)); - - ioc.run(); - ioc.restart(); - - if (!accept_done || accept_ec) - { - acc.close(); - throw std::runtime_error("socket_pair accept failed"); - } - - if (!connect_done || connect_ec) - { - acc.close(); - s1.close(); - throw std::runtime_error("socket_pair connect failed"); - } - - acc.close(); - - return {std::move(s1), std::move(s2)}; -} - -} // namespace test -} // namespace corosio -} // namespace boost +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost { +namespace corosio { +namespace test { + +namespace { + +// Use atomic for thread safety when tests run in parallel +std::atomic next_test_port{0}; + +std::uint16_t +get_test_port() noexcept +{ + // Use a wide port range in the dynamic/ephemeral range (49152-65535) + constexpr std::uint16_t port_base = 49152; + constexpr std::uint16_t port_range = 16383; // 49152-65535 + auto offset = next_test_port.fetch_add(1, std::memory_order_relaxed); + return static_cast(port_base + (offset % port_range)); +} + +} // namespace + +std::pair +make_socket_pair(io_context& ioc) +{ + auto ex = ioc.get_executor(); + + system::error_code accept_ec; + system::error_code connect_ec; + bool accept_done = false; + bool connect_done = false; + + // Try multiple ports in case of conflicts (TIME_WAIT, parallel tests, etc.) + std::uint16_t port = 0; + acceptor acc(ioc); + bool listening = false; + for (int attempt = 0; attempt < 20; ++attempt) + { + port = get_test_port(); + try + { + acc.listen(endpoint(urls::ipv4_address::loopback(), port)); + listening = true; + break; + } + catch (const system::system_error&) + { + // Port in use, try another + acc.close(); + acc = acceptor(ioc); + } + } + if (!listening) + throw std::runtime_error("socket_pair: failed to find available port"); + + socket s1(ioc); + socket s2(ioc); + s2.open(); + + capy::run_async(ex)( + [](acceptor& a, socket& s, + system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await a.accept(s); + ec_out = ec; + done_out = true; + }(acc, s1, accept_ec, accept_done)); + + capy::run_async(ex)( + [](socket& s, endpoint ep, + system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await s.connect(ep); + ec_out = ec; + done_out = true; + }(s2, endpoint(urls::ipv4_address::loopback(), port), + connect_ec, connect_done)); + + ioc.run(); + ioc.restart(); + + if (!accept_done || accept_ec) + { + acc.close(); + throw std::runtime_error("socket_pair accept failed"); + } + + if (!connect_done || connect_ec) + { + acc.close(); + s1.close(); + throw std::runtime_error("socket_pair connect failed"); + } + + acc.close(); + + return {std::move(s1), std::move(s2)}; +} + +} // namespace test +} // namespace corosio +} // namespace boost From d249ac33a15f9c06996acec128e526ca1df73112 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 23 Jan 2026 15:56:28 +0000 Subject: [PATCH 3/9] fix: epoll backend stop_token cancellation now actually cancels I/O Previously, when std::stop_token was signaled on the epoll backend, the stop_callback only set a cancelled flag. The I/O operation remained blocked on epoll_wait indefinitely, requiring the failsafe timer or explicit socket.cancel() to interrupt it. This fix implements proper stop_token cancellation for epoll: - Add cancel_single_op() to epoll_socket_impl and epoll_acceptor_impl that unregisters the fd from epoll and posts the operation for completion with cancellation error - Store socket/acceptor impl pointer in epoll_op so the stop_callback can call back to perform actual cancellation - Fix race condition where stop is requested before the operation registers with epoll: after register_fd(), check if cancelled was already set and handle it immediately - Implement canceller::operator() out-of-line to call cancel_single_op This makes stop_token cancellation behave consistently with IOCP (Windows) where CancelIoEx provides immediate kernel-level cancellation. --- src/corosio/src/detail/epoll/op.hpp | 37 +- src/corosio/src/detail/epoll/sockets.cpp | 139 +- src/corosio/src/detail/epoll/sockets.hpp | 2 + test/unit/acceptor.cpp | 8 +- test/unit/signal_set.cpp | 1662 +++++++++++----------- test/unit/socket.cpp | 154 +- test/unit/timer.cpp | 1326 ++++++++--------- 7 files changed, 1786 insertions(+), 1542 deletions(-) diff --git a/src/corosio/src/detail/epoll/op.hpp b/src/corosio/src/detail/epoll/op.hpp index 37f34925..4f258b44 100644 --- a/src/corosio/src/detail/epoll/op.hpp +++ b/src/corosio/src/detail/epoll/op.hpp @@ -80,12 +80,16 @@ namespace boost { namespace corosio { namespace detail { +// Forward declarations for cancellation support +class epoll_socket_impl; +class epoll_acceptor_impl; + struct epoll_op : scheduler_op { struct canceller { epoll_op* op; - void operator()() const noexcept { op->request_cancel(); } + void operator()() const noexcept; }; capy::coro h; @@ -105,6 +109,11 @@ struct epoll_op : scheduler_op // See "Impl Lifetime Management" in file header. std::shared_ptr impl_ptr; + // For stop_token cancellation - pointer to owning socket/acceptor impl. + // When stop is requested, we call back to the impl to perform actual I/O cancellation. + epoll_socket_impl* socket_impl_ = nullptr; + epoll_acceptor_impl* acceptor_impl_ = nullptr; + epoll_op() { data_ = this; @@ -118,6 +127,8 @@ struct epoll_op : scheduler_op cancelled.store(false, std::memory_order_relaxed); registered.store(false, std::memory_order_relaxed); impl_ptr.reset(); + socket_impl_ = nullptr; + acceptor_impl_ = nullptr; } void operator()() override @@ -160,6 +171,30 @@ struct epoll_op : scheduler_op { cancelled.store(false, std::memory_order_release); stop_cb.reset(); + socket_impl_ = nullptr; + acceptor_impl_ = nullptr; + + if (token.stop_possible()) + stop_cb.emplace(token, canceller{this}); + } + + void start(std::stop_token token, epoll_socket_impl* impl) + { + cancelled.store(false, std::memory_order_release); + stop_cb.reset(); + socket_impl_ = impl; + acceptor_impl_ = nullptr; + + if (token.stop_possible()) + stop_cb.emplace(token, canceller{this}); + } + + void start(std::stop_token token, epoll_acceptor_impl* impl) + { + cancelled.store(false, std::memory_order_release); + stop_cb.reset(); + socket_impl_ = nullptr; + acceptor_impl_ = impl; if (token.stop_possible()) stop_cb.emplace(token, canceller{this}); diff --git a/src/corosio/src/detail/epoll/sockets.cpp b/src/corosio/src/detail/epoll/sockets.cpp index af3f7e74..7e98a5f6 100644 --- a/src/corosio/src/detail/epoll/sockets.cpp +++ b/src/corosio/src/detail/epoll/sockets.cpp @@ -27,6 +27,24 @@ namespace boost { namespace corosio { namespace detail { +//------------------------------------------------------------------------------ +// epoll_op::canceller - implements stop_token cancellation +//------------------------------------------------------------------------------ + +void +epoll_op::canceller:: +operator()() const noexcept +{ + // When stop_token is signaled, we need to actually cancel the I/O operation, + // not just set a flag. Otherwise the operation stays blocked on epoll. + if (op->socket_impl_) + op->socket_impl_->cancel_single_op(*op); + else if (op->acceptor_impl_) + op->acceptor_impl_->cancel_single_op(*op); + else + op->request_cancel(); // fallback: just set flag (legacy behavior) +} + //------------------------------------------------------------------------------ // epoll_socket_impl //------------------------------------------------------------------------------ @@ -60,7 +78,7 @@ connect( op.d = d; op.ec_out = ec; op.fd = fd_; - op.start(token); + op.start(token, this); sockaddr_in addr = detail::to_sockaddr_in(ep); int result = ::connect(fd_, reinterpret_cast(&addr), sizeof(addr)); @@ -68,6 +86,7 @@ connect( if (result == 0) { op.complete(0, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -77,10 +96,24 @@ connect( svc_.work_started(); op.registered.store(true, std::memory_order_release); svc_.scheduler().register_fd(fd_, &op, EPOLLOUT | EPOLLET); + + // If cancelled was set before we registered, handle it now. + if (op.cancelled.load(std::memory_order_acquire)) + { + bool was_registered = op.registered.exchange(false, std::memory_order_acq_rel); + if (was_registered) + { + svc_.scheduler().unregister_fd(fd_); + op.impl_ptr = shared_from_this(); + svc_.post(&op); + svc_.work_finished(); + } + } return; } op.complete(errno, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); } @@ -101,7 +134,7 @@ read_some( op.ec_out = ec; op.bytes_out = bytes_out; op.fd = fd_; - op.start(token); + op.start(token, this); capy::mutable_buffer bufs[epoll_read_op::max_buffers]; op.iovec_count = static_cast(param.copy_to(bufs, epoll_read_op::max_buffers)); @@ -110,6 +143,7 @@ read_some( { op.empty_buffer_read = true; op.complete(0, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -125,6 +159,7 @@ read_some( if (n > 0) { op.complete(0, static_cast(n)); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -132,6 +167,7 @@ read_some( if (n == 0) { op.complete(0, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -141,10 +177,25 @@ read_some( svc_.work_started(); op.registered.store(true, std::memory_order_release); svc_.scheduler().register_fd(fd_, &op, EPOLLIN | EPOLLET); + + // If cancelled was set before we registered, the stop_callback couldn't + // post us because we weren't registered yet. Handle it now. + if (op.cancelled.load(std::memory_order_acquire)) + { + bool was_registered = op.registered.exchange(false, std::memory_order_acq_rel); + if (was_registered) + { + svc_.scheduler().unregister_fd(fd_); + op.impl_ptr = shared_from_this(); + svc_.post(&op); + svc_.work_finished(); + } + } return; } op.complete(errno, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); } @@ -165,7 +216,7 @@ write_some( op.ec_out = ec; op.bytes_out = bytes_out; op.fd = fd_; - op.start(token); + op.start(token, this); capy::mutable_buffer bufs[epoll_write_op::max_buffers]; op.iovec_count = static_cast(param.copy_to(bufs, epoll_write_op::max_buffers)); @@ -173,6 +224,7 @@ write_some( if (op.iovec_count == 0 || (op.iovec_count == 1 && bufs[0].size() == 0)) { op.complete(0, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -192,6 +244,7 @@ write_some( if (n > 0) { op.complete(0, static_cast(n)); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -201,10 +254,24 @@ write_some( svc_.work_started(); op.registered.store(true, std::memory_order_release); svc_.scheduler().register_fd(fd_, &op, EPOLLOUT | EPOLLET); + + // If cancelled was set before we registered, handle it now. + if (op.cancelled.load(std::memory_order_acquire)) + { + bool was_registered = op.registered.exchange(false, std::memory_order_acq_rel); + if (was_registered) + { + svc_.scheduler().unregister_fd(fd_); + op.impl_ptr = shared_from_this(); + svc_.post(&op); + svc_.work_finished(); + } + } return; } op.complete(errno ? errno : EIO, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); } @@ -254,6 +321,31 @@ cancel() noexcept cancel_op(wr_); } +void +epoll_socket_impl:: +cancel_single_op(epoll_op& op) noexcept +{ + // Called from stop_token callback to cancel a specific pending operation. + // This performs actual I/O cancellation, not just setting a flag. + bool was_registered = op.registered.exchange(false, std::memory_order_acq_rel); + op.request_cancel(); + + if (was_registered) + { + svc_.scheduler().unregister_fd(fd_); + + // Keep impl alive until op completes + try { + op.impl_ptr = shared_from_this(); + } catch (const std::bad_weak_ptr&) { + // Impl is being destroyed, op will be orphaned but that's ok + } + + svc_.post(&op); + svc_.work_finished(); + } +} + void epoll_socket_impl:: close_socket() noexcept @@ -301,7 +393,7 @@ accept( op.ec_out = ec; op.impl_out = impl_out; op.fd = fd_; - op.start(token); + op.start(token, this); op.service_ptr = &svc_; op.create_peer = [](void* svc_ptr, int new_fd) -> io_object::io_object_impl* { @@ -323,6 +415,7 @@ accept( op.accepted_fd = accepted; op.peer_impl = &peer_impl; op.complete(0, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); return; } @@ -332,10 +425,24 @@ accept( svc_.work_started(); op.registered.store(true, std::memory_order_release); svc_.scheduler().register_fd(fd_, &op, EPOLLIN | EPOLLET); + + // If cancelled was set before we registered, handle it now. + if (op.cancelled.load(std::memory_order_acquire)) + { + bool was_registered = op.registered.exchange(false, std::memory_order_acq_rel); + if (was_registered) + { + svc_.scheduler().unregister_fd(fd_); + op.impl_ptr = shared_from_this(); + svc_.post(&op); + svc_.work_finished(); + } + } return; } op.complete(errno, 0); + op.impl_ptr = shared_from_this(); svc_.post(&op); } @@ -362,6 +469,30 @@ cancel() noexcept } } +void +epoll_acceptor_impl:: +cancel_single_op(epoll_op& op) noexcept +{ + // Called from stop_token callback to cancel a specific pending operation. + bool was_registered = op.registered.exchange(false, std::memory_order_acq_rel); + op.request_cancel(); + + if (was_registered) + { + svc_.scheduler().unregister_fd(fd_); + + // Keep impl alive until op completes + try { + op.impl_ptr = shared_from_this(); + } catch (const std::bad_weak_ptr&) { + // Impl is being destroyed, op will be orphaned but that's ok + } + + svc_.post(&op); + svc_.work_finished(); + } +} + void epoll_acceptor_impl:: close_socket() noexcept diff --git a/src/corosio/src/detail/epoll/sockets.hpp b/src/corosio/src/detail/epoll/sockets.hpp index d1db3a9e..49096329 100644 --- a/src/corosio/src/detail/epoll/sockets.hpp +++ b/src/corosio/src/detail/epoll/sockets.hpp @@ -130,6 +130,7 @@ class epoll_socket_impl native_handle_type native_handle() const noexcept override { return fd_; } bool is_open() const noexcept { return fd_ >= 0; } void cancel() noexcept; + void cancel_single_op(epoll_op& op) noexcept; void close_socket() noexcept; void set_socket(int fd) noexcept { fd_ = fd; } @@ -166,6 +167,7 @@ class epoll_acceptor_impl int native_handle() const noexcept { return fd_; } bool is_open() const noexcept { return fd_ >= 0; } void cancel() noexcept; + void cancel_single_op(epoll_op& op) noexcept; void close_socket() noexcept; epoll_accept_op acc_; diff --git a/test/unit/acceptor.cpp b/test/unit/acceptor.cpp index 62f69ffe..474fbeb8 100644 --- a/test/unit/acceptor.cpp +++ b/test/unit/acceptor.cpp @@ -123,13 +123,13 @@ struct acceptor_test capy::run_async(ioc.get_executor())(nested_coro()); // Wait for timer then cancel - (void) co_await t.wait(); + (void)co_await t.wait(); acc.cancel(); // Wait for accept to complete timer t2(ioc); t2.expires_after(std::chrono::milliseconds(50)); - (void) co_await t2.wait(); + (void)co_await t2.wait(); BOOST_TEST(accept_done); BOOST_TEST(accept_ec == capy::cond::canceled); @@ -175,12 +175,12 @@ struct acceptor_test capy::run_async(ioc.get_executor())(nested_coro()); // Wait then close the acceptor - (void) co_await t.wait(); + (void)co_await t.wait(); acc.close(); timer t2(ioc); t2.expires_after(std::chrono::milliseconds(50)); - (void) co_await t2.wait(); + (void)co_await t2.wait(); BOOST_TEST(accept_done); BOOST_TEST(accept_ec == capy::cond::canceled); diff --git a/test/unit/signal_set.cpp b/test/unit/signal_set.cpp index f14eecf8..b0338914 100644 --- a/test/unit/signal_set.cpp +++ b/test/unit/signal_set.cpp @@ -1,831 +1,831 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include "test_suite.hpp" - -namespace boost { -namespace corosio { - -//------------------------------------------------ -// Signal set tests -// Focus: construction, add/remove, wait, and cancellation -//------------------------------------------------ - -struct signal_set_test -{ - //-------------------------------------------- - // Construction and move semantics - //-------------------------------------------- - - void - testConstruction() - { - io_context ioc; - signal_set s(ioc); - - BOOST_TEST_PASS(); - } - - void - testConstructWithOneSignal() - { - io_context ioc; - signal_set s(ioc, SIGINT); - - BOOST_TEST_PASS(); - } - - void - testConstructWithTwoSignals() - { - io_context ioc; - signal_set s(ioc, SIGINT, SIGTERM); - - BOOST_TEST_PASS(); - } - - void - testConstructWithThreeSignals() - { - io_context ioc; - signal_set s(ioc, SIGINT, SIGTERM, SIGABRT); - - BOOST_TEST_PASS(); - } - - void - testMoveConstruct() - { - io_context ioc; - signal_set s1(ioc, SIGINT); - - signal_set s2(std::move(s1)); - BOOST_TEST_PASS(); - } - - void - testMoveAssign() - { - io_context ioc; - signal_set s1(ioc, SIGINT); - signal_set s2(ioc); - - s2 = std::move(s1); - BOOST_TEST_PASS(); - } - - void - testMoveAssignCrossContextThrows() - { - io_context ioc1; - io_context ioc2; - signal_set s1(ioc1); - signal_set s2(ioc2); - - BOOST_TEST_THROWS(s2 = std::move(s1), std::logic_error); - } - - //-------------------------------------------- - // Add/remove/clear tests - //-------------------------------------------- - - void - testAdd() - { - io_context ioc; - signal_set s(ioc); - - auto result = s.add(SIGINT); - BOOST_TEST(result.has_value()); - } - - void - testAddDuplicate() - { - io_context ioc; - signal_set s(ioc); - - BOOST_TEST(s.add(SIGINT).has_value()); - auto result = s.add(SIGINT); // Should be no-op - BOOST_TEST(result.has_value()); - } - - void - testAddInvalidSignal() - { - io_context ioc; - signal_set s(ioc); - - auto result = s.add(-1); - BOOST_TEST(result.has_error()); - } - - void - testRemove() - { - io_context ioc; - signal_set s(ioc); - - BOOST_TEST(s.add(SIGINT).has_value()); - auto result = s.remove(SIGINT); - BOOST_TEST(result.has_value()); - } - - void - testRemoveNotPresent() - { - io_context ioc; - signal_set s(ioc); - - // Removing signal not in set should be a no-op - auto result = s.remove(SIGINT); - BOOST_TEST(result.has_value()); - } - - void - testClear() - { - io_context ioc; - signal_set s(ioc); - - BOOST_TEST(s.add(SIGINT).has_value()); - BOOST_TEST(s.add(SIGTERM).has_value()); - BOOST_TEST(s.clear().has_value()); - } - - void - testClearEmpty() - { - io_context ioc; - signal_set s(ioc); - - BOOST_TEST(s.clear().has_value()); // Should be no-op - } - - //-------------------------------------------- - // Async wait tests - //-------------------------------------------- - - void - testWaitWithSignal() - { - io_context ioc; - signal_set s(ioc, SIGINT); - timer t(ioc); - - bool completed = false; - int received_signal = 0; - system::error_code result_ec; - - auto wait_task = [](signal_set& s_ref, system::error_code& ec_out, int& sig_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - ec_out = ec; - sig_out = signum; - done_out = true; - }; - capy::run_async(ioc.get_executor())(wait_task(s, result_ec, received_signal, completed)); - - // Raise signal after a short delay - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - std::raise(SIGINT); - }; - capy::run_async(ioc.get_executor())(raise_task(t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - BOOST_TEST_EQ(received_signal, SIGINT); - } - - void - testWaitWithDifferentSignal() - { - io_context ioc; - signal_set s(ioc, SIGTERM); - timer t(ioc); - - bool completed = false; - int received_signal = 0; - - auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - sig_out = signum; - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); - - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - std::raise(SIGTERM); - }; - capy::run_async(ioc.get_executor())(raise_task(t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST_EQ(received_signal, SIGTERM); - } - - //-------------------------------------------- - // Cancellation tests - //-------------------------------------------- - - void - testCancel() - { - io_context ioc; - signal_set s(ioc, SIGINT); - timer cancel_timer(ioc); - - bool completed = false; - system::error_code result_ec; - - auto wait_task = [](signal_set& s_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - ec_out = ec; - done_out = true; - (void)signum; - }; - capy::run_async(ioc.get_executor())(wait_task(s, result_ec, completed)); - - cancel_timer.expires_after(std::chrono::milliseconds(10)); - auto cancel_task = [](timer& t_ref, signal_set& s_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - s_ref.cancel(); - }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, s)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void - testCancelNoWaiters() - { - io_context ioc; - signal_set s(ioc, SIGINT); - - s.cancel(); // Should be no-op - BOOST_TEST_PASS(); - } - - void - testCancelMultipleTimes() - { - io_context ioc; - signal_set s(ioc, SIGINT); - - s.cancel(); - s.cancel(); - s.cancel(); - BOOST_TEST_PASS(); - } - - //-------------------------------------------- - // Multiple signal set tests - //-------------------------------------------- - - void - testMultipleSignalSetsOnSameSignal() - { - io_context ioc; - signal_set s1(ioc, SIGINT); - signal_set s2(ioc, SIGINT); - timer t(ioc); - - bool s1_completed = false; - bool s2_completed = false; - int s1_signal = 0; - int s2_signal = 0; - - auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - sig_out = signum; - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(wait_task(s1, s1_signal, s1_completed)); - capy::run_async(ioc.get_executor())(wait_task(s2, s2_signal, s2_completed)); - - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - std::raise(SIGINT); - }; - capy::run_async(ioc.get_executor())(raise_task(t)); - - ioc.run(); - BOOST_TEST(s1_completed); - BOOST_TEST(s2_completed); - BOOST_TEST_EQ(s1_signal, SIGINT); - BOOST_TEST_EQ(s2_signal, SIGINT); - } - - void - testSignalSetWithMultipleSignals() - { - io_context ioc; - signal_set s(ioc, SIGINT, SIGTERM); - timer t(ioc); - - bool completed = false; - int received_signal = 0; - - auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - sig_out = signum; - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); - - // Raise SIGTERM (not SIGINT) - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - std::raise(SIGTERM); - }; - capy::run_async(ioc.get_executor())(raise_task(t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST_EQ(received_signal, SIGTERM); - } - - //-------------------------------------------- - // Queued signal tests - //-------------------------------------------- - - void - testQueuedSignal() - { - io_context ioc; - signal_set s(ioc, SIGINT); - - // Raise signal before starting wait - std::raise(SIGINT); - - bool completed = false; - int received_signal = 0; - - auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - sig_out = signum; - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST_EQ(received_signal, SIGINT); - } - - //-------------------------------------------- - // Sequential wait tests - //-------------------------------------------- - - void - testSequentialWaits() - { - io_context ioc; - signal_set s(ioc, SIGINT); - timer t(ioc); - - int wait_count = 0; - - auto task = [](signal_set& s_ref, timer& t_ref, int& count_out) -> capy::task<> - { - // First wait - t_ref.expires_after(std::chrono::milliseconds(5)); - (void) co_await t_ref.wait(); - std::raise(SIGINT); - - auto [ec1, sig1] = co_await s_ref.async_wait(); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(sig1, SIGINT); - ++count_out; - - // Second wait - t_ref.expires_after(std::chrono::milliseconds(5)); - (void) co_await t_ref.wait(); - std::raise(SIGINT); - - auto [ec2, sig2] = co_await s_ref.async_wait(); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(sig2, SIGINT); - ++count_out; - }; - capy::run_async(ioc.get_executor())(task(s, t, wait_count)); - - ioc.run(); - BOOST_TEST_EQ(wait_count, 2); - } - - //-------------------------------------------- - // io_result tests - //-------------------------------------------- - - void - testIoResultSuccess() - { - io_context ioc; - signal_set s(ioc, SIGINT); - timer t(ioc); - - bool result_ok = false; - - auto task = [](signal_set& s_ref, timer& t_ref, bool& ok_out) -> capy::task<> - { - t_ref.expires_after(std::chrono::milliseconds(5)); - (void) co_await t_ref.wait(); - std::raise(SIGINT); - - auto result = co_await s_ref.async_wait(); - ok_out = !result.ec; - }; - capy::run_async(ioc.get_executor())(task(s, t, result_ok)); - - ioc.run(); - BOOST_TEST(result_ok); - } - - void - testIoResultCanceled() - { - io_context ioc; - signal_set s(ioc, SIGINT); - timer cancel_timer(ioc); - - bool result_ok = true; - system::error_code result_ec; - - auto wait_task = [](signal_set& s_ref, bool& ok_out, system::error_code& ec_out) -> capy::task<> - { - auto result = co_await s_ref.async_wait(); - ok_out = !result.ec; - ec_out = result.ec; - }; - capy::run_async(ioc.get_executor())(wait_task(s, result_ok, result_ec)); - - cancel_timer.expires_after(std::chrono::milliseconds(10)); - auto cancel_task = [](timer& t_ref, signal_set& s_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - s_ref.cancel(); - }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, s)); - - ioc.run(); - BOOST_TEST(!result_ok); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void - testIoResultStructuredBinding() - { - io_context ioc; - signal_set s(ioc, SIGINT); - timer t(ioc); - - system::error_code captured_ec; - int captured_signal = 0; - - auto task = [](signal_set& s_ref, timer& t_ref, system::error_code& ec_out, int& sig_out) -> capy::task<> - { - t_ref.expires_after(std::chrono::milliseconds(5)); - (void) co_await t_ref.wait(); - std::raise(SIGINT); - - auto [ec, signum] = co_await s_ref.async_wait(); - ec_out = ec; - sig_out = signum; - }; - capy::run_async(ioc.get_executor())(task(s, t, captured_ec, captured_signal)); - - ioc.run(); - BOOST_TEST(!captured_ec); - BOOST_TEST_EQ(captured_signal, SIGINT); - } - - //-------------------------------------------- - // Signal flags tests (cross-platform) - //-------------------------------------------- - - void - testFlagsBitwiseOperations() - { - // Test OR - auto combined = signal_set::restart | signal_set::no_defer; - BOOST_TEST((combined & signal_set::restart) != signal_set::none); - BOOST_TEST((combined & signal_set::no_defer) != signal_set::none); - BOOST_TEST((combined & signal_set::no_child_stop) == signal_set::none); - - // Test compound assignment - auto flags = signal_set::none; - flags |= signal_set::restart; - BOOST_TEST((flags & signal_set::restart) != signal_set::none); - - // Test NOT - auto all_but_restart = ~signal_set::restart; - BOOST_TEST((all_but_restart & signal_set::restart) == signal_set::none); - } - - void - testAddWithNoneFlags() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with none (default behavior) - works on all platforms - auto result = s.add(SIGINT, signal_set::none); - BOOST_TEST(result.has_value()); - } - - void - testAddWithDontCareFlags() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with dont_care - works on all platforms - auto result = s.add(SIGINT, signal_set::dont_care); - BOOST_TEST(result.has_value()); - } - -#if !defined(_WIN32) - //-------------------------------------------- - // Signal flags tests (POSIX only) - // Windows returns operation_not_supported for - // flags other than none/dont_care - //-------------------------------------------- - - void - testAddWithFlags() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with restart flag - auto result = s.add(SIGINT, signal_set::restart); - BOOST_TEST(result.has_value()); - } - - void - testAddWithMultipleFlags() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with combined flags - auto result = s.add(SIGINT, signal_set::restart | signal_set::no_defer); - BOOST_TEST(result.has_value()); - } - - void - testAddSameSignalSameFlags() - { - io_context ioc; - signal_set s(ioc); - - // Add signal twice with same flags (should be no-op) - BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); - BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); - } - - void - testAddSameSignalDifferentFlags() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with one flag, then try to add with different flag - BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); - auto result = s.add(SIGINT, signal_set::no_defer); - BOOST_TEST(result.has_error()); // Should fail due to flag mismatch - } - - void - testAddSameSignalWithDontCare() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with specific flags, then add with dont_care - BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); - auto result = s.add(SIGINT, signal_set::dont_care); - BOOST_TEST(result.has_value()); // Should succeed with dont_care - } - - void - testAddSameSignalDontCareFirst() - { - io_context ioc; - signal_set s(ioc); - - // Add signal with dont_care, then add with specific flags - BOOST_TEST(s.add(SIGINT, signal_set::dont_care).has_value()); - auto result = s.add(SIGINT, signal_set::restart); - BOOST_TEST(result.has_value()); // Should succeed - } - - void - testMultipleSetsCompatibleFlags() - { - io_context ioc; - signal_set s1(ioc); - signal_set s2(ioc); - - // Both sets add same signal with same flags - BOOST_TEST(s1.add(SIGINT, signal_set::restart).has_value()); - BOOST_TEST(s2.add(SIGINT, signal_set::restart).has_value()); - } - - void - testMultipleSetsIncompatibleFlags() - { - io_context ioc; - signal_set s1(ioc); - signal_set s2(ioc); - - // First set adds with one flag - BOOST_TEST(s1.add(SIGINT, signal_set::restart).has_value()); - // Second set tries to add with different flag - auto result = s2.add(SIGINT, signal_set::no_defer); - BOOST_TEST(result.has_error()); // Should fail - } - - void - testMultipleSetsWithDontCare() - { - io_context ioc; - signal_set s1(ioc); - signal_set s2(ioc); - - // First set adds with specific flags - BOOST_TEST(s1.add(SIGINT, signal_set::restart).has_value()); - // Second set adds with dont_care - BOOST_TEST(s2.add(SIGINT, signal_set::dont_care).has_value()); - } - - void - testWaitWithFlagsWorks() - { - io_context ioc; - signal_set s(ioc); - timer t(ioc); - - // Add signal with restart flag and verify wait still works - BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); - - bool completed = false; - int received_signal = 0; - - auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> - { - auto [ec, signum] = co_await s_ref.async_wait(); - sig_out = signum; - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); - - t.expires_after(std::chrono::milliseconds(10)); - auto raise_task = [](timer& t_ref) -> capy::task<> - { - (void) co_await t_ref.wait(); - std::raise(SIGINT); - }; - capy::run_async(ioc.get_executor())(raise_task(t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST_EQ(received_signal, SIGINT); - } - -#else // _WIN32 - //-------------------------------------------- - // Signal flags tests (Windows only) - //-------------------------------------------- - - void - testFlagsNotSupportedOnWindows() - { - io_context ioc; - signal_set s(ioc); - - // Windows returns operation_not_supported for actual flags - auto result = s.add(SIGINT, signal_set::restart); - BOOST_TEST(result.has_error()); - BOOST_TEST(result.error() == system::errc::operation_not_supported); - } - -#endif // _WIN32 - - void - run() - { - // Construction and move semantics - testConstruction(); - testConstructWithOneSignal(); - testConstructWithTwoSignals(); - testConstructWithThreeSignals(); - testMoveConstruct(); - testMoveAssign(); - testMoveAssignCrossContextThrows(); - - // Add/remove/clear tests - testAdd(); - testAddDuplicate(); - testAddInvalidSignal(); - testRemove(); - testRemoveNotPresent(); - testClear(); - testClearEmpty(); - - // Async wait tests - testWaitWithSignal(); - testWaitWithDifferentSignal(); - - // Cancellation tests - testCancel(); - testCancelNoWaiters(); - testCancelMultipleTimes(); - - // Multiple signal set tests - testMultipleSignalSetsOnSameSignal(); - testSignalSetWithMultipleSignals(); - - // Queued signal tests - testQueuedSignal(); - - // Sequential wait tests - testSequentialWaits(); - - // io_result tests - testIoResultSuccess(); - testIoResultCanceled(); - testIoResultStructuredBinding(); - - // Signal flags tests (cross-platform) - testFlagsBitwiseOperations(); - testAddWithNoneFlags(); - testAddWithDontCareFlags(); - -#if !defined(_WIN32) - // Signal flags tests (POSIX only) - testAddWithFlags(); - testAddWithMultipleFlags(); - testAddSameSignalSameFlags(); - testAddSameSignalDifferentFlags(); - testAddSameSignalWithDontCare(); - testAddSameSignalDontCareFirst(); - testMultipleSetsCompatibleFlags(); - testMultipleSetsIncompatibleFlags(); - testMultipleSetsWithDontCare(); - testWaitWithFlagsWorks(); -#else - // Signal flags tests (Windows only) - testFlagsNotSupportedOnWindows(); -#endif - } -}; - -TEST_SUITE(signal_set_test, "boost.corosio.signal_set"); - -} // namespace corosio -} // namespace boost - +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace corosio { + +//------------------------------------------------ +// Signal set tests +// Focus: construction, add/remove, wait, and cancellation +//------------------------------------------------ + +struct signal_set_test +{ + //-------------------------------------------- + // Construction and move semantics + //-------------------------------------------- + + void + testConstruction() + { + io_context ioc; + signal_set s(ioc); + + BOOST_TEST_PASS(); + } + + void + testConstructWithOneSignal() + { + io_context ioc; + signal_set s(ioc, SIGINT); + + BOOST_TEST_PASS(); + } + + void + testConstructWithTwoSignals() + { + io_context ioc; + signal_set s(ioc, SIGINT, SIGTERM); + + BOOST_TEST_PASS(); + } + + void + testConstructWithThreeSignals() + { + io_context ioc; + signal_set s(ioc, SIGINT, SIGTERM, SIGABRT); + + BOOST_TEST_PASS(); + } + + void + testMoveConstruct() + { + io_context ioc; + signal_set s1(ioc, SIGINT); + + signal_set s2(std::move(s1)); + BOOST_TEST_PASS(); + } + + void + testMoveAssign() + { + io_context ioc; + signal_set s1(ioc, SIGINT); + signal_set s2(ioc); + + s2 = std::move(s1); + BOOST_TEST_PASS(); + } + + void + testMoveAssignCrossContextThrows() + { + io_context ioc1; + io_context ioc2; + signal_set s1(ioc1); + signal_set s2(ioc2); + + BOOST_TEST_THROWS(s2 = std::move(s1), std::logic_error); + } + + //-------------------------------------------- + // Add/remove/clear tests + //-------------------------------------------- + + void + testAdd() + { + io_context ioc; + signal_set s(ioc); + + auto result = s.add(SIGINT); + BOOST_TEST(result.has_value()); + } + + void + testAddDuplicate() + { + io_context ioc; + signal_set s(ioc); + + BOOST_TEST(s.add(SIGINT).has_value()); + auto result = s.add(SIGINT); // Should be no-op + BOOST_TEST(result.has_value()); + } + + void + testAddInvalidSignal() + { + io_context ioc; + signal_set s(ioc); + + auto result = s.add(-1); + BOOST_TEST(result.has_error()); + } + + void + testRemove() + { + io_context ioc; + signal_set s(ioc); + + BOOST_TEST(s.add(SIGINT).has_value()); + auto result = s.remove(SIGINT); + BOOST_TEST(result.has_value()); + } + + void + testRemoveNotPresent() + { + io_context ioc; + signal_set s(ioc); + + // Removing signal not in set should be a no-op + auto result = s.remove(SIGINT); + BOOST_TEST(result.has_value()); + } + + void + testClear() + { + io_context ioc; + signal_set s(ioc); + + BOOST_TEST(s.add(SIGINT).has_value()); + BOOST_TEST(s.add(SIGTERM).has_value()); + BOOST_TEST(s.clear().has_value()); + } + + void + testClearEmpty() + { + io_context ioc; + signal_set s(ioc); + + BOOST_TEST(s.clear().has_value()); // Should be no-op + } + + //-------------------------------------------- + // Async wait tests + //-------------------------------------------- + + void + testWaitWithSignal() + { + io_context ioc; + signal_set s(ioc, SIGINT); + timer t(ioc); + + bool completed = false; + int received_signal = 0; + system::error_code result_ec; + + auto wait_task = [](signal_set& s_ref, system::error_code& ec_out, int& sig_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + ec_out = ec; + sig_out = signum; + done_out = true; + }; + capy::run_async(ioc.get_executor())(wait_task(s, result_ec, received_signal, completed)); + + // Raise signal after a short delay + t.expires_after(std::chrono::milliseconds(10)); + auto raise_task = [](timer& t_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + std::raise(SIGINT); + }; + capy::run_async(ioc.get_executor())(raise_task(t)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(!result_ec); + BOOST_TEST_EQ(received_signal, SIGINT); + } + + void + testWaitWithDifferentSignal() + { + io_context ioc; + signal_set s(ioc, SIGTERM); + timer t(ioc); + + bool completed = false; + int received_signal = 0; + + auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + sig_out = signum; + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); + + t.expires_after(std::chrono::milliseconds(10)); + auto raise_task = [](timer& t_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + std::raise(SIGTERM); + }; + capy::run_async(ioc.get_executor())(raise_task(t)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST_EQ(received_signal, SIGTERM); + } + + //-------------------------------------------- + // Cancellation tests + //-------------------------------------------- + + void + testCancel() + { + io_context ioc; + signal_set s(ioc, SIGINT); + timer cancel_timer(ioc); + + bool completed = false; + system::error_code result_ec; + + auto wait_task = [](signal_set& s_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + ec_out = ec; + done_out = true; + (void)signum; + }; + capy::run_async(ioc.get_executor())(wait_task(s, result_ec, completed)); + + cancel_timer.expires_after(std::chrono::milliseconds(10)); + auto cancel_task = [](timer& t_ref, signal_set& s_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + s_ref.cancel(); + }; + capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, s)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + void + testCancelNoWaiters() + { + io_context ioc; + signal_set s(ioc, SIGINT); + + s.cancel(); // Should be no-op + BOOST_TEST_PASS(); + } + + void + testCancelMultipleTimes() + { + io_context ioc; + signal_set s(ioc, SIGINT); + + s.cancel(); + s.cancel(); + s.cancel(); + BOOST_TEST_PASS(); + } + + //-------------------------------------------- + // Multiple signal set tests + //-------------------------------------------- + + void + testMultipleSignalSetsOnSameSignal() + { + io_context ioc; + signal_set s1(ioc, SIGINT); + signal_set s2(ioc, SIGINT); + timer t(ioc); + + bool s1_completed = false; + bool s2_completed = false; + int s1_signal = 0; + int s2_signal = 0; + + auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + sig_out = signum; + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(wait_task(s1, s1_signal, s1_completed)); + capy::run_async(ioc.get_executor())(wait_task(s2, s2_signal, s2_completed)); + + t.expires_after(std::chrono::milliseconds(10)); + auto raise_task = [](timer& t_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + std::raise(SIGINT); + }; + capy::run_async(ioc.get_executor())(raise_task(t)); + + ioc.run(); + BOOST_TEST(s1_completed); + BOOST_TEST(s2_completed); + BOOST_TEST_EQ(s1_signal, SIGINT); + BOOST_TEST_EQ(s2_signal, SIGINT); + } + + void + testSignalSetWithMultipleSignals() + { + io_context ioc; + signal_set s(ioc, SIGINT, SIGTERM); + timer t(ioc); + + bool completed = false; + int received_signal = 0; + + auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + sig_out = signum; + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); + + // Raise SIGTERM (not SIGINT) + t.expires_after(std::chrono::milliseconds(10)); + auto raise_task = [](timer& t_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + std::raise(SIGTERM); + }; + capy::run_async(ioc.get_executor())(raise_task(t)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST_EQ(received_signal, SIGTERM); + } + + //-------------------------------------------- + // Queued signal tests + //-------------------------------------------- + + void + testQueuedSignal() + { + io_context ioc; + signal_set s(ioc, SIGINT); + + // Raise signal before starting wait + std::raise(SIGINT); + + bool completed = false; + int received_signal = 0; + + auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + sig_out = signum; + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST_EQ(received_signal, SIGINT); + } + + //-------------------------------------------- + // Sequential wait tests + //-------------------------------------------- + + void + testSequentialWaits() + { + io_context ioc; + signal_set s(ioc, SIGINT); + timer t(ioc); + + int wait_count = 0; + + auto task = [](signal_set& s_ref, timer& t_ref, int& count_out) -> capy::task<> + { + // First wait + t_ref.expires_after(std::chrono::milliseconds(5)); + (void)co_await t_ref.wait(); + std::raise(SIGINT); + + auto [ec1, sig1] = co_await s_ref.async_wait(); + BOOST_TEST(!ec1); + BOOST_TEST_EQ(sig1, SIGINT); + ++count_out; + + // Second wait + t_ref.expires_after(std::chrono::milliseconds(5)); + (void)co_await t_ref.wait(); + std::raise(SIGINT); + + auto [ec2, sig2] = co_await s_ref.async_wait(); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(sig2, SIGINT); + ++count_out; + }; + capy::run_async(ioc.get_executor())(task(s, t, wait_count)); + + ioc.run(); + BOOST_TEST_EQ(wait_count, 2); + } + + //-------------------------------------------- + // io_result tests + //-------------------------------------------- + + void + testIoResultSuccess() + { + io_context ioc; + signal_set s(ioc, SIGINT); + timer t(ioc); + + bool result_ok = false; + + auto task = [](signal_set& s_ref, timer& t_ref, bool& ok_out) -> capy::task<> + { + t_ref.expires_after(std::chrono::milliseconds(5)); + (void)co_await t_ref.wait(); + std::raise(SIGINT); + + auto result = co_await s_ref.async_wait(); + ok_out = !result.ec; + }; + capy::run_async(ioc.get_executor())(task(s, t, result_ok)); + + ioc.run(); + BOOST_TEST(result_ok); + } + + void + testIoResultCanceled() + { + io_context ioc; + signal_set s(ioc, SIGINT); + timer cancel_timer(ioc); + + bool result_ok = true; + system::error_code result_ec; + + auto wait_task = [](signal_set& s_ref, bool& ok_out, system::error_code& ec_out) -> capy::task<> + { + auto result = co_await s_ref.async_wait(); + ok_out = !result.ec; + ec_out = result.ec; + }; + capy::run_async(ioc.get_executor())(wait_task(s, result_ok, result_ec)); + + cancel_timer.expires_after(std::chrono::milliseconds(10)); + auto cancel_task = [](timer& t_ref, signal_set& s_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + s_ref.cancel(); + }; + capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, s)); + + ioc.run(); + BOOST_TEST(!result_ok); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + void + testIoResultStructuredBinding() + { + io_context ioc; + signal_set s(ioc, SIGINT); + timer t(ioc); + + system::error_code captured_ec; + int captured_signal = 0; + + auto task = [](signal_set& s_ref, timer& t_ref, system::error_code& ec_out, int& sig_out) -> capy::task<> + { + t_ref.expires_after(std::chrono::milliseconds(5)); + (void)co_await t_ref.wait(); + std::raise(SIGINT); + + auto [ec, signum] = co_await s_ref.async_wait(); + ec_out = ec; + sig_out = signum; + }; + capy::run_async(ioc.get_executor())(task(s, t, captured_ec, captured_signal)); + + ioc.run(); + BOOST_TEST(!captured_ec); + BOOST_TEST_EQ(captured_signal, SIGINT); + } + + //-------------------------------------------- + // Signal flags tests (cross-platform) + //-------------------------------------------- + + void + testFlagsBitwiseOperations() + { + // Test OR + auto combined = signal_set::restart | signal_set::no_defer; + BOOST_TEST((combined & signal_set::restart) != signal_set::none); + BOOST_TEST((combined & signal_set::no_defer) != signal_set::none); + BOOST_TEST((combined & signal_set::no_child_stop) == signal_set::none); + + // Test compound assignment + auto flags = signal_set::none; + flags |= signal_set::restart; + BOOST_TEST((flags & signal_set::restart) != signal_set::none); + + // Test NOT + auto all_but_restart = ~signal_set::restart; + BOOST_TEST((all_but_restart & signal_set::restart) == signal_set::none); + } + + void + testAddWithNoneFlags() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with none (default behavior) - works on all platforms + auto result = s.add(SIGINT, signal_set::none); + BOOST_TEST(result.has_value()); + } + + void + testAddWithDontCareFlags() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with dont_care - works on all platforms + auto result = s.add(SIGINT, signal_set::dont_care); + BOOST_TEST(result.has_value()); + } + +#if !defined(_WIN32) + //-------------------------------------------- + // Signal flags tests (POSIX only) + // Windows returns operation_not_supported for + // flags other than none/dont_care + //-------------------------------------------- + + void + testAddWithFlags() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with restart flag + auto result = s.add(SIGINT, signal_set::restart); + BOOST_TEST(result.has_value()); + } + + void + testAddWithMultipleFlags() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with combined flags + auto result = s.add(SIGINT, signal_set::restart | signal_set::no_defer); + BOOST_TEST(result.has_value()); + } + + void + testAddSameSignalSameFlags() + { + io_context ioc; + signal_set s(ioc); + + // Add signal twice with same flags (should be no-op) + BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); + BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); + } + + void + testAddSameSignalDifferentFlags() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with one flag, then try to add with different flag + BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); + auto result = s.add(SIGINT, signal_set::no_defer); + BOOST_TEST(result.has_error()); // Should fail due to flag mismatch + } + + void + testAddSameSignalWithDontCare() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with specific flags, then add with dont_care + BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); + auto result = s.add(SIGINT, signal_set::dont_care); + BOOST_TEST(result.has_value()); // Should succeed with dont_care + } + + void + testAddSameSignalDontCareFirst() + { + io_context ioc; + signal_set s(ioc); + + // Add signal with dont_care, then add with specific flags + BOOST_TEST(s.add(SIGINT, signal_set::dont_care).has_value()); + auto result = s.add(SIGINT, signal_set::restart); + BOOST_TEST(result.has_value()); // Should succeed + } + + void + testMultipleSetsCompatibleFlags() + { + io_context ioc; + signal_set s1(ioc); + signal_set s2(ioc); + + // Both sets add same signal with same flags + BOOST_TEST(s1.add(SIGINT, signal_set::restart).has_value()); + BOOST_TEST(s2.add(SIGINT, signal_set::restart).has_value()); + } + + void + testMultipleSetsIncompatibleFlags() + { + io_context ioc; + signal_set s1(ioc); + signal_set s2(ioc); + + // First set adds with one flag + BOOST_TEST(s1.add(SIGINT, signal_set::restart).has_value()); + // Second set tries to add with different flag + auto result = s2.add(SIGINT, signal_set::no_defer); + BOOST_TEST(result.has_error()); // Should fail + } + + void + testMultipleSetsWithDontCare() + { + io_context ioc; + signal_set s1(ioc); + signal_set s2(ioc); + + // First set adds with specific flags + BOOST_TEST(s1.add(SIGINT, signal_set::restart).has_value()); + // Second set adds with dont_care + BOOST_TEST(s2.add(SIGINT, signal_set::dont_care).has_value()); + } + + void + testWaitWithFlagsWorks() + { + io_context ioc; + signal_set s(ioc); + timer t(ioc); + + // Add signal with restart flag and verify wait still works + BOOST_TEST(s.add(SIGINT, signal_set::restart).has_value()); + + bool completed = false; + int received_signal = 0; + + auto wait_task = [](signal_set& s_ref, int& sig_out, bool& done_out) -> capy::task<> + { + auto [ec, signum] = co_await s_ref.async_wait(); + sig_out = signum; + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(wait_task(s, received_signal, completed)); + + t.expires_after(std::chrono::milliseconds(10)); + auto raise_task = [](timer& t_ref) -> capy::task<> + { + (void)co_await t_ref.wait(); + std::raise(SIGINT); + }; + capy::run_async(ioc.get_executor())(raise_task(t)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST_EQ(received_signal, SIGINT); + } + +#else // _WIN32 + //-------------------------------------------- + // Signal flags tests (Windows only) + //-------------------------------------------- + + void + testFlagsNotSupportedOnWindows() + { + io_context ioc; + signal_set s(ioc); + + // Windows returns operation_not_supported for actual flags + auto result = s.add(SIGINT, signal_set::restart); + BOOST_TEST(result.has_error()); + BOOST_TEST(result.error() == system::errc::operation_not_supported); + } + +#endif // _WIN32 + + void + run() + { + // Construction and move semantics + testConstruction(); + testConstructWithOneSignal(); + testConstructWithTwoSignals(); + testConstructWithThreeSignals(); + testMoveConstruct(); + testMoveAssign(); + testMoveAssignCrossContextThrows(); + + // Add/remove/clear tests + testAdd(); + testAddDuplicate(); + testAddInvalidSignal(); + testRemove(); + testRemoveNotPresent(); + testClear(); + testClearEmpty(); + + // Async wait tests + testWaitWithSignal(); + testWaitWithDifferentSignal(); + + // Cancellation tests + testCancel(); + testCancelNoWaiters(); + testCancelMultipleTimes(); + + // Multiple signal set tests + testMultipleSignalSetsOnSameSignal(); + testSignalSetWithMultipleSignals(); + + // Queued signal tests + testQueuedSignal(); + + // Sequential wait tests + testSequentialWaits(); + + // io_result tests + testIoResultSuccess(); + testIoResultCanceled(); + testIoResultStructuredBinding(); + + // Signal flags tests (cross-platform) + testFlagsBitwiseOperations(); + testAddWithNoneFlags(); + testAddWithDontCareFlags(); + +#if !defined(_WIN32) + // Signal flags tests (POSIX only) + testAddWithFlags(); + testAddWithMultipleFlags(); + testAddSameSignalSameFlags(); + testAddSameSignalDifferentFlags(); + testAddSameSignalWithDontCare(); + testAddSameSignalDontCareFirst(); + testMultipleSetsCompatibleFlags(); + testMultipleSetsIncompatibleFlags(); + testMultipleSetsWithDontCare(); + testWaitWithFlagsWorks(); +#else + // Signal flags tests (Windows only) + testFlagsNotSupportedOnWindows(); +#endif + } +}; + +TEST_SUITE(signal_set_test, "boost.corosio.signal_set"); + +} // namespace corosio +} // namespace boost + diff --git a/test/unit/socket.cpp b/test/unit/socket.cpp index 7acd6d88..2cfad85a 100644 --- a/test/unit/socket.cpp +++ b/test/unit/socket.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -24,6 +23,8 @@ #include #include +#include + #include "test_suite.hpp" namespace boost { @@ -114,7 +115,7 @@ struct socket_test char buf[32] = {}; auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec2); BOOST_TEST_EQ(n2, 5u); BOOST_TEST_EQ(std::string_view(buf, n2), "hello"); @@ -145,7 +146,7 @@ struct socket_test char buf[32] = {}; auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec2); BOOST_TEST_EQ(std::string_view(buf, n2), msg); } @@ -173,7 +174,7 @@ struct socket_test char buf[1024] = {}; auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec2); // read_some returns what's available, not buffer size BOOST_TEST_EQ(n2, 5u); @@ -197,23 +198,23 @@ struct socket_test char buf[32] = {}; // First exchange - (void) co_await a.write_some(capy::const_buffer("one", 3)); + (void)co_await a.write_some(capy::const_buffer("one", 3)); auto [ec1, n1] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec1); BOOST_TEST_EQ(std::string_view(buf, n1), "one"); // Second exchange - (void) co_await a.write_some(capy::const_buffer("two", 3)); + (void)co_await a.write_some(capy::const_buffer("two", 3)); auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec2); BOOST_TEST_EQ(std::string_view(buf, n2), "two"); // Third exchange - (void) co_await a.write_some(capy::const_buffer("three", 5)); + (void)co_await a.write_some(capy::const_buffer("three", 5)); auto [ec3, n3] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec3); BOOST_TEST_EQ(std::string_view(buf, n3), "three"); }; @@ -241,7 +242,7 @@ struct socket_test BOOST_TEST_EQ(n1, 6u); auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec2); BOOST_TEST_EQ(std::string_view(buf, n2), "from_a"); @@ -252,21 +253,21 @@ struct socket_test BOOST_TEST_EQ(n3, 6u); auto [ec4, n4] = co_await a.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec4); BOOST_TEST_EQ(std::string_view(buf, n4), "from_b"); // Interleaved: write a, write b, read b, read a - (void) co_await a.write_some(capy::const_buffer("msg_a", 5)); - (void) co_await b.write_some(capy::const_buffer("msg_b", 5)); + (void)co_await a.write_some(capy::const_buffer("msg_a", 5)); + (void)co_await b.write_some(capy::const_buffer("msg_b", 5)); auto [ec5, n5] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec5); BOOST_TEST_EQ(std::string_view(buf, n5), "msg_a"); auto [ec6, n6] = co_await a.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec6); BOOST_TEST_EQ(std::string_view(buf, n6), "msg_b"); }; @@ -297,7 +298,7 @@ struct socket_test BOOST_TEST_EQ(n1, 0u); // Send actual data so read can complete - (void) co_await a.write_some(capy::const_buffer("x", 1)); + (void)co_await a.write_some(capy::const_buffer("x", 1)); // Read with empty buffer should return 0 auto [ec2, n2] = co_await b.read_some( @@ -307,7 +308,7 @@ struct socket_test // Drain the actual data char buf[8]; - (void) co_await b.read_some(capy::make_buffer(buf)); + (void)co_await b.read_some(capy::mutable_buffer(buf, sizeof(buf))); }; capy::run_async(ioc.get_executor())(task(s1, s2)); @@ -409,19 +410,19 @@ struct socket_test auto task = [](socket& a, socket& b) -> capy::task<> { // Write data then close - (void) co_await a.write_some(capy::const_buffer("final", 5)); + (void)co_await a.write_some(capy::const_buffer("final", 5)); a.close(); // Read the data char buf[32] = {}; auto [ec1, n1] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec1); BOOST_TEST_EQ(std::string_view(buf, n1), "final"); // Next read should get EOF (0 bytes or error) auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); // EOF indicated by error or zero bytes BOOST_TEST(ec2 || n2 == 0); }; @@ -446,7 +447,7 @@ struct socket_test // Give OS time to process the close timer t(a.context()); t.expires_after(std::chrono::milliseconds(50)); - (void) co_await t.wait(); + (void)co_await t.wait(); // Writing to closed peer should eventually fail system::error_code last_ec; @@ -493,20 +494,20 @@ struct socket_test { char buf[32]; auto [ec, n] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); read_ec = ec; read_done = true; }; capy::run_async(ioc.get_executor())(nested_coro()); // Wait for timer then cancel - (void) co_await t.wait(); + (void)co_await t.wait(); b.cancel(); // Wait for read to complete timer t2(a.context()); t2.expires_after(std::chrono::milliseconds(50)); - (void) co_await t2.wait(); + (void)co_await t2.wait(); BOOST_TEST(read_done); BOOST_TEST(read_ec == capy::cond::canceled); @@ -539,19 +540,19 @@ struct socket_test { char buf[32]; auto [ec, n] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); read_ec = ec; read_done = true; }; capy::run_async(ioc.get_executor())(nested_coro()); // Wait then close the socket - (void) co_await t.wait(); + (void)co_await t.wait(); b.close(); timer t2(a.context()); t2.expires_after(std::chrono::milliseconds(50)); - (void) co_await t2.wait(); + (void)co_await t2.wait(); BOOST_TEST(read_done); // Close should cancel pending operations @@ -564,6 +565,80 @@ struct socket_test s2.close(); } + void + testStopTokenCancellation() + { + // Verifies that std::stop_token properly cancels pending I/O. + // On Linux/epoll, this requires the backend to actually unregister from + // epoll and post the operation to the scheduler, not just set a flag. + // Uses socket I/O for synchronization instead of timers. + io_context ioc; + auto [s1, s2] = test::make_socket_pair(ioc); + + std::stop_source stop_src; + bool read_done = false; + bool failsafe_hit = false; + system::error_code read_ec; + + // Reader task - signals ready then blocks waiting for data + auto reader_task = [&]() -> capy::task<> + { + // Signal we're about to start the blocking read + (void)co_await s2.write_some(capy::const_buffer("R", 1)); + + // Now block waiting for data that will never come + char buf[32]; + auto [ec, n] = co_await s2.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + read_ec = ec; + read_done = true; + }; + + // Canceller task - waits for reader to be ready, then requests stop + auto canceller_task = [&]() -> capy::task<> + { + // Wait for reader's "ready" signal + char buf[1]; + (void)co_await s1.read_some(capy::mutable_buffer(buf, 1)); + + // Reader is now blocked on read - request stop + stop_src.request_stop(); + }; + + // Failsafe task - detects if stop_token cancellation didn't work + auto failsafe_task = [&]() -> capy::task<> + { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(1000)); + auto [ec] = co_await t.wait(); + // Only trigger failsafe if reader hasn't completed yet. + // If read_done is true, stop_token cancellation worked. + if (!ec && !read_done) + { + // Failsafe triggered - stop_token cancellation didn't work! + failsafe_hit = true; + s2.cancel(); + } + }; + + // Launch all tasks + capy::run_async(ioc.get_executor(), stop_src.get_token())(reader_task()); + capy::run_async(ioc.get_executor())(canceller_task()); + capy::run_async(ioc.get_executor())(failsafe_task()); + + ioc.run(); + + BOOST_TEST(read_done); + BOOST_TEST(read_ec == capy::cond::canceled); + + // CRITICAL: The failsafe should NOT have been hit. + // If it was hit, it means stop_token didn't actually cancel the I/O. + BOOST_TEST(!failsafe_hit); + + s1.close(); + s2.close(); + } + // Composed Operations void @@ -576,7 +651,7 @@ struct socket_test { // Write exactly 100 bytes std::string send_data(100, 'X'); - (void) co_await write(a, capy::const_buffer( + (void)co_await write(a, capy::const_buffer( send_data.data(), send_data.size())); // Read exactly 100 bytes using corosio::read @@ -632,7 +707,7 @@ struct socket_test auto task = [](socket& a, socket& b) -> capy::task<> { std::string send_data = "Hello, this is a test message!"; - (void) co_await write(a, capy::const_buffer( + (void)co_await write(a, capy::const_buffer( send_data.data(), send_data.size())); a.close(); @@ -661,7 +736,7 @@ struct socket_test { // Send 50 bytes but try to read 100 std::string send_data(50, 'Z'); - (void) co_await write(a, capy::const_buffer( + (void)co_await write(a, capy::const_buffer( send_data.data(), send_data.size())); a.close(); @@ -691,19 +766,19 @@ struct socket_test auto task = [](socket& a, socket& b) -> capy::task<> { // Write data then shutdown send - (void) co_await a.write_some(capy::const_buffer("hello", 5)); + (void)co_await a.write_some(capy::const_buffer("hello", 5)); a.shutdown(socket::shutdown_send); // Read the data char buf[32] = {}; auto [ec1, n1] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec1); BOOST_TEST_EQ(std::string_view(buf, n1), "hello"); // Next read should get EOF auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(ec2 == capy::cond::eof); }; capy::run_async(ioc.get_executor())(task(s1, s2)); @@ -725,11 +800,11 @@ struct socket_test b.shutdown(socket::shutdown_receive); // b can still send - (void) co_await b.write_some(capy::const_buffer("from_b", 6)); + (void)co_await b.write_some(capy::const_buffer("from_b", 6)); char buf[32] = {}; auto [ec, n] = co_await a.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec); BOOST_TEST_EQ(std::string_view(buf, n), "from_b"); }; @@ -761,19 +836,19 @@ struct socket_test auto task = [](socket& a, socket& b) -> capy::task<> { // Write data then shutdown both - (void) co_await a.write_some(capy::const_buffer("goodbye", 7)); + (void)co_await a.write_some(capy::const_buffer("goodbye", 7)); a.shutdown(socket::shutdown_both); // Peer should receive the data char buf[32] = {}; auto [ec1, n1] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(!ec1); BOOST_TEST_EQ(std::string_view(buf, n1), "goodbye"); // Next read should get EOF auto [ec2, n2] = co_await b.read_some( - capy::make_buffer(buf)); + capy::mutable_buffer(buf, sizeof(buf))); BOOST_TEST(ec2 == capy::cond::eof); }; capy::run_async(ioc.get_executor())(task(s1, s2)); @@ -883,6 +958,7 @@ struct socket_test // Cancellation testCancelRead(); testCloseWhileReading(); + testStopTokenCancellation(); // Composed operations testReadFull(); diff --git a/test/unit/timer.cpp b/test/unit/timer.cpp index 831eac20..4489538e 100644 --- a/test/unit/timer.cpp +++ b/test/unit/timer.cpp @@ -1,663 +1,663 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include - -#include - -#include "test_suite.hpp" - -namespace boost { -namespace corosio { - -//------------------------------------------------ -// Timer-specific tests -// Focus: timer construction, expiry, wait, and cancellation -//------------------------------------------------ - -struct timer_test -{ - //-------------------------------------------- - // Construction and move semantics - //-------------------------------------------- - - void - testConstruction() - { - io_context ioc; - timer t(ioc); - - BOOST_TEST_PASS(); - } - - void - testMoveConstruct() - { - io_context ioc; - timer t1(ioc); - t1.expires_after(std::chrono::milliseconds(100)); - auto expiry = t1.expiry(); - - timer t2(std::move(t1)); - BOOST_TEST(t2.expiry() == expiry); - } - - void - testMoveAssign() - { - io_context ioc; - timer t1(ioc); - timer t2(ioc); - - t1.expires_after(std::chrono::milliseconds(100)); - auto expiry = t1.expiry(); - - t2 = std::move(t1); - BOOST_TEST(t2.expiry() == expiry); - } - - void - testMoveAssignCrossContextThrows() - { - io_context ioc1; - io_context ioc2; - timer t1(ioc1); - timer t2(ioc2); - - BOOST_TEST_THROWS(t2 = std::move(t1), std::logic_error); - } - - //-------------------------------------------- - // Expiry setting and retrieval - //-------------------------------------------- - - void - testDefaultExpiry() - { - io_context ioc; - timer t(ioc); - - auto expiry = t.expiry(); - BOOST_TEST(expiry == timer::time_point{}); - } - - void - testExpiresAfter() - { - io_context ioc; - timer t(ioc); - - auto before = timer::clock_type::now(); - t.expires_after(std::chrono::milliseconds(100)); - auto after = timer::clock_type::now(); - - auto expiry = t.expiry(); - BOOST_TEST(expiry >= before + std::chrono::milliseconds(100)); - BOOST_TEST(expiry <= after + std::chrono::milliseconds(100)); - } - - void - testExpiresAfterDifferentDurations() - { - io_context ioc; - timer t(ioc); - - auto before = timer::clock_type::now(); - t.expires_after(std::chrono::seconds(1)); - auto expiry = t.expiry(); - BOOST_TEST(expiry >= before + std::chrono::seconds(1)); - - before = timer::clock_type::now(); - t.expires_after(std::chrono::microseconds(500000)); - expiry = t.expiry(); - BOOST_TEST(expiry >= before + std::chrono::microseconds(500000)); - - before = timer::clock_type::now(); - t.expires_after(std::chrono::hours(0)); - expiry = t.expiry(); - BOOST_TEST(expiry <= before + std::chrono::milliseconds(10)); - } - - void - testExpiresAt() - { - io_context ioc; - timer t(ioc); - - auto target = timer::clock_type::now() + std::chrono::milliseconds(200); - t.expires_at(target); - - BOOST_TEST(t.expiry() == target); - } - - void - testExpiresAtPast() - { - io_context ioc; - timer t(ioc); - - auto target = timer::clock_type::now() - std::chrono::seconds(1); - t.expires_at(target); - - BOOST_TEST(t.expiry() == target); - } - - void - testExpiresAtReplace() - { - io_context ioc; - timer t(ioc); - - auto first = timer::clock_type::now() + std::chrono::seconds(10); - t.expires_at(first); - BOOST_TEST(t.expiry() == first); - - auto second = timer::clock_type::now() + std::chrono::seconds(5); - t.expires_at(second); - BOOST_TEST(t.expiry() == second); - } - - //-------------------------------------------- - // Async wait tests - //-------------------------------------------- - - void - testWaitBasic() - { - io_context ioc; - timer t(ioc); - - bool completed = false; - system::error_code result_ec; - - t.expires_after(std::chrono::milliseconds(10)); - - auto task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void - testWaitTimingAccuracy() - { - io_context ioc; - timer t(ioc); - - auto start = timer::clock_type::now(); - timer::duration elapsed; - - t.expires_after(std::chrono::milliseconds(50)); - - auto task = [](timer& t_ref, timer::time_point start_val, timer::duration& elapsed_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - elapsed_out = timer::clock_type::now() - start_val; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t, start, elapsed)); - - ioc.run(); - - BOOST_TEST(elapsed >= std::chrono::milliseconds(50)); - BOOST_TEST(elapsed < std::chrono::milliseconds(200)); - } - - void - testWaitExpiredTimer() - { - io_context ioc; - timer t(ioc); - - bool completed = false; - system::error_code result_ec; - - t.expires_at(timer::clock_type::now() - std::chrono::seconds(1)); - - auto task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - void - testWaitZeroDuration() - { - io_context ioc; - timer t(ioc); - - bool completed = false; - system::error_code result_ec; - - t.expires_after(std::chrono::milliseconds(0)); - - auto task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(!result_ec); - } - - //-------------------------------------------- - // Cancellation tests - //-------------------------------------------- - - void - testCancel() - { - io_context ioc; - timer t(ioc); - timer cancel_timer(ioc); - - bool completed = false; - system::error_code result_ec; - - t.expires_after(std::chrono::seconds(60)); - cancel_timer.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(wait_task(t, result_ec, completed)); - - auto cancel_task = [](timer& cancel_t_ref, timer& t_ref) -> capy::task<> - { - (void) co_await cancel_t_ref.wait(); - t_ref.cancel(); - }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, t)); - - ioc.run(); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void - testCancelNoWaiters() - { - io_context ioc; - timer t(ioc); - - t.expires_after(std::chrono::seconds(60)); - - t.cancel(); - BOOST_TEST_PASS(); - } - - void - testCancelMultipleTimes() - { - io_context ioc; - timer t(ioc); - - t.expires_after(std::chrono::seconds(60)); - - t.cancel(); - t.cancel(); - t.cancel(); - BOOST_TEST_PASS(); - } - - void - testExpiresAtCancelsWaiter() - { - io_context ioc; - timer t(ioc); - timer delay_timer(ioc); - - bool completed = false; - system::error_code result_ec; - - t.expires_after(std::chrono::seconds(60)); - delay_timer.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - done_out = true; - }; - capy::run_async(ioc.get_executor())(wait_task(t, result_ec, completed)); - - auto delay_task = [](timer& delay_ref, timer& t_ref) -> capy::task<> - { - (void) co_await delay_ref.wait(); - t_ref.expires_after(std::chrono::seconds(30)); - }; - capy::run_async(ioc.get_executor())(delay_task(delay_timer, t)); - - ioc.run_for(std::chrono::milliseconds(100)); - BOOST_TEST(completed); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - //-------------------------------------------- - // Multiple timer tests - //-------------------------------------------- - - void - testMultipleTimersDifferentExpiry() - { - io_context ioc; - timer t1(ioc); - timer t2(ioc); - timer t3(ioc); - - int order = 0; - int t1_order = 0, t2_order = 0, t3_order = 0; - - t1.expires_after(std::chrono::milliseconds(30)); - t2.expires_after(std::chrono::milliseconds(10)); - t3.expires_after(std::chrono::milliseconds(20)); - - auto task = [](timer& t_ref, int& order_ref, int& t_order_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - t_order_out = ++order_ref; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t1, order, t1_order)); - capy::run_async(ioc.get_executor())(task(t2, order, t2_order)); - capy::run_async(ioc.get_executor())(task(t3, order, t3_order)); - - ioc.run(); - - BOOST_TEST_EQ(t2_order, 1); - BOOST_TEST_EQ(t3_order, 2); - BOOST_TEST_EQ(t1_order, 3); - } - - void - testMultipleTimersSameExpiry() - { - io_context ioc; - timer t1(ioc); - timer t2(ioc); - - bool t1_done = false, t2_done = false; - - auto expiry = timer::clock_type::now() + std::chrono::milliseconds(20); - t1.expires_at(expiry); - t2.expires_at(expiry); - - auto task = [](timer& t_ref, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t1, t1_done)); - capy::run_async(ioc.get_executor())(task(t2, t2_done)); - - ioc.run(); - - BOOST_TEST(t1_done); - BOOST_TEST(t2_done); - } - - //-------------------------------------------- - // Sequential wait tests - //-------------------------------------------- - - void - testSequentialWaits() - { - io_context ioc; - timer t(ioc); - - int wait_count = 0; - - auto task = [](timer& t_ref, int& count_out) -> capy::task<> - { - t_ref.expires_after(std::chrono::milliseconds(5)); - auto [ec1] = co_await t_ref.wait(); - BOOST_TEST(!ec1); - ++count_out; - - t_ref.expires_after(std::chrono::milliseconds(5)); - auto [ec2] = co_await t_ref.wait(); - BOOST_TEST(!ec2); - ++count_out; - - t_ref.expires_after(std::chrono::milliseconds(5)); - auto [ec3] = co_await t_ref.wait(); - BOOST_TEST(!ec3); - ++count_out; - }; - capy::run_async(ioc.get_executor())(task(t, wait_count)); - - ioc.run(); - BOOST_TEST_EQ(wait_count, 3); - } - - //-------------------------------------------- - // io_result tests - //-------------------------------------------- - - void - testIoResultSuccess() - { - io_context ioc; - timer t(ioc); - - bool result_ok = false; - - t.expires_after(std::chrono::milliseconds(5)); - - auto task = [](timer& t_ref, bool& ok_out) -> capy::task<> - { - auto result = co_await t_ref.wait(); - ok_out = !result.ec; - }; - capy::run_async(ioc.get_executor())(task(t, result_ok)); - - ioc.run(); - BOOST_TEST(result_ok); - } - - void - testIoResultCanceled() - { - io_context ioc; - timer t(ioc); - timer cancel_timer(ioc); - - bool result_ok = true; - system::error_code result_ec; - - t.expires_after(std::chrono::seconds(60)); - cancel_timer.expires_after(std::chrono::milliseconds(10)); - - auto wait_task = [](timer& t_ref, bool& ok_out, system::error_code& ec_out) -> capy::task<> - { - auto result = co_await t_ref.wait(); - ok_out = !result.ec; - ec_out = result.ec; - }; - capy::run_async(ioc.get_executor())(wait_task(t, result_ok, result_ec)); - - auto cancel_task = [](timer& cancel_t_ref, timer& t_ref) -> capy::task<> - { - (void) co_await cancel_t_ref.wait(); - t_ref.cancel(); - }; - capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, t)); - - ioc.run(); - BOOST_TEST(!result_ok); - BOOST_TEST(result_ec == capy::cond::canceled); - } - - void - testIoResultStructuredBinding() - { - io_context ioc; - timer t(ioc); - - system::error_code captured_ec; - - t.expires_after(std::chrono::milliseconds(5)); - - auto task = [](timer& t_ref, system::error_code& ec_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - ec_out = ec; - }; - capy::run_async(ioc.get_executor())(task(t, captured_ec)); - - ioc.run(); - BOOST_TEST(!captured_ec); - } - - //-------------------------------------------- - // Edge cases - //-------------------------------------------- - - void - testLongDuration() - { - io_context ioc; - timer t(ioc); - - t.expires_after(std::chrono::hours(24 * 365)); - - auto expiry = t.expiry(); - BOOST_TEST(expiry > timer::clock_type::now()); - - t.cancel(); - BOOST_TEST_PASS(); - } - - void - testNegativeDuration() - { - io_context ioc; - timer t(ioc); - - bool completed = false; - - t.expires_after(std::chrono::milliseconds(-100)); - - auto task = [](timer& t_ref, bool& done_out) -> capy::task<> - { - auto [ec] = co_await t_ref.wait(); - done_out = true; - (void)ec; - }; - capy::run_async(ioc.get_executor())(task(t, completed)); - - ioc.run(); - BOOST_TEST(completed); - } - - //-------------------------------------------- - // Type trait tests - //-------------------------------------------- - - void - testTypeAliases() - { - static_assert(std::is_same_v< - timer::clock_type, - std::chrono::steady_clock>); - - static_assert(std::is_same_v< - timer::time_point, - std::chrono::steady_clock::time_point>); - - static_assert(std::is_same_v< - timer::duration, - std::chrono::steady_clock::duration>); - - BOOST_TEST_PASS(); - } - - void - run() - { - // Construction and move semantics - testConstruction(); - testMoveConstruct(); - testMoveAssign(); - testMoveAssignCrossContextThrows(); - - // Expiry setting and retrieval - testDefaultExpiry(); - testExpiresAfter(); - testExpiresAfterDifferentDurations(); - testExpiresAt(); - testExpiresAtPast(); - testExpiresAtReplace(); - - // Async wait tests - testWaitBasic(); - testWaitTimingAccuracy(); - testWaitExpiredTimer(); - testWaitZeroDuration(); - - // Cancellation tests - testCancel(); - testCancelNoWaiters(); - testCancelMultipleTimes(); - testExpiresAtCancelsWaiter(); - - // Multiple timer tests - testMultipleTimersDifferentExpiry(); - testMultipleTimersSameExpiry(); - - // Sequential wait tests - testSequentialWaits(); - - // io_result tests - testIoResultSuccess(); - testIoResultCanceled(); - testIoResultStructuredBinding(); - - // Edge cases - testLongDuration(); - testNegativeDuration(); - - // Type trait tests - testTypeAliases(); - } -}; - -TEST_SUITE(timer_test, "boost.corosio.timer"); - -} // namespace corosio -} // namespace boost +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include + +#include + +#include "test_suite.hpp" + +namespace boost { +namespace corosio { + +//------------------------------------------------ +// Timer-specific tests +// Focus: timer construction, expiry, wait, and cancellation +//------------------------------------------------ + +struct timer_test +{ + //-------------------------------------------- + // Construction and move semantics + //-------------------------------------------- + + void + testConstruction() + { + io_context ioc; + timer t(ioc); + + BOOST_TEST_PASS(); + } + + void + testMoveConstruct() + { + io_context ioc; + timer t1(ioc); + t1.expires_after(std::chrono::milliseconds(100)); + auto expiry = t1.expiry(); + + timer t2(std::move(t1)); + BOOST_TEST(t2.expiry() == expiry); + } + + void + testMoveAssign() + { + io_context ioc; + timer t1(ioc); + timer t2(ioc); + + t1.expires_after(std::chrono::milliseconds(100)); + auto expiry = t1.expiry(); + + t2 = std::move(t1); + BOOST_TEST(t2.expiry() == expiry); + } + + void + testMoveAssignCrossContextThrows() + { + io_context ioc1; + io_context ioc2; + timer t1(ioc1); + timer t2(ioc2); + + BOOST_TEST_THROWS(t2 = std::move(t1), std::logic_error); + } + + //-------------------------------------------- + // Expiry setting and retrieval + //-------------------------------------------- + + void + testDefaultExpiry() + { + io_context ioc; + timer t(ioc); + + auto expiry = t.expiry(); + BOOST_TEST(expiry == timer::time_point{}); + } + + void + testExpiresAfter() + { + io_context ioc; + timer t(ioc); + + auto before = timer::clock_type::now(); + t.expires_after(std::chrono::milliseconds(100)); + auto after = timer::clock_type::now(); + + auto expiry = t.expiry(); + BOOST_TEST(expiry >= before + std::chrono::milliseconds(100)); + BOOST_TEST(expiry <= after + std::chrono::milliseconds(100)); + } + + void + testExpiresAfterDifferentDurations() + { + io_context ioc; + timer t(ioc); + + auto before = timer::clock_type::now(); + t.expires_after(std::chrono::seconds(1)); + auto expiry = t.expiry(); + BOOST_TEST(expiry >= before + std::chrono::seconds(1)); + + before = timer::clock_type::now(); + t.expires_after(std::chrono::microseconds(500000)); + expiry = t.expiry(); + BOOST_TEST(expiry >= before + std::chrono::microseconds(500000)); + + before = timer::clock_type::now(); + t.expires_after(std::chrono::hours(0)); + expiry = t.expiry(); + BOOST_TEST(expiry <= before + std::chrono::milliseconds(10)); + } + + void + testExpiresAt() + { + io_context ioc; + timer t(ioc); + + auto target = timer::clock_type::now() + std::chrono::milliseconds(200); + t.expires_at(target); + + BOOST_TEST(t.expiry() == target); + } + + void + testExpiresAtPast() + { + io_context ioc; + timer t(ioc); + + auto target = timer::clock_type::now() - std::chrono::seconds(1); + t.expires_at(target); + + BOOST_TEST(t.expiry() == target); + } + + void + testExpiresAtReplace() + { + io_context ioc; + timer t(ioc); + + auto first = timer::clock_type::now() + std::chrono::seconds(10); + t.expires_at(first); + BOOST_TEST(t.expiry() == first); + + auto second = timer::clock_type::now() + std::chrono::seconds(5); + t.expires_at(second); + BOOST_TEST(t.expiry() == second); + } + + //-------------------------------------------- + // Async wait tests + //-------------------------------------------- + + void + testWaitBasic() + { + io_context ioc; + timer t(ioc); + + bool completed = false; + system::error_code result_ec; + + t.expires_after(std::chrono::milliseconds(10)); + + auto task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + ec_out = ec; + done_out = true; + }; + capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(!result_ec); + } + + void + testWaitTimingAccuracy() + { + io_context ioc; + timer t(ioc); + + auto start = timer::clock_type::now(); + timer::duration elapsed; + + t.expires_after(std::chrono::milliseconds(50)); + + auto task = [](timer& t_ref, timer::time_point start_val, timer::duration& elapsed_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + elapsed_out = timer::clock_type::now() - start_val; + (void)ec; + }; + capy::run_async(ioc.get_executor())(task(t, start, elapsed)); + + ioc.run(); + + BOOST_TEST(elapsed >= std::chrono::milliseconds(50)); + BOOST_TEST(elapsed < std::chrono::milliseconds(200)); + } + + void + testWaitExpiredTimer() + { + io_context ioc; + timer t(ioc); + + bool completed = false; + system::error_code result_ec; + + t.expires_at(timer::clock_type::now() - std::chrono::seconds(1)); + + auto task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + ec_out = ec; + done_out = true; + }; + capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(!result_ec); + } + + void + testWaitZeroDuration() + { + io_context ioc; + timer t(ioc); + + bool completed = false; + system::error_code result_ec; + + t.expires_after(std::chrono::milliseconds(0)); + + auto task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + ec_out = ec; + done_out = true; + }; + capy::run_async(ioc.get_executor())(task(t, result_ec, completed)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(!result_ec); + } + + //-------------------------------------------- + // Cancellation tests + //-------------------------------------------- + + void + testCancel() + { + io_context ioc; + timer t(ioc); + timer cancel_timer(ioc); + + bool completed = false; + system::error_code result_ec; + + t.expires_after(std::chrono::seconds(60)); + cancel_timer.expires_after(std::chrono::milliseconds(10)); + + auto wait_task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + ec_out = ec; + done_out = true; + }; + capy::run_async(ioc.get_executor())(wait_task(t, result_ec, completed)); + + auto cancel_task = [](timer& cancel_t_ref, timer& t_ref) -> capy::task<> + { + (void)co_await cancel_t_ref.wait(); + t_ref.cancel(); + }; + capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, t)); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + void + testCancelNoWaiters() + { + io_context ioc; + timer t(ioc); + + t.expires_after(std::chrono::seconds(60)); + + t.cancel(); + BOOST_TEST_PASS(); + } + + void + testCancelMultipleTimes() + { + io_context ioc; + timer t(ioc); + + t.expires_after(std::chrono::seconds(60)); + + t.cancel(); + t.cancel(); + t.cancel(); + BOOST_TEST_PASS(); + } + + void + testExpiresAtCancelsWaiter() + { + io_context ioc; + timer t(ioc); + timer delay_timer(ioc); + + bool completed = false; + system::error_code result_ec; + + t.expires_after(std::chrono::seconds(60)); + delay_timer.expires_after(std::chrono::milliseconds(10)); + + auto wait_task = [](timer& t_ref, system::error_code& ec_out, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + ec_out = ec; + done_out = true; + }; + capy::run_async(ioc.get_executor())(wait_task(t, result_ec, completed)); + + auto delay_task = [](timer& delay_ref, timer& t_ref) -> capy::task<> + { + (void)co_await delay_ref.wait(); + t_ref.expires_after(std::chrono::seconds(30)); + }; + capy::run_async(ioc.get_executor())(delay_task(delay_timer, t)); + + ioc.run_for(std::chrono::milliseconds(100)); + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + //-------------------------------------------- + // Multiple timer tests + //-------------------------------------------- + + void + testMultipleTimersDifferentExpiry() + { + io_context ioc; + timer t1(ioc); + timer t2(ioc); + timer t3(ioc); + + int order = 0; + int t1_order = 0, t2_order = 0, t3_order = 0; + + t1.expires_after(std::chrono::milliseconds(30)); + t2.expires_after(std::chrono::milliseconds(10)); + t3.expires_after(std::chrono::milliseconds(20)); + + auto task = [](timer& t_ref, int& order_ref, int& t_order_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + t_order_out = ++order_ref; + (void)ec; + }; + capy::run_async(ioc.get_executor())(task(t1, order, t1_order)); + capy::run_async(ioc.get_executor())(task(t2, order, t2_order)); + capy::run_async(ioc.get_executor())(task(t3, order, t3_order)); + + ioc.run(); + + BOOST_TEST_EQ(t2_order, 1); + BOOST_TEST_EQ(t3_order, 2); + BOOST_TEST_EQ(t1_order, 3); + } + + void + testMultipleTimersSameExpiry() + { + io_context ioc; + timer t1(ioc); + timer t2(ioc); + + bool t1_done = false, t2_done = false; + + auto expiry = timer::clock_type::now() + std::chrono::milliseconds(20); + t1.expires_at(expiry); + t2.expires_at(expiry); + + auto task = [](timer& t_ref, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(task(t1, t1_done)); + capy::run_async(ioc.get_executor())(task(t2, t2_done)); + + ioc.run(); + + BOOST_TEST(t1_done); + BOOST_TEST(t2_done); + } + + //-------------------------------------------- + // Sequential wait tests + //-------------------------------------------- + + void + testSequentialWaits() + { + io_context ioc; + timer t(ioc); + + int wait_count = 0; + + auto task = [](timer& t_ref, int& count_out) -> capy::task<> + { + t_ref.expires_after(std::chrono::milliseconds(5)); + auto [ec1] = co_await t_ref.wait(); + BOOST_TEST(!ec1); + ++count_out; + + t_ref.expires_after(std::chrono::milliseconds(5)); + auto [ec2] = co_await t_ref.wait(); + BOOST_TEST(!ec2); + ++count_out; + + t_ref.expires_after(std::chrono::milliseconds(5)); + auto [ec3] = co_await t_ref.wait(); + BOOST_TEST(!ec3); + ++count_out; + }; + capy::run_async(ioc.get_executor())(task(t, wait_count)); + + ioc.run(); + BOOST_TEST_EQ(wait_count, 3); + } + + //-------------------------------------------- + // io_result tests + //-------------------------------------------- + + void + testIoResultSuccess() + { + io_context ioc; + timer t(ioc); + + bool result_ok = false; + + t.expires_after(std::chrono::milliseconds(5)); + + auto task = [](timer& t_ref, bool& ok_out) -> capy::task<> + { + auto result = co_await t_ref.wait(); + ok_out = !result.ec; + }; + capy::run_async(ioc.get_executor())(task(t, result_ok)); + + ioc.run(); + BOOST_TEST(result_ok); + } + + void + testIoResultCanceled() + { + io_context ioc; + timer t(ioc); + timer cancel_timer(ioc); + + bool result_ok = true; + system::error_code result_ec; + + t.expires_after(std::chrono::seconds(60)); + cancel_timer.expires_after(std::chrono::milliseconds(10)); + + auto wait_task = [](timer& t_ref, bool& ok_out, system::error_code& ec_out) -> capy::task<> + { + auto result = co_await t_ref.wait(); + ok_out = !result.ec; + ec_out = result.ec; + }; + capy::run_async(ioc.get_executor())(wait_task(t, result_ok, result_ec)); + + auto cancel_task = [](timer& cancel_t_ref, timer& t_ref) -> capy::task<> + { + (void)co_await cancel_t_ref.wait(); + t_ref.cancel(); + }; + capy::run_async(ioc.get_executor())(cancel_task(cancel_timer, t)); + + ioc.run(); + BOOST_TEST(!result_ok); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + void + testIoResultStructuredBinding() + { + io_context ioc; + timer t(ioc); + + system::error_code captured_ec; + + t.expires_after(std::chrono::milliseconds(5)); + + auto task = [](timer& t_ref, system::error_code& ec_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + ec_out = ec; + }; + capy::run_async(ioc.get_executor())(task(t, captured_ec)); + + ioc.run(); + BOOST_TEST(!captured_ec); + } + + //-------------------------------------------- + // Edge cases + //-------------------------------------------- + + void + testLongDuration() + { + io_context ioc; + timer t(ioc); + + t.expires_after(std::chrono::hours(24 * 365)); + + auto expiry = t.expiry(); + BOOST_TEST(expiry > timer::clock_type::now()); + + t.cancel(); + BOOST_TEST_PASS(); + } + + void + testNegativeDuration() + { + io_context ioc; + timer t(ioc); + + bool completed = false; + + t.expires_after(std::chrono::milliseconds(-100)); + + auto task = [](timer& t_ref, bool& done_out) -> capy::task<> + { + auto [ec] = co_await t_ref.wait(); + done_out = true; + (void)ec; + }; + capy::run_async(ioc.get_executor())(task(t, completed)); + + ioc.run(); + BOOST_TEST(completed); + } + + //-------------------------------------------- + // Type trait tests + //-------------------------------------------- + + void + testTypeAliases() + { + static_assert(std::is_same_v< + timer::clock_type, + std::chrono::steady_clock>); + + static_assert(std::is_same_v< + timer::time_point, + std::chrono::steady_clock::time_point>); + + static_assert(std::is_same_v< + timer::duration, + std::chrono::steady_clock::duration>); + + BOOST_TEST_PASS(); + } + + void + run() + { + // Construction and move semantics + testConstruction(); + testMoveConstruct(); + testMoveAssign(); + testMoveAssignCrossContextThrows(); + + // Expiry setting and retrieval + testDefaultExpiry(); + testExpiresAfter(); + testExpiresAfterDifferentDurations(); + testExpiresAt(); + testExpiresAtPast(); + testExpiresAtReplace(); + + // Async wait tests + testWaitBasic(); + testWaitTimingAccuracy(); + testWaitExpiredTimer(); + testWaitZeroDuration(); + + // Cancellation tests + testCancel(); + testCancelNoWaiters(); + testCancelMultipleTimes(); + testExpiresAtCancelsWaiter(); + + // Multiple timer tests + testMultipleTimersDifferentExpiry(); + testMultipleTimersSameExpiry(); + + // Sequential wait tests + testSequentialWaits(); + + // io_result tests + testIoResultSuccess(); + testIoResultCanceled(); + testIoResultStructuredBinding(); + + // Edge cases + testLongDuration(); + testNegativeDuration(); + + // Type trait tests + testTypeAliases(); + } +}; + +TEST_SUITE(timer_test, "boost.corosio.timer"); + +} // namespace corosio +} // namespace boost From a2856310b0eb8bae83cdd592b1f0bcc862b913bb Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 23 Jan 2026 16:45:39 +0000 Subject: [PATCH 4/9] Fix CI issues --- .github/workflows/ci.yml | 161 ++++++++++++++++++++++++-- CMakeLists.txt | 16 ++- cmake/FindWolfSSL.cmake | 12 +- include/boost/corosio/tls/context.hpp | 7 ++ src/openssl/src/openssl_stream.cpp | 54 --------- 5 files changed, 181 insertions(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f21ba94c..70f5b541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,6 @@ env: UBSAN_OPTIONS: "print_stacktrace=1" DEBIAN_FRONTEND: "noninteractive" TZ: "Europe/London" - # Enable verbose TLS test logging for debugging CI timeout issues - COROSIO_TLS_TEST_VERBOSE: "1" - COROSIO_TLS_DEBUG: "1" jobs: # Self-hosted runner selection is disabled to allow re-running individual @@ -105,6 +102,7 @@ jobs: shared: false build-type: "Release" build-cmake: true + vcpkg-triplet: "x64-mingw-static" # macOS (2 configurations) # TODO: Re-enable when BSD/kqueue support is implemented @@ -304,6 +302,22 @@ jobs: ref: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} path: capy-root + # Test dependency of corosio + - name: Clone Asio + uses: actions/checkout@v4 + with: + repository: boostorg/asio + ref: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} + path: asio-root + + # Test dependency of corosio + - name: Clone Filesystem + uses: actions/checkout@v4 + with: + repository: boostorg/filesystem + ref: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} + path: filesystem-root + - name: Clone Boost uses: alandefreitas/cpp-actions/boost-clone@v1.9.0 id: boost-clone @@ -382,6 +396,12 @@ jobs: } EOF + - name: Set vcpkg triplet + if: matrix.vcpkg-triplet + shell: bash + run: | + echo "VCPKG_DEFAULT_TRIPLET=${{ matrix.vcpkg-triplet }}" >> $GITHUB_ENV + - name: Setup vcpkg uses: lukka/run-vcpkg@v11 with: @@ -392,18 +412,53 @@ jobs: - name: Set vcpkg paths (Windows) if: runner.os == 'Windows' + id: vcpkg-paths-windows shell: bash run: | - vcpkg_installed="${{ github.workspace }}/vcpkg/vcpkg_installed/x64-windows" + # Determine triplet (mingw uses x64-mingw-static, msvc uses x64-windows) + triplet="${{ matrix.vcpkg-triplet || 'x64-windows' }}" + echo "Using triplet: ${triplet}" + + # lukka/run-vcpkg sets VCPKG_INSTALLED_DIR with a UUID-based path + # Use that directly instead of trying to find it + echo "Debug: VCPKG_INSTALLED_DIR=${VCPKG_INSTALLED_DIR:-not set}" + + if [ -n "${VCPKG_INSTALLED_DIR}" ] && [ -d "${VCPKG_INSTALLED_DIR}/${triplet}" ]; then + vcpkg_installed="${VCPKG_INSTALLED_DIR}/${triplet}" + else + # Fallback: try common locations + vcpkg_installed="${{ github.workspace }}/corosio-root/vcpkg_installed/${triplet}" + if [ ! -d "${vcpkg_installed}" ]; then + vcpkg_installed=$(find "${{ github.workspace }}" -type d -path "*/vcpkg_installed/${triplet}" 2>/dev/null | head -1) + fi + fi + + if [ -z "${vcpkg_installed}" ] || [ ! -d "${vcpkg_installed}" ]; then + echo "ERROR: Could not find vcpkg installed directory!" + echo "VCPKG_INSTALLED_DIR=${VCPKG_INSTALLED_DIR:-not set}" + echo "triplet=${triplet}" + echo "GITHUB_WORKSPACE=${{ github.workspace }}" + find "${{ github.workspace }}" -type d -name "vcpkg_installed" 2>/dev/null || true + exit 1 + fi + + # Convert backslashes to forward slashes to avoid escape issues in YAML + vcpkg_installed=$(echo "${vcpkg_installed}" | sed 's|\\|/|g') - # Debug: show what was installed - echo "Checking vcpkg installed packages:" + echo "Using vcpkg_installed: ${vcpkg_installed}" ls -la "${vcpkg_installed}/" || true ls -la "${vcpkg_installed}/include/" || true ls -la "${vcpkg_installed}/lib/" || true + ls -la "${vcpkg_installed}/bin/" || true - # For CMake (vcpkg toolchain file handles finding packages) + # Add vcpkg bin directory to PATH for DLLs (needed for test discovery POST_BUILD) + echo "${vcpkg_installed}/bin" >> $GITHUB_PATH + + # For CMake - tell vcpkg toolchain where packages are installed echo "CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" >> $GITHUB_ENV + # Get the parent of vcpkg_installed/ (i.e., vcpkg_installed/) + vcpkg_installed_dir=$(dirname "${vcpkg_installed}") + echo "VCPKG_INSTALLED_DIR=${vcpkg_installed_dir}" >> $GITHUB_ENV # For B2 (uses explicit paths) echo "WOLFSSL_INCLUDE=${vcpkg_installed}/include" >> $GITHUB_ENV @@ -411,6 +466,18 @@ jobs: echo "OPENSSL_INCLUDE=${vcpkg_installed}/include" >> $GITHUB_ENV echo "OPENSSL_LIBRARY_PATH=${vcpkg_installed}/lib" >> $GITHUB_ENV + # Output for cmake extra-args (use forward slashes to avoid YAML escape issues) + # Use .a extension for MinGW, .lib for MSVC + if [[ "${triplet}" == *"mingw"* ]]; then + echo "wolfssl_include=${vcpkg_installed}/include" >> $GITHUB_OUTPUT + echo "wolfssl_library=${vcpkg_installed}/lib/libwolfssl.a" >> $GITHUB_OUTPUT + echo "openssl_root=${vcpkg_installed}" >> $GITHUB_OUTPUT + else + echo "wolfssl_include=${vcpkg_installed}/include" >> $GITHUB_OUTPUT + echo "wolfssl_library=${vcpkg_installed}/lib/wolfssl.lib" >> $GITHUB_OUTPUT + echo "openssl_root=${vcpkg_installed}" >> $GITHUB_OUTPUT + fi + - name: Set vcpkg paths (Linux) if: runner.os == 'Linux' id: vcpkg-paths-linux @@ -454,7 +521,8 @@ jobs: - name: Boost B2 Workflow uses: alandefreitas/cpp-actions/b2-workflow@v1.9.0 - if: ${{ !matrix.coverage }} + # TEMP: Skip B2 on Windows to test if CMake builds pass + if: ${{ !matrix.coverage && runner.os != 'Windows' }} env: ASAN_OPTIONS: ${{ ((matrix.compiler == 'apple-clang' || matrix.compiler == 'clang') && 'detect_invalid_pointer_pairs=0:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1') || 'detect_invalid_pointer_pairs=2:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1' }} with: @@ -496,8 +564,11 @@ jobs: -D BOOST_INCLUDE_LIBRARIES="${{ steps.patch.outputs.module }}" ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} - # Windows: CMAKE_TOOLCHAIN_FILE is set via environment variable in "Set vcpkg paths" step - # The action picks it up automatically. Don't pass via extra-args (format() corrupts backslashes) + ${{ runner.os == 'Windows' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-windows.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-windows.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D OPENSSL_ROOT_DIR={0}', steps.vcpkg-paths-windows.outputs.openssl_root) || '' }} + ${{ matrix.vcpkg-triplet && format('-D VCPKG_TARGET_TRIPLET={0}', matrix.vcpkg-triplet) || '' }} + # CMAKE_TOOLCHAIN_FILE and VCPKG_INSTALLED_DIR are set via environment variables toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} package: false package-artifact: false @@ -534,6 +605,10 @@ jobs: -D CMAKE_PREFIX_PATH=${{ steps.patch.outputs.workspace_root }}/.local ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-windows.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-windows.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D OPENSSL_ROOT_DIR={0}', steps.vcpkg-paths-windows.outputs.openssl_root) || '' }} + ${{ matrix.vcpkg-triplet && format('-D VCPKG_TARGET_TRIPLET={0}', matrix.vcpkg-triplet) || '' }} toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} ref-source-dir: boost-root/libs/corosio trace-commands: true @@ -559,6 +634,10 @@ jobs: -D BOOST_CI_INSTALL_TEST=OFF ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-windows.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-windows.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D OPENSSL_ROOT_DIR={0}', steps.vcpkg-paths-windows.outputs.openssl_root) || '' }} + ${{ matrix.vcpkg-triplet && format('-D VCPKG_TARGET_TRIPLET={0}', matrix.vcpkg-triplet) || '' }} toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} ref-source-dir: boost-root/libs/corosio/test/cmake_test @@ -582,13 +661,75 @@ jobs: shared: ${{ matrix.shared }} cmake-version: '>=3.20' extra-args: | + -D Boost_VERBOSE=ON ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-windows.outputs.wolfssl_include) || '' }} + ${{ runner.os == 'Windows' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-windows.outputs.wolfssl_library) || '' }} + ${{ runner.os == 'Windows' && format('-D OPENSSL_ROOT_DIR={0}', steps.vcpkg-paths-windows.outputs.openssl_root) || '' }} + ${{ matrix.vcpkg-triplet && format('-D VCPKG_TARGET_TRIPLET={0}', matrix.vcpkg-triplet) || '' }} toolchain: ${{ env.CMAKE_TOOLCHAIN_FILE }} package: false package-artifact: false ref-source-dir: boost-root + # Diagnostic: Compare test executables between workflows + - name: Diagnose Root Project Build (Windows) + if: ${{ (matrix.build-cmake || matrix.is-earliest) && runner.os == 'Windows' }} + shell: bash + run: | + # Helper function to get file size portably + get_size() { + if [ -f "$1" ]; then + wc -c < "$1" | tr -d ' ' + else + echo "0" + fi + } + + echo "=== Comparing builds between Boost CMake and Root Project workflows ===" + echo "" + echo "=== PATH ===" + echo "$PATH" | tr ':' '\n' | head -20 + echo "" + echo "=== Root Project test executable ===" + root_exe=$(find __build_root_test__ -name "boost_corosio_tests.exe" 2>/dev/null | head -1) + boost_exe=$(find __build_cmake_test__ -name "boost_corosio_tests.exe" 2>/dev/null | head -1) + + if [ -n "$root_exe" ] && [ -f "$root_exe" ]; then + echo "Found: $root_exe" + echo "Size: $(get_size "$root_exe") bytes" + echo "" + echo "=== Dependencies (dumpbin) ===" + dumpbin //dependents "$root_exe" 2>/dev/null || echo "dumpbin not available" + else + echo "Root project test executable not found" + fi + + echo "" + echo "=== Boost CMake test executable ===" + if [ -n "$boost_exe" ] && [ -f "$boost_exe" ]; then + echo "Found: $boost_exe" + echo "Size: $(get_size "$boost_exe") bytes" + else + echo "Boost CMake test executable not found" + fi + + echo "" + echo "=== Size comparison ===" + if [ -n "$root_exe" ] && [ -f "$root_exe" ] && [ -n "$boost_exe" ] && [ -f "$boost_exe" ]; then + root_size=$(get_size "$root_exe") + boost_size=$(get_size "$boost_exe") + echo "Root project: $root_size bytes" + echo "Boost CMake: $boost_size bytes" + if [ "$root_size" != "$boost_size" ]; then + echo "WARNING: Executable sizes differ!" + fi + fi + + echo "" + echo "=== Diagnostic complete ===" + - name: Codecov if: ${{ matrix.coverage }} env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 82bd77df..e4a7e975 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,7 +64,7 @@ foreach (BOOST_COROSIO_DEPENDENCY ${BOOST_COROSIO_DEPENDENCIES}) endif () endforeach () -# Include asio and filesystem which are needed by capy's tests +# Include asio and filesystem which are needed by corosio's tests if (BOOST_COROSIO_BUILD_TESTS) list(APPEND BOOST_COROSIO_INCLUDE_LIBRARIES asio filesystem) endif () @@ -209,7 +209,12 @@ if (WolfSSL_FOUND) add_library(Boost::corosio_wolfssl ALIAS boost_corosio_wolfssl) boost_corosio_setup_properties(boost_corosio_wolfssl) target_link_libraries(boost_corosio_wolfssl PUBLIC boost_corosio) - target_link_libraries(boost_corosio_wolfssl PRIVATE WolfSSL::WolfSSL) + # PUBLIC ensures WolfSSL is linked into final executables (static lib deps don't embed) + target_link_libraries(boost_corosio_wolfssl PUBLIC WolfSSL::WolfSSL) + # WolfSSL on Windows/MinGW needs crypt32 for certificate store access + if (WIN32) + target_link_libraries(boost_corosio_wolfssl PUBLIC crypt32) + endif () target_compile_definitions(boost_corosio_wolfssl PUBLIC BOOST_COROSIO_HAS_WOLFSSL) endif () @@ -231,7 +236,12 @@ if (OpenSSL_FOUND) add_library(Boost::corosio_openssl ALIAS boost_corosio_openssl) boost_corosio_setup_properties(boost_corosio_openssl) target_link_libraries(boost_corosio_openssl PUBLIC boost_corosio) - target_link_libraries(boost_corosio_openssl PRIVATE OpenSSL::SSL OpenSSL::Crypto) + # PUBLIC ensures OpenSSL is linked into final executables (static lib deps don't embed) + target_link_libraries(boost_corosio_openssl PUBLIC OpenSSL::SSL OpenSSL::Crypto) + # OpenSSL on Windows/MinGW needs ws2_32 and crypt32 for socket and cert APIs + if (WIN32) + target_link_libraries(boost_corosio_openssl PUBLIC ws2_32 crypt32) + endif () target_compile_definitions(boost_corosio_openssl PUBLIC BOOST_COROSIO_HAS_OPENSSL) endif () diff --git a/cmake/FindWolfSSL.cmake b/cmake/FindWolfSSL.cmake index 9e2d19aa..8276f8d6 100644 --- a/cmake/FindWolfSSL.cmake +++ b/cmake/FindWolfSSL.cmake @@ -10,8 +10,16 @@ # Provides imported targets: # WolfSSL::WolfSSL -find_path(WolfSSL_INCLUDE_DIR "wolfssl/ssl.h") -find_library(WolfSSL_LIBRARY NAMES "wolfssl" "libwolfssl") +find_path(WolfSSL_INCLUDE_DIR "wolfssl/ssl.h" + HINTS + "$ENV{WOLFSSL_INCLUDE}" + "$ENV{WOLFSSL_ROOT}/include" +) +find_library(WolfSSL_LIBRARY NAMES "wolfssl" "libwolfssl" + HINTS + "$ENV{WOLFSSL_LIBRARY_PATH}" + "$ENV{WOLFSSL_ROOT}/lib" +) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(WolfSSL diff --git a/include/boost/corosio/tls/context.hpp b/include/boost/corosio/tls/context.hpp index 2e23b378..6f5eae72 100644 --- a/include/boost/corosio/tls/context.hpp +++ b/include/boost/corosio/tls/context.hpp @@ -186,6 +186,10 @@ get_context_data( context const& ) noexcept; @see role */ +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4251) // shared_ptr needs dll-interface +#endif class BOOST_COROSIO_DECL context { struct impl; @@ -900,6 +904,9 @@ class BOOST_COROSIO_DECL context void set_password_callback( Callback callback ); }; +#ifdef _MSC_VER +#pragma warning(pop) +#endif template void diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 00bd8d37..43140a3b 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -23,31 +23,9 @@ #include #include -#include -#include #include -#include #include -// Debug logging for CI timeout investigation -// Set COROSIO_TLS_DEBUG=1 to enable detailed logging -namespace { -inline bool tls_debug_enabled() -{ - static bool enabled = std::getenv("COROSIO_TLS_DEBUG") != nullptr; - return enabled; -} - -inline auto now_ms() -{ - return std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); -} -} // anonymous namespace - -#define TLS_DEBUG(msg) \ - do { if (tls_debug_enabled()) std::cerr << "[OPENSSL " << now_ms() << "ms] " << msg << "\n"; } while(0) - /* openssl_stream Architecture =========================== @@ -349,58 +327,44 @@ struct openssl_stream_impl_ capy::task flush_output(std::stop_token token) { - TLS_DEBUG("flush_output: start, stop_requested=" << token.stop_requested()); while(BIO_ctrl_pending(ext_bio_) > 0 && !token.stop_requested()) { int pending = static_cast(BIO_ctrl_pending(ext_bio_)); int to_read = (std::min)(pending, static_cast(out_buf_.size())); int n = BIO_read(ext_bio_, out_buf_.data(), to_read); - TLS_DEBUG("flush_output: BIO_read returned " << n << " bytes"); if(n <= 0) break; // Write to underlying stream - TLS_DEBUG("flush_output: acquiring mutex"); auto guard = co_await io_mutex_.scoped_lock(); - TLS_DEBUG("flush_output: calling s_.write_some(" << n << " bytes)"); auto [ec, written] = co_await s_.write_some( capy::mutable_buffer(out_buf_.data(), static_cast(n))); - TLS_DEBUG("flush_output: s_.write_some returned ec=" << ec.message() << " written=" << written); if(ec) co_return ec; } if(token.stop_requested()) { - TLS_DEBUG("flush_output: stop_requested, returning canceled"); co_return make_error_code(system::errc::operation_canceled); } - TLS_DEBUG("flush_output: done"); co_return system::error_code{}; } capy::task read_input(std::stop_token token) { - TLS_DEBUG("read_input: start, stop_requested=" << token.stop_requested()); if(token.stop_requested()) { - TLS_DEBUG("read_input: already stopped, returning canceled"); co_return make_error_code(system::errc::operation_canceled); } - - TLS_DEBUG("read_input: acquiring mutex"); auto guard = co_await io_mutex_.scoped_lock(); - TLS_DEBUG("read_input: calling s_.read_some"); auto [ec, n] = co_await s_.read_some( capy::mutable_buffer(in_buf_.data(), in_buf_.size())); - TLS_DEBUG("read_input: s_.read_some returned ec=" << ec.message() << " n=" << n); if(ec) co_return ec; // Feed data into OpenSSL int written = BIO_write(ext_bio_, in_buf_.data(), static_cast(n)); (void)written; - TLS_DEBUG("read_input: BIO_write returned " << written); co_return system::error_code{}; } @@ -601,14 +565,12 @@ struct openssl_stream_impl_ std::coroutine_handle<> continuation, capy::executor_ref d) { - TLS_DEBUG("do_handshake: start, type=" << (type == openssl_stream::client ? "client" : "server") << " stop_requested=" << token.stop_requested()); system::error_code ec; int iteration = 0; while(!token.stop_requested()) { ++iteration; - TLS_DEBUG("do_handshake: iteration " << iteration << " stop_requested=" << token.stop_requested()); ERR_clear_error(); int ret; if(type == openssl_stream::client) @@ -616,49 +578,36 @@ struct openssl_stream_impl_ else ret = SSL_accept(ssl_); - TLS_DEBUG("do_handshake: SSL_connect/accept returned " << ret); - if(ret == 1) { - TLS_DEBUG("do_handshake: handshake complete, flushing output"); // Handshake completed - flush any remaining output ec = co_await flush_output(token); - TLS_DEBUG("do_handshake: flush_output returned ec=" << ec.message()); break; } else { int err = SSL_get_error(ssl_, ret); - TLS_DEBUG("do_handshake: SSL_get_error=" << err); if(err == SSL_ERROR_WANT_WRITE) { - TLS_DEBUG("do_handshake: WANT_WRITE, flushing"); ec = co_await flush_output(token); - TLS_DEBUG("do_handshake: flush_output returned ec=" << ec.message()); if(ec) break; } else if(err == SSL_ERROR_WANT_READ) { - TLS_DEBUG("do_handshake: WANT_READ, flushing then reading"); // Flush output first (e.g., ClientHello) ec = co_await flush_output(token); - TLS_DEBUG("do_handshake: flush_output returned ec=" << ec.message()); if(ec) break; - - TLS_DEBUG("do_handshake: calling read_input"); // Then read response ec = co_await read_input(token); - TLS_DEBUG("do_handshake: read_input returned ec=" << ec.message()); if(ec) break; } else { unsigned long ssl_err = ERR_get_error(); - TLS_DEBUG("do_handshake: SSL error " << ssl_err); ec = system::error_code( static_cast(ssl_err), system::system_category()); break; @@ -668,11 +617,8 @@ struct openssl_stream_impl_ if(token.stop_requested()) { - TLS_DEBUG("do_handshake: stop_requested after loop, setting canceled"); ec = make_error_code(system::errc::operation_canceled); } - - TLS_DEBUG("do_handshake: done, ec=" << ec.message()); *ec_out = ec; d.dispatch(capy::coro{continuation}).resume(); From ad9d257e9702607ecd061f6b240f9e4db3facfd6 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Sat, 24 Jan 2026 00:36:08 +0000 Subject: [PATCH 5/9] fix: mocket port allocation for parallel test execution --- .github/workflows/ci.yml | 6 ++-- src/corosio/src/test/mocket.cpp | 47 +++++++++++++++++++++------- src/corosio/src/test/socket_pair.cpp | 8 +++++ test/unit/CMakeLists.txt | 5 +++ 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70f5b541..07345928 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -562,6 +562,7 @@ jobs: extra-args: | -D Boost_VERBOSE=ON -D BOOST_INCLUDE_LIBRARIES="${{ steps.patch.outputs.module }}" + ${{ matrix.compiler == 'mingw' && '-D CMAKE_VERBOSE_MAKEFILE=ON' || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} ${{ runner.os == 'Windows' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-windows.outputs.wolfssl_include) || '' }} @@ -662,6 +663,7 @@ jobs: cmake-version: '>=3.20' extra-args: | -D Boost_VERBOSE=ON + ${{ matrix.compiler == 'mingw' && '-D CMAKE_VERBOSE_MAKEFILE=ON' || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-linux.outputs.wolfssl_include) || '' }} ${{ runner.os == 'Linux' && format('-D WolfSSL_LIBRARY={0}', steps.vcpkg-paths-linux.outputs.wolfssl_library) || '' }} ${{ runner.os == 'Windows' && format('-D WolfSSL_INCLUDE_DIR={0}', steps.vcpkg-paths-windows.outputs.wolfssl_include) || '' }} @@ -693,8 +695,8 @@ jobs: echo "$PATH" | tr ':' '\n' | head -20 echo "" echo "=== Root Project test executable ===" - root_exe=$(find __build_root_test__ -name "boost_corosio_tests.exe" 2>/dev/null | head -1) - boost_exe=$(find __build_cmake_test__ -name "boost_corosio_tests.exe" 2>/dev/null | head -1) + root_exe=$(find boost-root/libs/corosio/__build_root_test__ -name "boost_corosio_tests.exe" 2>/dev/null | head -1 || true) + boost_exe=$(find boost-root/__build_cmake_test__ -name "boost_corosio_tests.exe" 2>/dev/null | head -1 || true) if [ -n "$root_exe" ] && [ -f "$root_exe" ]; then echo "Found: $root_exe" diff --git a/src/corosio/src/test/mocket.cpp b/src/corosio/src/test/mocket.cpp index c5c6d116..fb03b6d2 100644 --- a/src/corosio/src/test/mocket.cpp +++ b/src/corosio/src/test/mocket.cpp @@ -20,6 +20,8 @@ #include #include +#include +#include #include #include #include @@ -429,17 +431,17 @@ is_open() const noexcept namespace { -// Test port range for mocket connections -constexpr std::uint16_t test_port_base = 49200; -constexpr std::uint16_t test_port_range = 100; -std::uint16_t next_test_port = 0; +// Use atomic for thread safety when tests run in parallel +std::atomic next_test_port{0}; std::uint16_t get_test_port() noexcept { - auto port = test_port_base + (next_test_port % test_port_range); - ++next_test_port; - return static_cast(port); + // Use a wide port range in the dynamic/ephemeral range (49152-65535) + constexpr std::uint16_t port_base = 49152; + constexpr std::uint16_t port_range = 16383; + auto offset = next_test_port.fetch_add(1, std::memory_order_relaxed); + return static_cast(port_base + (offset % port_range)); } } // namespace @@ -460,16 +462,35 @@ make_mockets(capy::execution_context& ctx, capy::test::fuse& f) auto& ioc = static_cast(ctx); auto ex = ioc.get_executor(); - // Get a test port - std::uint16_t port = get_test_port(); system::error_code accept_ec; system::error_code connect_ec; bool accept_done = false; bool connect_done = false; - // Set up loopback connection using acceptor + // Try multiple ports in case of conflicts (TIME_WAIT, parallel tests, etc.) + std::uint16_t port = 0; acceptor acc(ctx); - acc.listen(endpoint(urls::ipv4_address::loopback(), port)); + bool listening = false; + for (int attempt = 0; attempt < 20; ++attempt) + { + port = get_test_port(); + try + { + acc.listen(endpoint(urls::ipv4_address::loopback(), port)); + listening = true; + break; + } + catch (const system::system_error&) + { + acc.close(); + acc = acceptor(ctx); + } + } + if (!listening) + { + std::fprintf(stderr, "make_mockets: failed to find available port after 20 attempts\n"); + throw std::runtime_error("make_mockets: failed to find available port"); + } // Open impl2's socket for connect impl2.get_socket().open(); @@ -507,12 +528,16 @@ make_mockets(capy::execution_context& ctx, capy::test::fuse& f) // Check for errors if (!accept_done || accept_ec) { + std::fprintf(stderr, "make_mockets: accept failed (done=%d, ec=%s)\n", + accept_done, accept_ec.message().c_str()); acc.close(); throw std::runtime_error("mocket accept failed"); } if (!connect_done || connect_ec) { + std::fprintf(stderr, "make_mockets: connect failed (done=%d, ec=%s)\n", + connect_done, connect_ec.message().c_str()); acc.close(); accepted_socket.close(); throw std::runtime_error("mocket connect failed"); diff --git a/src/corosio/src/test/socket_pair.cpp b/src/corosio/src/test/socket_pair.cpp index 2f34c492..6a7f3f4d 100644 --- a/src/corosio/src/test/socket_pair.cpp +++ b/src/corosio/src/test/socket_pair.cpp @@ -16,6 +16,7 @@ #include #include +#include #include namespace boost { @@ -70,7 +71,10 @@ make_socket_pair(io_context& ioc) } } if (!listening) + { + std::fprintf(stderr, "socket_pair: failed to find available port after 20 attempts\n"); throw std::runtime_error("socket_pair: failed to find available port"); + } socket s1(ioc); socket s2(ioc); @@ -100,12 +104,16 @@ make_socket_pair(io_context& ioc) if (!accept_done || accept_ec) { + std::fprintf(stderr, "socket_pair: accept failed (done=%d, ec=%s)\n", + accept_done, accept_ec.message().c_str()); acc.close(); throw std::runtime_error("socket_pair accept failed"); } if (!connect_done || connect_ec) { + std::fprintf(stderr, "socket_pair: connect failed (done=%d, ec=%s)\n", + connect_done, connect_ec.message().c_str()); acc.close(); s1.close(); throw std::runtime_error("socket_pair connect failed"); diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 87cab659..17f4641d 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -34,6 +34,11 @@ if (OpenSSL_FOUND) target_compile_definitions(boost_corosio_tests PRIVATE BOOST_COROSIO_HAS_OPENSSL=1) endif() +# MinGW linker is order-sensitive; system libs must come last +if (MINGW) + target_link_libraries(boost_corosio_tests PRIVATE ws2_32 crypt32) +endif() + target_include_directories(boost_corosio_tests PRIVATE . ../../) # Register individual tests with CTest From a07db3bcf8655c365e3ba287c9d468d3673682f4 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Sat, 24 Jan 2026 00:57:13 +0000 Subject: [PATCH 6/9] fix: CI Issues --- .github/workflows/ci.yml | 3 ++- CMakeLists.txt | 24 ++++++++++++++++++++---- build/Jamfile | 3 +++ test/unit/CMakeLists.txt | 9 ++++----- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07345928..349b965c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -522,7 +522,8 @@ jobs: - name: Boost B2 Workflow uses: alandefreitas/cpp-actions/b2-workflow@v1.9.0 # TEMP: Skip B2 on Windows to test if CMake builds pass - if: ${{ !matrix.coverage && runner.os != 'Windows' }} + # if: ${{ !matrix.coverage && runner.os != 'Windows' }} + if: ${{ !matrix.coverage }} env: ASAN_OPTIONS: ${{ ((matrix.compiler == 'apple-clang' || matrix.compiler == 'clang') && 'detect_invalid_pointer_pairs=0:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1') || 'detect_invalid_pointer_pairs=2:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1' }} with: diff --git a/CMakeLists.txt b/CMakeLists.txt index e4a7e975..5c769a9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,13 @@ boost_corosio_setup_properties(boost_corosio) #------------------------------------------------- list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") find_package(WolfSSL) +# MinGW's linker is single-pass and order-sensitive; system libs must follow +# the static libraries that reference them. Add as interface dependencies so +# CMake's dependency ordering places them after WolfSSL in the link command. +if (MINGW AND TARGET WolfSSL::WolfSSL) + set_property(TARGET WolfSSL::WolfSSL APPEND PROPERTY + INTERFACE_LINK_LIBRARIES ws2_32 crypt32) +endif() if (WolfSSL_FOUND) file(GLOB_RECURSE BOOST_COROSIO_WOLFSSL_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/boost/corosio/wolfssl/*.hpp") @@ -211,8 +218,9 @@ if (WolfSSL_FOUND) target_link_libraries(boost_corosio_wolfssl PUBLIC boost_corosio) # PUBLIC ensures WolfSSL is linked into final executables (static lib deps don't embed) target_link_libraries(boost_corosio_wolfssl PUBLIC WolfSSL::WolfSSL) - # WolfSSL on Windows/MinGW needs crypt32 for certificate store access - if (WIN32) + # WolfSSL on Windows needs crypt32 for certificate store access. + # For MinGW, this is handled via WolfSSL::WolfSSL's interface deps (link order matters). + if (WIN32 AND NOT MINGW) target_link_libraries(boost_corosio_wolfssl PUBLIC crypt32) endif () target_compile_definitions(boost_corosio_wolfssl PUBLIC BOOST_COROSIO_HAS_WOLFSSL) @@ -224,6 +232,13 @@ endif () # #------------------------------------------------- find_package(OpenSSL) +# MinGW's linker is single-pass and order-sensitive; system libs must follow +# the static libraries that reference them. Add as interface dependencies so +# CMake's dependency ordering places them after OpenSSL in the link command. +if (MINGW AND TARGET OpenSSL::Crypto) + set_property(TARGET OpenSSL::Crypto APPEND PROPERTY + INTERFACE_LINK_LIBRARIES ws2_32 crypt32) +endif() if (OpenSSL_FOUND) file(GLOB_RECURSE BOOST_COROSIO_OPENSSL_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/boost/corosio/openssl/*.hpp") @@ -238,8 +253,9 @@ if (OpenSSL_FOUND) target_link_libraries(boost_corosio_openssl PUBLIC boost_corosio) # PUBLIC ensures OpenSSL is linked into final executables (static lib deps don't embed) target_link_libraries(boost_corosio_openssl PUBLIC OpenSSL::SSL OpenSSL::Crypto) - # OpenSSL on Windows/MinGW needs ws2_32 and crypt32 for socket and cert APIs - if (WIN32) + # OpenSSL on Windows needs ws2_32 and crypt32 for socket and cert APIs. + # For MinGW, this is handled via OpenSSL::Crypto's interface deps (link order matters). + if (WIN32 AND NOT MINGW) target_link_libraries(boost_corosio_openssl PUBLIC ws2_32 crypt32) endif () target_compile_definitions(boost_corosio_openssl PUBLIC BOOST_COROSIO_HAS_OPENSSL) diff --git a/build/Jamfile b/build/Jamfile index ae2781e1..8923d08e 100644 --- a/build/Jamfile +++ b/build/Jamfile @@ -31,6 +31,7 @@ project boost/corosio # System libraries lib ws2_32 ; +lib crypt32 ; alias corosio_sources : [ glob-tree-ex src/corosio/src : *.cpp ] ; @@ -79,9 +80,11 @@ lib boost_corosio_wolfssl /boost/corosio//boost_corosio ../src/corosio [ ac.check-library /wolfssl//wolfssl : /wolfssl//wolfssl : no ] + windows:crypt32 : usage-requirements /boost/corosio//boost_corosio BOOST_COROSIO_HAS_WOLFSSL + windows:crypt32 ; boost-install boost_corosio boost_corosio_openssl boost_corosio_wolfssl ; diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 17f4641d..dcd70fb9 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -27,16 +27,15 @@ target_link_libraries( if (WolfSSL_FOUND) target_link_libraries(boost_corosio_tests PRIVATE boost_corosio_wolfssl) target_compile_definitions(boost_corosio_tests PRIVATE BOOST_COROSIO_HAS_WOLFSSL=1) +else() + message(FATAL_ERROR "WolfSSL is required for corosio tests") endif() if (OpenSSL_FOUND) target_link_libraries(boost_corosio_tests PRIVATE boost_corosio_openssl) target_compile_definitions(boost_corosio_tests PRIVATE BOOST_COROSIO_HAS_OPENSSL=1) -endif() - -# MinGW linker is order-sensitive; system libs must come last -if (MINGW) - target_link_libraries(boost_corosio_tests PRIVATE ws2_32 crypt32) +else() + message(FATAL_ERROR "OpenSSL is required for corosio tests") endif() target_include_directories(boost_corosio_tests PRIVATE . ../../) From ee2715c7fe6eb141eb69b4526fb59959ea736b49 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Sat, 24 Jan 2026 10:04:00 +0000 Subject: [PATCH 7/9] fix: additional remediation on mocket and socket pair for intermittent failures --- src/corosio/src/test/mocket.cpp | 21 ++++++++++++++++++++- src/corosio/src/test/socket_pair.cpp | 21 ++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/corosio/src/test/mocket.cpp b/src/corosio/src/test/mocket.cpp index fb03b6d2..d718cb90 100644 --- a/src/corosio/src/test/mocket.cpp +++ b/src/corosio/src/test/mocket.cpp @@ -26,6 +26,12 @@ #include #include +#ifdef _WIN32 +#include // _getpid() +#else +#include // getpid() +#endif + namespace boost { namespace corosio { namespace test { @@ -440,8 +446,21 @@ get_test_port() noexcept // Use a wide port range in the dynamic/ephemeral range (49152-65535) constexpr std::uint16_t port_base = 49152; constexpr std::uint16_t port_range = 16383; + + // Include PID to avoid port collisions between parallel test processes. + // On Windows with SO_REUSEADDR, multiple processes can bind the same port, + // causing connections to go to the wrong listener ("port stealing"). + // By using different port ranges per process, we avoid this issue. +#ifdef _WIN32 + auto pid = static_cast(_getpid()); +#else + auto pid = static_cast(getpid()); +#endif + // Mix the PID bits to spread processes across the port range + auto pid_offset = static_cast((pid * 7919) % port_range); + auto offset = next_test_port.fetch_add(1, std::memory_order_relaxed); - return static_cast(port_base + (offset % port_range)); + return static_cast(port_base + ((pid_offset + offset) % port_range)); } } // namespace diff --git a/src/corosio/src/test/socket_pair.cpp b/src/corosio/src/test/socket_pair.cpp index 6a7f3f4d..ad125229 100644 --- a/src/corosio/src/test/socket_pair.cpp +++ b/src/corosio/src/test/socket_pair.cpp @@ -19,6 +19,12 @@ #include #include +#ifdef _WIN32 +#include // _getpid() +#else +#include // getpid() +#endif + namespace boost { namespace corosio { namespace test { @@ -34,8 +40,21 @@ get_test_port() noexcept // Use a wide port range in the dynamic/ephemeral range (49152-65535) constexpr std::uint16_t port_base = 49152; constexpr std::uint16_t port_range = 16383; // 49152-65535 + + // Include PID to avoid port collisions between parallel test processes. + // On Windows with SO_REUSEADDR, multiple processes can bind the same port, + // causing connections to go to the wrong listener ("port stealing"). + // By using different port ranges per process, we avoid this issue. +#ifdef _WIN32 + auto pid = static_cast(_getpid()); +#else + auto pid = static_cast(getpid()); +#endif + // Mix the PID bits to spread processes across the port range + auto pid_offset = static_cast((pid * 7919) % port_range); + auto offset = next_test_port.fetch_add(1, std::memory_order_relaxed); - return static_cast(port_base + (offset % port_range)); + return static_cast(port_base + ((pid_offset + offset) % port_range)); } } // namespace From 3784fc87e19e2119215d30704cc08ed2b9f27c3f Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Sat, 24 Jan 2026 10:06:07 +0000 Subject: [PATCH 8/9] Fix potential IOCP race on synchronous completion When I/O operations complete synchronously (returning 0 instead of WSA_IO_PENDING), FILE_SKIP_COMPLETION_PORT_ON_SUCCESS should prevent IOCP from posting a completion packet. However, if the flag failed to set or under certain edge conditions, both the direct path and IOCP completion handler could process the same operation. Use InterlockedCompareExchange to atomically race for ownership of the completion. Only the winner (CAS returns 0) proceeds to set the result fields and post to the scheduler. This prevents double coroutine resumption and potential state corruption. Applies to connect, read_some, write_some, and accept operations. --- src/corosio/src/detail/iocp/sockets.cpp | 46 +++++++++++++++++++------ 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/corosio/src/detail/iocp/sockets.cpp b/src/corosio/src/detail/iocp/sockets.cpp index 54ce6504..fdc33889 100644 --- a/src/corosio/src/detail/iocp/sockets.cpp +++ b/src/corosio/src/detail/iocp/sockets.cpp @@ -407,9 +407,17 @@ connect( } else { + // Synchronous completion - with FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, + // IOCP shouldn't post a packet. But if the flag failed to set or under + // certain conditions, IOCP might still deliver a completion. Use CAS + // to race with IOCP: only set fields and post if we win (CAS returns 0). + // If IOCP wins, it already set the fields via complete() and processed. svc_.work_finished(); - op.dwError = 0; - svc_.post(&op); + if (::InterlockedCompareExchange(&op.ready_, 1, 0) == 0) + { + op.dwError = 0; + svc_.post(&op); + } } } @@ -480,10 +488,18 @@ read_some( } else { + // Synchronous completion - with FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, + // IOCP shouldn't post a packet. But if the flag failed to set or under + // certain conditions, IOCP might still deliver a completion. Use CAS + // to race with IOCP: only set fields and post if we win (CAS returns 0). + // If IOCP wins, it already set the fields via complete() and processed. svc_.work_finished(); - op.bytes_transferred = static_cast(op.InternalHigh); - op.dwError = 0; - svc_.post(&op); + if (::InterlockedCompareExchange(&op.ready_, 1, 0) == 0) + { + op.bytes_transferred = static_cast(op.InternalHigh); + op.dwError = 0; + svc_.post(&op); + } } } @@ -551,10 +567,15 @@ write_some( } else { + // Synchronous completion - use CAS to race with IOCP. + // See read_some for detailed explanation. svc_.work_finished(); - op.bytes_transferred = static_cast(op.InternalHigh); - op.dwError = 0; - svc_.post(&op); + if (::InterlockedCompareExchange(&op.ready_, 1, 0) == 0) + { + op.bytes_transferred = static_cast(op.InternalHigh); + op.dwError = 0; + svc_.post(&op); + } } } @@ -1030,9 +1051,14 @@ accept( } else { + // Synchronous completion - use CAS to race with IOCP. + // See win_socket_impl_internal::read_some for detailed explanation. svc_.work_finished(); - op.dwError = 0; - svc_.post(&op); + if (::InterlockedCompareExchange(&op.ready_, 1, 0) == 0) + { + op.dwError = 0; + svc_.post(&op); + } } } From 8f706f72d09a860c993aa95cbc6b700b2142c579 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Sat, 24 Jan 2026 13:54:09 +0000 Subject: [PATCH 9/9] Restore TLS test utilities lost during rebase --- test/unit/tls/openssl_stream.cpp | 222 +++++- test/unit/tls/test_utils.hpp | 1155 ++++++++++++++++++++++++++++-- 2 files changed, 1320 insertions(+), 57 deletions(-) diff --git a/test/unit/tls/openssl_stream.cpp b/test/unit/tls/openssl_stream.cpp index 701fdbb8..2334a5fc 100644 --- a/test/unit/tls/openssl_stream.cpp +++ b/test/unit/tls/openssl_stream.cpp @@ -17,6 +17,7 @@ #include #include "test_utils.hpp" +#include #include "test_suite.hpp" namespace boost { @@ -103,6 +104,213 @@ struct openssl_stream_test make_stream, make_stream ); } } + + void + testStopTokenCancellation() + { + using namespace tls::test; + + // Cancel during handshake + { + io_context ioc; + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + run_stop_token_handshake_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Cancel during read + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( context_mode::separate_cert ); + run_stop_token_read_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Cancel during write + { + io_context ioc; + auto [client_ctx, server_ctx] = make_contexts( context_mode::separate_cert ); + run_stop_token_write_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testSocketErrorPropagation() + { + using namespace tls::test; + + // socket.cancel() while TLS blocked on socket I/O + { + io_context ioc; + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + run_socket_cancel_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Connection reset during handshake + { + io_context ioc; + auto client_ctx = make_client_context(); + auto server_ctx = make_server_context(); + run_connection_reset_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testCertificateValidation() + { + using namespace tls::test; + + // Untrusted CA - client trusts different CA than server's cert + // Should fail immediately during certificate verification + { + io_context ioc; + auto client_ctx = make_untrusted_ca_client_context(); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Expired certificate - server cert expired Jan 2, 2020 + // Client trusts the cert but should reject due to expiry + { + io_context ioc; + auto client_ctx = make_expired_client_context(); + auto server_ctx = make_expired_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testSni() + { + using namespace tls::test; + + // Test SNI + hostname verification - correct hostname succeeds + // Server cert has CN=www.example.com + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "www.example.com" ); + auto server_ctx = make_server_context(); + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Test hostname verification - wrong hostname fails + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "wrong.example.com" ); + auto server_ctx = make_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testSniCallback() + { + using namespace tls::test; + + // SNI callback accepts the hostname - handshake succeeds + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "www.example.com" ); + + auto server_ctx = make_server_context(); + server_ctx.set_servername_callback( + []( std::string_view hostname ) -> bool + { + return hostname == "www.example.com"; + }); + + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // SNI callback rejects the hostname - handshake fails + { + io_context ioc; + auto client_ctx = make_client_context(); + client_ctx.set_hostname( "www.example.com" ); + + auto server_ctx = make_server_context(); + server_ctx.set_servername_callback( + []( std::string_view hostname ) -> bool + { + return hostname == "api.example.com"; // Only accept api.* + }); + + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testMtls() + { + using namespace tls::test; + + // mTLS success - client provides valid cert + { + io_context ioc; + auto client_ctx = make_mtls_client_context(); + auto server_ctx = make_mtls_server_context(); + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // mTLS failure - client provides no cert but server requires it + { + io_context ioc; + auto client_ctx = make_chain_client_context(); + auto server_ctx = make_mtls_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // mTLS failure - client provides cert signed by WRONG CA + { + io_context ioc; + auto client_ctx = make_invalid_mtls_client_context(); + auto server_ctx = make_mtls_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } + + void + testCertificateChain() + { + using namespace tls::test; + + // Server sends full chain (entity + intermediate) - client trusts only root + // Should succeed because server sends intermediate in chain + { + io_context ioc; + auto client_ctx = make_rootonly_client_context(); + auto server_ctx = make_fullchain_server_context(); + run_tls_test( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + + // Server sends only entity cert - client trusts only root + // Should fail because client can't build chain to root + { + io_context ioc; + auto client_ctx = make_rootonly_client_context(); + auto server_ctx = make_chain_server_context(); + run_tls_test_fail( ioc, client_ctx, server_ctx, + make_stream, make_stream ); + } + } #endif void @@ -112,9 +320,17 @@ struct openssl_stream_test testSuccessCases(); testTlsShutdown(); testStreamTruncated(); - // Failure tests disabled: socket cancellation doesn't propagate to - // TLS handshake operations, causing hangs when one side fails. - // testFailureCases(); + testFailureCases(); + testStopTokenCancellation(); + testSocketErrorPropagation(); + testCertificateValidation(); + testSni(); + testSniCallback(); + testMtls(); + testCertificateChain(); +#else + std::cerr << "openssl_stream tests SKIPPED: OpenSSL not found\n"; + static_assert(false, "OpenSSL not found"); #endif } }; diff --git a/test/unit/tls/test_utils.hpp b/test/unit/tls/test_utils.hpp index 969fcacc..38917610 100644 --- a/test/unit/tls/test_utils.hpp +++ b/test/unit/tls/test_utils.hpp @@ -23,6 +23,10 @@ #include "test_suite.hpp" +#include +#include +#include + namespace boost { namespace corosio { namespace tls { @@ -34,8 +38,12 @@ namespace test { // //------------------------------------------------------------------------------ -// Self-signed server certificate from Boost.Beast (valid, self-signed) -// This cert is also its own CA (self-signed) +// Self-signed server certificate from Boost.Beast +// Subject: C=US, ST=CA, L=Los Angeles, O=Beast, CN=www.example.com +// Valid: 2021-07-06 to 2048-11-21 (self-signed, CA:TRUE) +// Command: +// openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 10000 -nodes +// -subj "/C=US/ST=CA/L=Los Angeles/O=Beast/CN=www.example.com" inline constexpr char const* server_cert_pem = "-----BEGIN CERTIFICATE-----\n" "MIIDlTCCAn2gAwIBAgIUOLxr3q7Wd/pto1+2MsW4fdRheCIwDQYJKoZIhvcNAQEL\n" @@ -63,7 +71,8 @@ inline constexpr char const* server_cert_pem = // CA cert is the same as server cert (self-signed) inline constexpr char const* ca_cert_pem = server_cert_pem; -// Server private key from Boost.Beast +// Server private key from Boost.Beast (RSA 2048-bit) +// Matches server_cert_pem above inline constexpr char const* server_key_pem = "-----BEGIN PRIVATE KEY-----\n" "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCz0GwgnxSBhygx\n" @@ -95,7 +104,12 @@ inline constexpr char const* server_key_pem = "-----END PRIVATE KEY-----\n"; // Different self-signed CA for "wrong CA" test scenarios -// (A different self-signed cert that won't verify server_cert_pem) +// Subject: CN=localhost +// Valid: 2023-01-01 to 2033-01-01 (self-signed) +// A different CA that won't verify server_cert_pem +// Command: +// openssl req -x509 -newkey rsa:2048 -keyout wrong_ca_key.pem -out wrong_ca_cert.pem +// -days 3650 -nodes -subj "/CN=localhost" inline constexpr char const* wrong_ca_cert_pem = "-----BEGIN CERTIFICATE-----\n" "MIICpDCCAYwCCQDU+pQ4P0jwoDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls\n" @@ -113,6 +127,443 @@ inline constexpr char const* wrong_ca_cert_pem = "0000000000000000000000000000000000000000000000000000000000000000000\n" "0000000000000000000000000000000000000000000000=\n" "-----END CERTIFICATE-----\n"; + +// Expired certificate for testing certificate expiry validation +// Subject: CN=www.example.com +// Valid: 2020-01-01 to 2020-01-02 (expired, self-signed, CA:TRUE) +// Command (Linux with faketime): +// faketime '2020-01-01 00:00:00' openssl req -x509 -newkey rsa:2048 +// -keyout expired_key.pem -out expired_cert.pem -days 1 -nodes +// -subj "/CN=www.example.com" +inline constexpr char const* expired_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDFTCCAf2gAwIBAgIUcWCw0O1DjiTT+alvcOHTN56vTh0wDQYJKoZIhvcNAQEL\n" + "BQAwGjEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMB4XDTIwMDEwMTAwMDAwMFoX\n" + "DTIwMDEwMjAwMDAwMFowGjEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIIBIjAN\n" + "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt5XT6f6Z/abfLI+L0MYD5cszhBa+\n" + "3h5ddlXypIerCwxiKR1gnjWafdWm/ZriML073ozTAhgF0bQg1VPRNDeSvyAUSJQp\n" + "5dPLjq1K4FwFBKAuo5GYWePE42vysAlOaJ70Rr0F2Lerk8e+FJJKGS9APWsi4FeQ\n" + "fSJc1zfODCieSuePBtjmbZJPe9gGrcv8d4KjQo3C0hA2qKZIQTkr0bHmqUtup9m7\n" + "0W5VNJdWgGdNpirDigCD/x4IZmEzP3mMnP0gp4JRsBEuGXi5nzejcpwrUHZL/Vmo\n" + "MAvYOsIHU8ewOxuKflaCq5rJjF1uk/i2+CoPiMGebSekJ0J8PAIcqCVrowIDAQAB\n" + "o1MwUTAdBgNVHQ4EFgQU2p57iEUtXAtUQV/iT5JZNSoHvdswHwYDVR0jBBgwFoAU\n" + "2p57iEUtXAtUQV/iT5JZNSoHvdswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B\n" + "AQsFAAOCAQEAEezlsqs0yc5FuegqLO3Hwko0knt4jpC2jOqYsId+90dpv8u/s/Um\n" + "znr8i5jiiv9R665DpTEFF9/ur4bJ5a3rmTE2udy9qn4MZZco0pBZ/7+dtOHwEsfY\n" + "+bS3Z+weVtsy8LpI6lUxREBUsmPrY+ZzEFOPdfWR1sh5NRX28oWW1ZhmaAdWjHNe\n" + "YQUC+yyblwFCNqSEdVUdtAOlndY5OrYdUSG1AE7T9z7p/simSKLfC/5IbgX+N3PP\n" + "0ntHB4+omQsBCqcgtrr0HC8he8xQrFBeEJBNwYevjMXvkcQIuwvvWvZtyMMJIw/i\n" + "/V5+QRAgU4In8r91KfCHHIY2jnjopTDELA==\n" + "-----END CERTIFICATE-----\n"; + +// Expired certificate private key (RSA 2048-bit) +// Matches expired_cert_pem above +inline constexpr char const* expired_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC3ldPp/pn9pt8s\n" + "j4vQxgPlyzOEFr7eHl12VfKkh6sLDGIpHWCeNZp91ab9muIwvTvejNMCGAXRtCDV\n" + "U9E0N5K/IBRIlCnl08uOrUrgXAUEoC6jkZhZ48Tja/KwCU5onvRGvQXYt6uTx74U\n" + "kkoZL0A9ayLgV5B9IlzXN84MKJ5K548G2OZtkk972Aaty/x3gqNCjcLSEDaopkhB\n" + "OSvRseapS26n2bvRblU0l1aAZ02mKsOKAIP/HghmYTM/eYyc/SCnglGwES4ZeLmf\n" + "N6NynCtQdkv9WagwC9g6wgdTx7A7G4p+VoKrmsmMXW6T+Lb4Kg+IwZ5tJ6QnQnw8\n" + "AhyoJWujAgMBAAECggEAMVH0pQPrzduzUC7eQn+4E1eUZvOPYm/o7v4nGjGCb4zr\n" + "oB0O1GIVN6Ia4z3lb2+fMmpF0+WtRomsWnNSnEMjzuno2RjI6sAMCzAeEglWpcf8\n" + "z5+xPND2l5xsDgPqByxQ9uIYPIEXfLOoKrGka4Cosvdh3sBXhm6hX4ZT+is9X2TC\n" + "kyoW906lMYXPFX5M9zb+GuGl3HuOXeLbZijwJ1tTMUZnk1fZyWEJt9kms4Fh7yS6\n" + "CNYzjKNK5LSvqjKMlcirj0x+X3GI4oJ+KWCeUxoUMtokSpHVVVFry/noEa7o1yOr\n" + "zCYWZQWeIJ5I2RrC3AFTMQATSg2s/DvjHPHazJ7UzQKBgQDhaamCOnPjk3vJBRNh\n" + "lt8/47rBOLD/Ua/Hh4iKgZ8MNJz6lHBTSd+ESZsSg9PNUCk8wmY8+LLV0CpRI+hF\n" + "0VDckyjmr1TqVBoc2GBpjPE6skUod/xBZOdQ4Upm2rF8E+JDMbuB8brcCJFCQYLM\n" + "GG6llHDHIczOgvp2yujCMxWJNQKBgQDQfywH3yQVuePiPbyiGK8ARFuMdHwlVwSP\n" + "FzivNXVVJp1E6zHoLHAOHIwUsVZYunflDKZriZ3AxjeiSSIMTaLAcPgGp8fP4sdX\n" + "lvENvjM4QggtYEVyuo5XrmovEtV6at8O5p984dwaAQoznZZv2K9Kt6/gx6a3+zQt\n" + "H8bdKJCUdwKBgD7KgD2WqtGqM8E7eLqmnGnfthY9BJEa4CxkxNRQZ02vGktzLhcF\n" + "bQ4csuXlcwquWc5jGLfDT43f/um7ZuiL9kp7c9lO3giohN2kKLc+W7ROFJXBVrOg\n" + "uA7/swoTwX0ezNiK8gCwpazFdjFOrnDMHYZiY0gVUkf0lHCi9VOjh0xBAoGAEW5A\n" + "WRwfoS1cTuLIbWjQ4J3WZYSriFehCvFvDL7UY10KEuPy1S055QQf9e7pgBt+wIhx\n" + "NVZY+O/ZYNjqXsryy1Hmem/2dXvJHJqC5po7H/3tPxXoWHIeSlhLiknxzP04Tr+b\n" + "H86mHwptNul61TjxVrbKnmkyl/kJYKhicMTeaXsCgYB+wNCxuQ4MIzErm7CXnKCp\n" + "xQoFFzR0Fhay5x86Ry9hxBYCeio2CSByV+pFX0AOvvJ7hhm3iSD5p91ulIgl3YfL\n" + "23Ot+Yles5ZYawVJ3cqeFGiG7vPi2KU9EztdnRlmJwF7P7m4XzzcNvvbK/FbOQT5\n" + "E7D5rHt+zVEyi3BDrCSTZw==\n" + "-----END PRIVATE KEY-----\n"; + +// Root CA certificate for certificate chain tests +// Subject: CN=Test Root CA +// Valid: 2026-01-22 to 2036-01-20 (self-signed, CA:TRUE) +// Command: +// openssl req -x509 -newkey rsa:2048 -keyout root_ca_key.pem -out root_ca_cert.pem +// -days 3650 -nodes -subj "/CN=Test Root CA" +inline constexpr char const* root_ca_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDDzCCAfegAwIBAgIUQFc5HqhX9NsPK7m+gssB9iLY6VwwDQYJKoZIhvcNAQEL\n" + "BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI2MDEyMjE3MzAwOFoXDTM2\n" + "MDEyMDE3MzAwOFowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG\n" + "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuZrR4YgRV9BC/9MnG6U0+3m8l+UDhklBeF04\n" + "nVeRhPQmDMDbZ4TxnH9zBc71EdvgCqVJr2GGa5QXU0a9yjKB7Vb97VFjO+MAZGjq\n" + "GRzuYDdNUlj0ZOa04ZIWLhRvTr5sA649DonSxw6tEla+PZtsr/numK6OOCkAa24D\n" + "WDEtWOHIp/xyLwGsJrwkqDniteQHec7RugufC9nvZHpiC/y23oFeRsg9cOda6hzq\n" + "LMvFV9lZkjp5ChlEoY3bNhDXG53l47k11Z0Qnv4A6SPVmveFS+D74KxbORdWIu6k\n" + "dd/C2zJ18XiT8N+NXgacEaSj8ygHExQ4BC8MyvJGqm8ZH6nZ4wIDAQABo1MwUTAd\n" + "BgNVHQ4EFgQUgzNRvlv4m9jsyfNAVU34IbvmiMcwHwYDVR0jBBgwFoAUgzNRvlv4\n" + "m9jsyfNAVU34IbvmiMcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" + "AQEAQC4S3sa4ZbSH7Df62GSZaQhD19HKMshlXCk+E2QwC7cfnaAAE1CKemd6hPe5\n" + "4Ofci9YdbRl6g0LF3SQe+DMMiK1sqjCSnEAOuPJ0fRcaVkh87SuUHOhucC9TQoLn\n" + "/oUPSQHvprghJk1HVOq7qQI6iQZjurODNBtddVAkk5r/1p4vaRPBtr471i3GSBBc\n" + "Hy51FXBcO+9910w7Pxrs5htSnAh5Eprn0+P0h/1liQhT5Fuz27PFTxCttcNvagfD\n" + "rdtULUbjRBePcR3ooCj88M2ndF0ifvMvGBYtsBdaY56dc0zkYACyiiFWV5kmSLM8\n" + "ay5B/d3dN2x7UoJRiZ2X7jD7sA==\n" + "-----END CERTIFICATE-----\n"; + +// Root CA private key (RSA 2048-bit) +// Matches root_ca_cert_pem above +inline constexpr char const* root_ca_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5mtHhiBFX0EL/\n" + "0ycbpTT7ebyX5QOGSUF4XTidV5GE9CYMwNtnhPGcf3MFzvUR2+AKpUmvYYZrlBdT\n" + "Rr3KMoHtVv3tUWM74wBkaOoZHO5gN01SWPRk5rThkhYuFG9OvmwDrj0OidLHDq0S\n" + "Vr49m2yv+e6Yro44KQBrbgNYMS1Y4cin/HIvAawmvCSoOeK15Ad5ztG6C58L2e9k\n" + "emIL/LbegV5GyD1w51rqHOosy8VX2VmSOnkKGUShjds2ENcbneXjuTXVnRCe/gDp\n" + "I9Wa94VL4PvgrFs5F1Yi7qR138LbMnXxeJPw341eBpwRpKPzKAcTFDgELwzK8kaq\n" + "bxkfqdnjAgMBAAECggEAWytxbRcpbbkfMArIawv7uotR2ErmMFBLmJQx+xfIo0ZK\n" + "anlRTMhA5l60YWYHe35FzvTh/QQqwy07R+y3zVqB99ODZ89Sr1gSGUBvvWY4sYp4\n" + "sLqBUg8BSsw3mOrwwf1HkYdE9p88qgrLePai/CAcg1SBnv4fXfbF/f9MJUYCwGVP\n" + "bXrWq9JQcL2e867UqVqlJMiFB0uLs4kYGJEz5CZQMwBU9bgpBtnPpBXntgsbyDIu\n" + "5y3kNiiPHs1VU9F99J9kacVfVAv6vBZH2Y3X9IOG8gQwOoAil4f6zpM9CFUj4LZs\n" + "tPPS1glYbjmhOdlljC5eJCfLJC+9Xpwyp5ZN5duoAQKBgQD8tBCefPEeY2yajtg9\n" + "0/L/+ODX+AjfyijdSs5G/0U0ZsHaKSedE0GNbEksLgXAgH1JsZxwBOVoxJizprTn\n" + "q8hu0umaoJ3Zf50l23uMJqZK4Tnd3R+oTBuHjVY052zSEbpbtbB1Ha3urnHMCcdS\n" + "5nYg0qLG0bYA+FwR1c8tG3RVsQKBgQC8BquqrCPsZxRge3+bBTEKj9W4/vPWKp+s\n" + "jWI1mXyQhFceZ9RLYTAOp0Mbi9tAvk9ovcetodtCnJqoD04NaoLRE4hIC+/UUqWI\n" + "OUEWCDO+02g+mMuTsRkuFj9HIWPUXd9P1j4iTycSSKFp1QM85t/ggYqDbIiOgfhL\n" + "s/sbYQQJ0wKBgCW80jqI2A08tcxDBsH88+4MAa/e55xb+UxKzpFFr9UKf2qP+M15\n" + "QbHX+PlzCgLcbVli/8Suxn+l1FQH0j5CphT+xEoGMGx5pUMxCrs8TlsiVVzvl7mv\n" + "W/EbR0NxSAv6/8SQVoC25PGe9XmOAEk+B2gRbKOaT77HWCCFuIG49t+RAoGBALQ3\n" + "dIyWh6wLtLUxScJsvG+SI1g4TcAlhHvf25TiM0lU/yduf0VstqIk4SZi61hn0Dbl\n" + "R6D9tOlorreMS9SCFTaOER51Cn8oY+5oaiDS5b3uZUkyLFW39hl9S1NDBqtC+kpM\n" + "X6uE0D8vDD8i4wKZi1Vk9D05Zr2ohzMQJAs+9p7vAoGBAPCocp7uY17s1rKvtZvM\n" + "N+aTXVRpxVya6ICunCpk1VhcAwT6EHxvXKKqa3c6xZLGMkLOpTqByzC2f7+Ur7i4\n" + "btGnK3i/LAhBPWDvfpafnUeGaODCbxr8+i/e6xwF3a1bwCd5SPxPzS14FRfcScmZ\n" + "A4Q1Y46cpHN/bzeTQFG6tMOB\n" + "-----END PRIVATE KEY-----\n"; + +// Intermediate CA certificate (signed by root CA) +// Subject: CN=Test Intermediate CA +// Issuer: CN=Test Root CA +// Valid: 2026-01-22 to 2031-01-21 (CA:TRUE) +// Commands: +// openssl req -new -newkey rsa:2048 -keyout intermediate_key.pem -out intermediate.csr +// -nodes -subj "/CN=Test Intermediate CA" +// openssl x509 -req -in intermediate.csr -CA root_ca_cert.pem -CAkey root_ca_key.pem +// -CAcreateserial -out intermediate_cert.pem -days 1825 +// -extfile <(echo "basicConstraints=CA:TRUE") +inline constexpr char const* intermediate_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDFDCCAfygAwIBAgIUTxjxnkuFSB8P+4VeoVw5wrVEv9swDQYJKoZIhvcNAQEL\n" + "BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI2MDEyMjE3MzIxM1oXDTMx\n" + "MDEyMTE3MzIxM1owHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwggEi\n" + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCqobUGWRLfletWGsTWGdySYCb\n" + "l2DJ06wVSW/TXvozFmIMKve4T5LKFDTAQtVrp/hK97HqAlTXWjhMTqq1SYHlN4dv\n" + "utguzY7Vf96nJWVoJzsq7jAVhukK3bpRo6ytMcj6TRK7DIELKsbCOtvsLTxl0iGk\n" + "26uE1zn2xk78GXJLRL5QHgeMrkgwWEdY8AeHm9VJ+dxBtnhzPR0z/AFaMmPODMSN\n" + "+HGkDwVyBxOiPrt9GouEci+rx7AUv3Iv8wLZ+AOiCC0Fbfe9zMqVxVppRB8mUt4c\n" + "+Np45GnIUk6/Fi+pdNJLTEE5WnoiA87GK+CbAezZt36vYIxSUIfoGz0jKrbpAgMB\n" + "AAGjUDBOMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFGxcuu5CLhAiH3moziBaSMvW\n" + "BzVkMB8GA1UdIwQYMBaAFIMzUb5b+JvY7MnzQFVN+CG75ojHMA0GCSqGSIb3DQEB\n" + "CwUAA4IBAQAEr9QYAOU47frtpTt/TYazaPRt0gzJMQeG+YlFf+Zgsk02L81kxx+U\n" + "4cxggby/TGJlJs8x5X7p6AIW1xHXh976uk1wQjR8A4xojdxauQ7pZXrawCesNfz+\n" + "BJD4rtWD1GL+mGAwL8RT9w5MW+i+6M2IHsxfNp/gVuzEIUeKSaN3hEw10nQ/GZla\n" + "xXlsA7IDcCDBLR35yV/i2kgUlELJMGJfuMJyLt3nbf4y1exZHoq4q4tP4TYU3338\n" + "UXsP85AFORr1q+hDwpXoThPn9aAMlQpzgx6UvGekQK3IheMoqVtsir4N9EL2yMyo\n" + "fDrPhvAUJTaYU/pWeMqNGpOBmvGyiXh9\n" + "-----END CERTIFICATE-----\n"; + +// Intermediate CA private key (RSA 2048-bit) +// Matches intermediate_cert_pem above +inline constexpr char const* intermediate_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCqobUGWRLflet\n" + "WGsTWGdySYCbl2DJ06wVSW/TXvozFmIMKve4T5LKFDTAQtVrp/hK97HqAlTXWjhM\n" + "Tqq1SYHlN4dvutguzY7Vf96nJWVoJzsq7jAVhukK3bpRo6ytMcj6TRK7DIELKsbC\n" + "OtvsLTxl0iGk26uE1zn2xk78GXJLRL5QHgeMrkgwWEdY8AeHm9VJ+dxBtnhzPR0z\n" + "/AFaMmPODMSN+HGkDwVyBxOiPrt9GouEci+rx7AUv3Iv8wLZ+AOiCC0Fbfe9zMqV\n" + "xVppRB8mUt4c+Np45GnIUk6/Fi+pdNJLTEE5WnoiA87GK+CbAezZt36vYIxSUIfo\n" + "Gz0jKrbpAgMBAAECggEACa+QuLM5lykrAxFMm74XwLjcrHN9ws0NtOTePPcBHa7D\n" + "tiNHdUCHMGCNAIUb7oaBUHdQ48L/E/kqFIQvzj8YEgx8+qnrTy+2As/FrAiIBbuC\n" + "jd210aD1G3kEQ2ei3UxhtQuzjFAr0UPawHNLkN/uL2Y1e3tKnS7nKyPkksO979FM\n" + "CbcZw5fsxrI1zup1sUY+Z6SFoHxZmsXcUze2Nh5kdtJ09DSiMR7FhnnK84Q42UXF\n" + "IsqzzMH5MzGzloX8TRJvEwQkuLZXmDSx+3rjCh2hGhTgx8XkL25Q6q1PNv0+OYcu\n" + "ivsARHxtNiZyjXnBn/F5AxEzOAIpuowYHiJmk3J2+QKBgQD5SEe3VhP32Z0zYBsL\n" + "4OK3jUPYTMa8fA9A1RKnAB/ygI81CnGA/p/Sluo47WmgEcxuaA32cox+i80rt4f9\n" + "/1agVjRRJclxHn5+KSsnJlznGONsY9+DlvHyaoJoyT1yrWFtReywLKe9vhfjUrjK\n" + "2xZq3/KClmJMd47Qq/NKec2gfwKBgQDH6XdtlnNYaO8qw0Tomy0m9wwjjLQVk8OW\n" + "neTG7dePvD9g1CFMYlYSE5+8nSpy+56hOkgdz5ngT9tspue8RoIyqkEdxlMlaPqM\n" + "67cjxdhdMqB0YtK7M07rkYqp4+k91SNWUPSKyXEVPMMbtITO9cHBuc2kl0Iq2T7N\n" + "vMEuvhj0lwKBgA9MZkpUGAmf60vZ3A8QkBlfrAg8Pf4XRwBdkzV4hn1lcmR47ZpT\n" + "Bg/wfxNbTp4qOXeVHzY+tWyWu9KxAsGNyA0y/Sb1wLUWgADSGfnfGth76IkgX/k9\n" + "bD/KVZKEtyawiUghgHMXanv0jJbA3uJkK64HbGSjQgkbVUJtKxMpAnuVAoGAFMnL\n" + "WIL/pZ7r1/eMT9/rFxUzlvLHu0KtYRk0NBeBhfneYVRNziKfrquJvdReGKzftwZX\n" + "f3oaF0BWofrNOD/gxCH+OXlpJge/ni3Y0oh9Ulu0YcXxAfR47XgqAjaoB30FerFa\n" + "bKA7+ShjZZslAFx/9IQ8xTPRdqE2rbBGKnUsJSsCgYEAwJHMAurHH16QSGPnEFTJ\n" + "3x63BYzRf+4S+IYtlZVJk/iZvk5Ru/ezW0cOK+Ty3y/w6vANlc2Eaf2nZ4UZH07o\n" + "MqPoJs1OF0fCZwjWq26fJ3MigLvp1Mo+EwHUExIvkB4QOs9bcDH9FHNBs+qiV6By\n" + "p91byQ0HYRzDCcHYULcZjkM=\n" + "-----END PRIVATE KEY-----\n"; + +// Server certificate signed by intermediate CA +// Subject: CN=www.example.com +// Issuer: CN=Test Intermediate CA +// Valid: 2026-01-22 to 2027-01-22 (end-entity certificate) +// Commands: +// openssl req -new -newkey rsa:2048 -keyout chain_server_key.pem -out server.csr +// -nodes -subj "/CN=www.example.com" +// openssl x509 -req -in server.csr -CA intermediate_cert.pem -CAkey intermediate_key.pem +// -CAcreateserial -out chain_server_cert.pem -days 365 +inline constexpr char const* chain_server_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDCTCCAfGgAwIBAgIUICKZdMPYLi+vx0rER9U9G0/zzecwDQYJKoZIhvcNAQEL\n" + "BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjYwMTIyMTcz\n" + "MzEyWhcNMjcwMTIyMTczMzEyWjAaMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20w\n" + "ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChfRaitIy/YbFh4Wa0KomP\n" + "EF8tU3QzyOQ8tD0bxQx8hG6POBEjVh7FUf++n6Sm72UbHGH7txQTNpmoihBp0M1N\n" + "Bkv85MtaevOkTEGtmY552rHPWezIpOMM6A9Vlu5H6tYs+2zorQJ9VfPt7mGbC56L\n" + "nOCMEujSwn2B8y0/jh1ZXSe8wGHokBrbigvsJIGNJ1T9HmLf+SaXN4hrLPar8u6S\n" + "bsDe78l9ZYxyUr8HTAzHuJksxkRbi7z1kQUVKXSg6YoKArHbVVYF8COKRApgTmjY\n" + "FxIkgpRyYPOnwTQWShzx+Frb0jx1wMagapR07B9Q2Ozk+X2UDPsOj//94J7xJq2f\n" + "AgMBAAGjQjBAMB0GA1UdDgQWBBQ0aZz4UflELiLyRCbpfJJbn/uFqTAfBgNVHSME\n" + "GDAWgBRsXLruQi4QIh95qM4gWkjL1gc1ZDANBgkqhkiG9w0BAQsFAAOCAQEAiUKb\n" + "rDKCzkxU+yT6xG+Dplwhw1218C34QSaMQfx/6qyGYTZfhklqUUeA2sjtBFzFeeWy\n" + "H7f5eM+i9IBPskd5AJMZpWDv2jA2TgJypvJuTdR3JC0M5bbOLeU57JxLxdizGzAd\n" + "GR56ERvzeOtHJwnEOsaz8AnSGY3gurAgPI6n9FpQtc25/bhLreknhx5Y0JYaBRPw\n" + "O98I4pZz0QmtWuaro4LN6vlJf58krvKPKhvuCwEWZvGN7PkC2XbKGf/Xko9/a0Bn\n" + "l2+4NI2lFdUrd3bperQVMXKm+U3cFHLXm6x+mqUcA5Epz5DUsQZhs18GcsdQh7NG\n" + "7T5qXswPM7MpHozuTg==\n" + "-----END CERTIFICATE-----\n"; + +// Server private key for chain_server_cert_pem (RSA 2048-bit) +// Matches chain_server_cert_pem above +inline constexpr char const* chain_server_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChfRaitIy/YbFh\n" + "4Wa0KomPEF8tU3QzyOQ8tD0bxQx8hG6POBEjVh7FUf++n6Sm72UbHGH7txQTNpmo\n" + "ihBp0M1NBkv85MtaevOkTEGtmY552rHPWezIpOMM6A9Vlu5H6tYs+2zorQJ9VfPt\n" + "7mGbC56LnOCMEujSwn2B8y0/jh1ZXSe8wGHokBrbigvsJIGNJ1T9HmLf+SaXN4hr\n" + "LPar8u6SbsDe78l9ZYxyUr8HTAzHuJksxkRbi7z1kQUVKXSg6YoKArHbVVYF8COK\n" + "RApgTmjYFxIkgpRyYPOnwTQWShzx+Frb0jx1wMagapR07B9Q2Ozk+X2UDPsOj//9\n" + "4J7xJq2fAgMBAAECggEALb1S5HnUJb7jcZBYuS4VMUbXVmy9TI+ZidIZPtzUmQ4f\n" + "jIQ6YnJZm9UKZXEtPzUuQ3wKCrRDxN9hrVmRpY8FH0xpyHL7YCDUEpSgw61rK/t0\n" + "AoF7bic5wiWWdk0eJ5ON30bFha+/NUXbpegvkC091lh0R2hxtoRs7Ro2FjrH+E/V\n" + "oLT23HGnUYSI2dNjxduFspAqPh3xNv7yjRrCc2KT83ku5GYhsiSg8WTbq7IBtUav\n" + "1QJ1tyqsLxFnFcDpl9N3Wh5r7Xbf8FL3w12m66efJ7yGMCLOJDxGDkRL4fnQyGQV\n" + "WPYe5K9vxyw/IZH6f2cq/3FEZmgo5nTz4rxInmQ3xQKBgQDRAV8MpzF6xygmcG+/\n" + "udIQdS0RDJrH1VE2mwyGvQqJbsNGIBOgDN/UIApRlhhA6gJygBd3Uj1cMAAmRI8d\n" + "KvJBEB8ivbzwBO3L2eE9918aPQ5p+bNbN5c7uohBpZqmN7eUgWodi/omQR+86Kfb\n" + "VAILXQhd4cO8dDNrCI3W+ahAQwKBgQDFzJclQZVMDdjuM4MyafF1ro2azhdHUe7n\n" + "a3JCi1PkqM1BjxuEfhFZKViqcnDrpOLamW0cMICfICOCtTapH1QaxjdaoLDS90DN\n" + "SEishTMJ2e7nHXr2TNeE/PXWNm9yualu7EUwhTgoEBM5fvFbywCfHiVFS72QBJrD\n" + "CgWNWgAFdQKBgQC/XCYOi7X92AKmzyNBw3zVnLNafNPqSyFEgcmCQ+s10bfwqMXP\n" + "MHpu2bcY4/fo11jORQE3OpD7quc4ImV2KzAK6hvXzykCCUE/94kHF0p315cu6HSS\n" + "+973zN2cXWeu8CyhR6xEyTiLdez9JXcqlUwZ42AZtO9lyG6bfQWA4qxtyQKBgDgL\n" + "8sABx1YXjmJggkpkrqCT51f4EayJ0NIOJgApDop6Mj7jV/7A4hWLm64gY1LCE+2x\n" + "D7OvIqL0Llu5EVX2pJQ5mjG52qDMorYIR19rFr0x3XnrZo4n0+HA87/RCN9PMG1X\n" + "0XsgJHtloqzmBWnnKbPsjM8H2RzX0Sp2yn/1ApCJAoGBAME9q4pqI+5blm/9r77R\n" + "OtmUGjIFCxQgViscMpAUq4vNJziofgYdXB/GjtYV75coruvP3MqMc4+Zgrp5tyU+\n" + "slMAs4tq3nqXViDFJBU/IEDk+8Fwn0zDPCWvlHjEDgZ3J7FioxbTjSqMn8ozoReL\n" + "ivz83oi40E6Mou2cdfF/o5S+\n" + "-----END PRIVATE KEY-----\n"; + +// Client certificate for mTLS (signed by intermediate CA) +// Subject: CN=Test Client +// Issuer: CN=Test Intermediate CA +// Valid: 2026-01-22 to 2027-01-22 (end-entity certificate) +// Commands: +// openssl req -new -newkey rsa:2048 -keyout client_key.pem -out client.csr +// -nodes -subj "/CN=Test Client" +// openssl x509 -req -in client.csr -CA intermediate_cert.pem -CAkey intermediate_key.pem +// -CAcreateserial -out client_cert.pem -days 365 +inline constexpr char const* client_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDBTCCAe2gAwIBAgIUICKZdMPYLi+vx0rER9U9G0/zzeYwDQYJKoZIhvcNAQEL\n" + "BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjYwMTIyMTcz\n" + "MzEyWhcNMjcwMTIyMTczMzEyWjAWMRQwEgYDVQQDDAtUZXN0IENsaWVudDCCASIw\n" + "DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANWTZ/JgpF91Xh3RukLVwnuu1Tld\n" + "fSYVmCgoFh/lYQeBjHmls2JXRaIsCG35Fn8h0kaas7B/Zz8Ym92k0zMvhfE7XzYi\n" + "EVQO9BMFnpTI5bUgQId4p7tZ4FyCQ58lnlVE6ytFkx9yWBS3YK89qsHqtVLFz4ry\n" + "ASNZCKVSPBiDi3rmH88BHPQid6agj/1vU3qti4YptNMXclMmUfgIZoGq3sjVvfMl\n" + "FKW8fDRl2GVlH9NgfnCeDoobOszw7Xckn3bibTh1tmNbQ/DXHXDQqwHqDu/nCCR0\n" + "BDHNxFeZj1WW0AVgN/qd/MSZetslyjrVnUrhf33FiMf3JUw+iExEIYKE02MCAwEA\n" + "AaNCMEAwHQYDVR0OBBYEFPv4jcET7PmxUHqXqV8uSmLBdW1yMB8GA1UdIwQYMBaA\n" + "FGxcuu5CLhAiH3moziBaSMvWBzVkMA0GCSqGSIb3DQEBCwUAA4IBAQA1yECFvGJ0\n" + "+KBBUzU++8v7xhl/tMKt7gqCd/2dvr4KW9iH6euYW/m3sl3iZ/h2O4kshSWTyVnc\n" + "aumFusDxsMFW6h0XdQ0MlX1BIQC9aERZhXTG7LeXPKvrUmDTNeNdCI2xokVSVGmh\n" + "FiQLllUhmjlKpwI5r5AyoUegpdNmXGmDqfpkrQ7aHijwZ7agyceCLlfJAujDVMBe\n" + "5AKW6CXiAlWbTuzPDzl1SZGTIzBNErHqEGg/MfxNVJfqxvhT5/pVQTaoLICvVgZG\n" + "Y7aGqGhK6eBv9NjOFHoUJvfBKTXfzklc0S8LgMZCvFTkoLMAvQiS4ebohgW9iQuo\n" + "8KVXaw2EqiiJ\n" + "-----END CERTIFICATE-----\n"; + +// Client private key for mTLS (RSA 2048-bit) +// Matches client_cert_pem above +inline constexpr char const* client_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVk2fyYKRfdV4d\n" + "0bpC1cJ7rtU5XX0mFZgoKBYf5WEHgYx5pbNiV0WiLAht+RZ/IdJGmrOwf2c/GJvd\n" + "pNMzL4XxO182IhFUDvQTBZ6UyOW1IECHeKe7WeBcgkOfJZ5VROsrRZMfclgUt2Cv\n" + "ParB6rVSxc+K8gEjWQilUjwYg4t65h/PARz0InemoI/9b1N6rYuGKbTTF3JTJlH4\n" + "CGaBqt7I1b3zJRSlvHw0ZdhlZR/TYH5wng6KGzrM8O13JJ924m04dbZjW0Pw1x1w\n" + "0KsB6g7v5wgkdAQxzcRXmY9VltAFYDf6nfzEmXrbJco61Z1K4X99xYjH9yVMPohM\n" + "RCGChNNjAgMBAAECggEAWl1pILtVMPKG5NUFGxw4kn5Rx1jQB9ohK/RyEALMgBGH\n" + "Lz013gkQ9GHvGyDGLPpRbwArwSTWuXKfGDOSDNkxsfSt/0iAznEZQichhtBNqMpB\n" + "o1Agn/uSG3IeTGrtSCTF3+QrMKX/sJw6M0tDQZMeLyx0+NQWOS+FofVeafzWeiO3\n" + "soY3iQLCsVInQALFMrPUHbNGln/8gH+SuqSYThVx0nF8k464v/3rueiNGX552pMX\n" + "0hkiLoXq92AlLrrqoSurJxgwQghAMtO+fyupfeE+HcNWBX0nTAl/DuFud5qJJD+O\n" + "A4p6Oz7lD+wThLxpAItfe+XWsDcYlIee+AcrgpKvcQKBgQD7fg6JvePqm3OtT9oW\n" + "wk+ozWeGnP3u5AVq2HgHtmmOCWhehqDJPoLkF9bkEymrMAQiuBIga4JdOZeG+tho\n" + "sobAhBbtBPV6HtE0Xt/i53x7T+v4kF7LNcL+/eZf2FX7ARW9xcNHhayHNTmgLWXC\n" + "sFizkmAAjrwYhcIZWMJA4xLS2wKBgQDZZ1+WpOh7VeSeopiwJ8fkPXooSUwf130f\n" + "DM9x+0F2yRcr4UrSOU6XQSlc8LKmRSDJ1Orol7RRTtFGg/pd91hIjkQJodMyed1/\n" + "gCKADy0p3rDhzCq+rwUHD9G7T5AhQiPr1eyXx3Wo1PyvlGc+IJeDL4cAsbaIWhkZ\n" + "dHYqgFl0GQKBgE1Yy6fZWwuAm+cls/Fj+ZP0+G4SQpcCUhg2U1Qr6fLhOdQ4m6LJ\n" + "MwBrxI+IxTv9HIiBDDIkXofFerDs3Tn2DjOPbG2hJM5WRAlTVJA4mbRjNDPSUxU0\n" + "h7Bc7kl0A52bC9C9zf1lQ1aiLALzc2SZT+6KijQhsf/ow3WAMt45+EQZAoGBAL1S\n" + "uHuHwK0nb6B2GGHPQtQQdYD/07sm/V882Kp6E9hN5k/gMjhAj6BIrqyxL+J78MHT\n" + "GX7UHcNwz+6IoE+URt1ohvecZT9fwPR3sZOzo7ECrSb1lYPZBpfPvuVPtERCROXr\n" + "tc23dU9Bq4t7wSzpVQh5Kyf/muXDEHiKYx1ACKaBAoGANfg390PT1HCo8/i0t1ZA\n" + "LnXWFQHU9Wg6UxK1KGN+sU6qsyaplE3E9N4M4CsfDUarx0W8KaiaQWp6jdveU7r9\n" + "n81WOIBRLb4/Ew9ZJXS3V+bf5DS2LIHc0C9NUWSeeI3inB0xgERy6vtbdaSBfnmq\n" + "J8I8kP82/dhlU/5NJGiPwqg=\n" + "-----END PRIVATE KEY-----\n"; + +// Wrong hostname certificate for hostname verification failure tests +// Subject: CN=wrong.example.com (instead of www.example.com) +// Valid: 2026-01-22 to 2027-01-22 (self-signed, CA:TRUE) +// Command: +// openssl req -x509 -newkey rsa:2048 -keyout wrong_host_key.pem -out wrong_host_cert.pem +// -days 365 -nodes -subj "/CN=wrong.example.com" +inline constexpr char const* wrong_host_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDGTCCAgGgAwIBAgIUAJXP7QDgWvI47I5I8IQcxzXmtP0wDQYJKoZIhvcNAQEL\n" + "BQAwHDEaMBgGA1UEAwwRd3JvbmcuZXhhbXBsZS5jb20wHhcNMjYwMTIyMTczMzI2\n" + "WhcNMjcwMTIyMTczMzI2WjAcMRowGAYDVQQDDBF3cm9uZy5leGFtcGxlLmNvbTCC\n" + "ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALjYkpWNUeZgZvdsaawTDd0P\n" + "W6DReKnBP10u73ZgY/8a6XJxVqo4jUK5mKH5SD/LS1rJB4nsgi2l8P5eNx2UpFED\n" + "/ybGNxo5nPhIYnwyvpsmNj8lZGdMUke+AwTh3QIM7lRebPxhSlMbnS/F9+1mCFG3\n" + "ijReW7UcwGewMx2s775dFww6tNmzVcvXeer5vgAlw/LkgI1HPhqwOCvnJQn1Q+Y4\n" + "VzMzb1FYEM3gPfNP4qPwJe8ut38CYVadEofKnRtTuutgjKAWlGe+EveBTbUuHfe3\n" + "laA672JDrdwzgeJ+LfrsMerzsyzQnrh8/eMiGjdLAduTw3H3lM6e2SVYfPYjIBUC\n" + "AwEAAaNTMFEwHQYDVR0OBBYEFLeFbAlvr2VFP3+vubuYVXZwm7YAMB8GA1UdIwQY\n" + "MBaAFLeFbAlvr2VFP3+vubuYVXZwm7YAMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI\n" + "hvcNAQELBQADggEBAJzvr3/8X18AmAM5CRUFwgoLLVxGLpmqeRcNxGcHUC7GrboY\n" + "/HhuV1kPrn2vrdilCl3Ya+OeF8xh1t5ky8lX+MRkESWxylBh1/1E9hTSz/sIKmD0\n" + "4dmJE65mc6YEez4CijGIKA4PqO1wHs8jnsxQCFDyRyAbTI2kBZv2i7OHtv8vo3EX\n" + "6bhW4kV+x8//4RjZ1dAwr7fbDlkOleOdCe48kFX91q0AAhEjjpgUNWXMN2CoICLe\n" + "QCphWvMv8vkKzRyyyH8FyBAc5ZnNb3gcBEZeuicivi7Jy/DZdA+KJKF607Fb7SPZ\n" + "bC60J6FqZXhJQSss8hllyLXgzIYX/gTK8+Gadn0=\n" + "-----END CERTIFICATE-----\n"; + +// Wrong hostname certificate private key (RSA 2048-bit) +// Matches wrong_host_cert_pem above +inline constexpr char const* wrong_host_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC42JKVjVHmYGb3\n" + "bGmsEw3dD1ug0XipwT9dLu92YGP/GulycVaqOI1CuZih+Ug/y0tayQeJ7IItpfD+\n" + "XjcdlKRRA/8mxjcaOZz4SGJ8Mr6bJjY/JWRnTFJHvgME4d0CDO5UXmz8YUpTG50v\n" + "xfftZghRt4o0Xlu1HMBnsDMdrO++XRcMOrTZs1XL13nq+b4AJcPy5ICNRz4asDgr\n" + "5yUJ9UPmOFczM29RWBDN4D3zT+Kj8CXvLrd/AmFWnRKHyp0bU7rrYIygFpRnvhL3\n" + "gU21Lh33t5WgOu9iQ63cM4Hifi367DHq87Ms0J64fP3jIho3SwHbk8Nx95TOntkl\n" + "WHz2IyAVAgMBAAECggEAHaU4Ty1w1Oyhnu1/fiY5KzrHFIH74ufYIHsCUz8u0m9v\n" + "wNe+EUNMHoczHEklZfvWDEug/qUUlLsgLT+RgdhAyTCFp6OTG0zhqK09RFOEH9Bv\n" + "U03NLkb+jDyEcfBCeI133MafHpQA7lbHrS2IL4YVb/uqee8nMKMZlZeb/xapBaPV\n" + "HqZ5+UTV6hT/yFqXD2g0nhHFkZb36JlU+R+WZJjcIzbJxuakteKZlTtjfwLDaida\n" + "/kHbHlMVtiZkmVm5CZP8ICVykEfUvyzA/9/t9LE63GzUcBiEE23S+ZrBIrzncHe4\n" + "h7I2dpi3sIZ1STguGjhdFZeTbrLLT0KsQJr1R4eMAQKBgQDeoKkkuLtjPhFHquXT\n" + "GXwMRMBRVILtBsc4hZ8Qy+D1GiFTWxdxrLe/oU1z1zEWZhJjz20VmmqCmqGMbTHn\n" + "Q5N+ZO3/k/2Y0KmnJ8iM3bKozROsMTv+xxB19+Xk3aXtFSqDzeu4UgyOYLO9AXRY\n" + "1jkhv3ehjzfqZPQJ4aV4OQaUAQKBgQDUjgrN4jIQA7CbpHIqT95OwQBuu8I6bM9R\n" + "zbAodBhFqXOMwcVOFY66CwSAjp+xEYaPZ+7aNKu+YkCdpOaBlGGzHhYUB2GU0E9w\n" + "Byhf/LAI8WMGUMb6TSEZi8NysDgCzhEB+rIJKxmL5rKTlYx6K7NWErqarDdVKlX+\n" + "3hrvruj8FQKBgF/mZ1ZJOXdjuj/cD0pjNPt39jxSol+GRvVDIiUzHgGXMvncSHoQ\n" + "Q8sJqfqXnS6f45YZOU1QCkeeYq7CLvgHNRcCVT9+OYTFhf9adNqxeY+bX7kSMFzs\n" + "1Vtr4R04mYxKTNkgMEVjGsOORn7JjJvkFBJEjz0KG7Udrb4/9G6YagwBAoGAGxJY\n" + "R+6mR6ngpYIlVERF4SvtvSzGySAwq4+R/yUCLmUtpWDMm2xdeE6M7T69EhVUWRF4\n" + "t2v778ydxDZLcXePlfuf/j8Oa6C4bWFMACWz2f+8iAJjxV9rdtB5PTM6fwj125Wt\n" + "dUN7BnmEhw2GDc1hEvZhs+95QKyatVJeheZ2IB0CgYAfyONzjt3rN+MwzoqbK6zq\n" + "MJzeQIAZy5qrP4j8WX6kMqc8o74K1XcQ6D2rCXnsl6zI4nZMVC4/OSr7qSi3pqiS\n" + "KdcRSeK9FiCNJKrHVIFF6ESIZQbu3nRmPbe5ia9UAYPFZjR5cfL86HLURlNDP8Ig\n" + "pkVV7X2vCKbi7v7voSZAwQ==\n" + "-----END PRIVATE KEY-----\n"; + +// Untrusted CA certificate for testing verification failures +// Subject: CN=Untrusted CA +// Valid: 2026-01-22 to 2036-01-20 (self-signed, CA:TRUE) +// Command: +// openssl req -x509 -newkey rsa:2048 -keyout untrusted_ca_key.pem -out untrusted_ca_cert.pem +// -days 3650 -nodes -subj "/CN=Untrusted CA" +inline constexpr char const* untrusted_ca_cert_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDDzCCAfegAwIBAgIUc+DM0BNA1pDpUpezmCQiUQoe+3YwDQYJKoZIhvcNAQEL\n" + "BQAwFzEVMBMGA1UEAwwMVW50cnVzdGVkIENBMB4XDTI2MDEyMjE3MzMzOVoXDTM2\n" + "MDEyMDE3MzMzOVowFzEVMBMGA1UEAwwMVW50cnVzdGVkIENBMIIBIjANBgkqhkiG\n" + "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7EyTGB9e6wmVFVwEHJzOni999nFV1sGirC5k\n" + "cSFUu2Ab853h8wn7tBhzfdiWEIKTpW4evQX0RDEsIUQXLQumjP8G2GOprsi75yVA\n" + "VHTNZrF6c7zjEahGqW1JX3KlVc88uSZGPOG66JXM3BYlCjY3tBlBHPbySYSzXdNG\n" + "SpFI5TN/gISgLAnjwMwPG7Jo+DEOGhezHjDmZadL8uUvXOYSbONqyIaMJ67Sh0HM\n" + "52x/nxkzk6TO/PjfAroXLtki+xD301j5voUTwL3v539hr1dJimqASdUOFmP2NKYB\n" + "ZICIjoBIx49wSz1ZDtV5FYmZ9O9yOg+98ISK/Tv9PI7oZryEywIDAQABo1MwUTAd\n" + "BgNVHQ4EFgQUextp3IEu2z5jYyXw3DrYQmThtHowHwYDVR0jBBgwFoAUextp3IEu\n" + "2z5jYyXw3DrYQmThtHowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" + "AQEARJ1sFG8ceBq2iCCb6ninM+pC/nsxfxktqDxPZgc6Dybb6mTSb3sKwKRH0pTM\n" + "0z61JbWEVdNpT1tShjnJ5e/YWn90e/8lQBS8LVH/QsfKjGZk5GxUS9186BvAuKQR\n" + "R668C4CFsxgv0do1Hur8857KvH/z3sruR/ZEgeWTeVqSIxYZaC6HboSoHafq0J/L\n" + "SCfyoTn+iBxPMdnhwCvpONL8sEkvGW8cYW2URZqFlO/775K+sPbfeYXxuUq/ocEf\n" + "XmvTRzAeijN/sDeGKVZhi/yGtMv0Q/t0ZwXFU0Mj+fGCmti8QzEFa9RCf+Tx3CiZ\n" + "zzXbHDJUEOFjKq67XelGy9zeNQ==\n" + "-----END CERTIFICATE-----\n"; + +// Full certificate chain: server cert + intermediate cert (for chain tests) +// Contains: chain_server_cert_pem followed by intermediate_cert_pem +// Command: +// cat chain_server_cert.pem intermediate_cert.pem > server_fullchain.pem +inline constexpr char const* server_fullchain_pem = + "-----BEGIN CERTIFICATE-----\n" + "MIIDCTCCAfGgAwIBAgIUICKZdMPYLi+vx0rER9U9G0/zzecwDQYJKoZIhvcNAQEL\n" + "BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjYwMTIyMTcz\n" + "MzEyWhcNMjcwMTIyMTczMzEyWjAaMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20w\n" + "ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChfRaitIy/YbFh4Wa0KomP\n" + "EF8tU3QzyOQ8tD0bxQx8hG6POBEjVh7FUf++n6Sm72UbHGH7txQTNpmoihBp0M1N\n" + "Bkv85MtaevOkTEGtmY552rHPWezIpOMM6A9Vlu5H6tYs+2zorQJ9VfPt7mGbC56L\n" + "nOCMEujSwn2B8y0/jh1ZXSe8wGHokBrbigvsJIGNJ1T9HmLf+SaXN4hrLPar8u6S\n" + "bsDe78l9ZYxyUr8HTAzHuJksxkRbi7z1kQUVKXSg6YoKArHbVVYF8COKRApgTmjY\n" + "FxIkgpRyYPOnwTQWShzx+Frb0jx1wMagapR07B9Q2Ozk+X2UDPsOj//94J7xJq2f\n" + "AgMBAAGjQjBAMB0GA1UdDgQWBBQ0aZz4UflELiLyRCbpfJJbn/uFqTAfBgNVHSME\n" + "GDAWgBRsXLruQi4QIh95qM4gWkjL1gc1ZDANBgkqhkiG9w0BAQsFAAOCAQEAiUKb\n" + "rDKCzkxU+yT6xG+Dplwhw1218C34QSaMQfx/6qyGYTZfhklqUUeA2sjtBFzFeeWy\n" + "H7f5eM+i9IBPskd5AJMZpWDv2jA2TgJypvJuTdR3JC0M5bbOLeU57JxLxdizGzAd\n" + "GR56ERvzeOtHJwnEOsaz8AnSGY3gurAgPI6n9FpQtc25/bhLreknhx5Y0JYaBRPw\n" + "O98I4pZz0QmtWuaro4LN6vlJf58krvKPKhvuCwEWZvGN7PkC2XbKGf/Xko9/a0Bn\n" + "l2+4NI2lFdUrd3bperQVMXKm+U3cFHLXm6x+mqUcA5Epz5DUsQZhs18GcsdQh7NG\n" + "7T5qXswPM7MpHozuTg==\n" + "-----END CERTIFICATE-----\n" + "-----BEGIN CERTIFICATE-----\n" + "MIIDFDCCAfygAwIBAgIUTxjxnkuFSB8P+4VeoVw5wrVEv9swDQYJKoZIhvcNAQEL\n" + "BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI2MDEyMjE3MzIxM1oXDTMx\n" + "MDEyMTE3MzIxM1owHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwggEi\n" + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCqobUGWRLfletWGsTWGdySYCb\n" + "l2DJ06wVSW/TXvozFmIMKve4T5LKFDTAQtVrp/hK97HqAlTXWjhMTqq1SYHlN4dv\n" + "utguzY7Vf96nJWVoJzsq7jAVhukK3bpRo6ytMcj6TRK7DIELKsbCOtvsLTxl0iGk\n" + "26uE1zn2xk78GXJLRL5QHgeMrkgwWEdY8AeHm9VJ+dxBtnhzPR0z/AFaMmPODMSN\n" + "+HGkDwVyBxOiPrt9GouEci+rx7AUv3Iv8wLZ+AOiCC0Fbfe9zMqVxVppRB8mUt4c\n" + "+Np45GnIUk6/Fi+pdNJLTEE5WnoiA87GK+CbAezZt36vYIxSUIfoGz0jKrbpAgMB\n" + "AAGjUDBOMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFGxcuu5CLhAiH3moziBaSMvW\n" + "BzVkMB8GA1UdIwQYMBaAFIMzUb5b+JvY7MnzQFVN+CG75ojHMA0GCSqGSIb3DQEB\n" + "CwUAA4IBAQAEr9QYAOU47frtpTt/TYazaPRt0gzJMQeG+YlFf+Zgsk02L81kxx+U\n" + "4cxggby/TGJlJs8x5X7p6AIW1xHXh976uk1wQjR8A4xojdxauQ7pZXrawCesNfz+\n" + "BJD4rtWD1GL+mGAwL8RT9w5MW+i+6M2IHsxfNp/gVuzEIUeKSaN3hEw10nQ/GZla\n" + "xXlsA7IDcCDBLR35yV/i2kgUlELJMGJfuMJyLt3nbf4y1exZHoq4q4tP4TYU3338\n" + "UXsP85AFORr1q+hDwpXoThPn9aAMlQpzgx6UvGekQK3IheMoqVtsir4N9EL2yMyo\n" + "fDrPhvAUJTaYU/pWeMqNGpOBmvGyiXh9\n" + "-----END CERTIFICATE-----\n"; + //------------------------------------------------------------------------------ // // Context Helpers @@ -394,51 +845,61 @@ run_tls_test_fail( bool client_done = false; bool server_done = false; + // Timer to unblock stuck handshakes (failsafe only) + timer timeout( ioc ); + timeout.expires_after( std::chrono::milliseconds( 200 ) ); + // Store lambdas in named variables before invoking - anonymous lambda + immediate // invocation pattern [...](){}() can cause capture corruption with run_async - auto client_task = [&client, &client_failed, &client_done]() -> capy::task<> + auto client_task = [&client, &client_failed, &client_done, &server_done, &timeout, &s1, &s2]() -> capy::task<> { auto [ec] = co_await client.handshake( tls_stream::client ); if( ec ) + { client_failed = true; + // Cancel then close sockets to unblock server immediately (IOCP needs cancel) + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } client_done = true; + if( server_done ) + timeout.cancel(); }; - auto server_task = [&server, &server_failed, &server_done]() -> capy::task<> + auto server_task = [&server, &server_failed, &server_done, &client_done, &timeout, &s1, &s2]() -> capy::task<> { auto [ec] = co_await server.handshake( tls_stream::server ); if( ec ) + { server_failed = true; + // Cancel then close sockets to unblock client immediately (IOCP needs cancel) + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } server_done = true; + if( client_done ) + timeout.cancel(); }; - capy::run_async( ioc.get_executor() )( client_task() ); - capy::run_async( ioc.get_executor() )( server_task() ); - - // Timer to unblock stuck handshakes - when one side fails, the other - // may block waiting for data. Timer cancels socket operations to unblock them. - timer timeout( ioc ); - timeout.expires_after( std::chrono::milliseconds( 500 ) ); - auto timeout_task = [&timeout, &s1, &s2, &client_done, &server_done]() -> capy::task<> + bool failsafe_hit = false; + auto timeout_task = [&timeout, &failsafe_hit, &s1, &s2]() -> capy::task<> { - (void)client_done; - (void)server_done; auto [ec] = co_await timeout.wait(); if( !ec ) { + failsafe_hit = true; // Timer expired - cancel pending operations then close sockets - s1.cancel(); - s2.cancel(); - s1.close(); - s2.close(); + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } } }; + + capy::run_async( ioc.get_executor() )( client_task() ); + capy::run_async( ioc.get_executor() )( server_task() ); capy::run_async( ioc.get_executor() )( timeout_task() ); ioc.run(); - - // Cancel timer if handshakes completed before timeout - timeout.cancel(); + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit // At least one side should have failed BOOST_TEST( client_failed || server_failed ); @@ -506,53 +967,53 @@ run_tls_shutdown_test( ioc.run(); ioc.restart(); - // Shutdown phase with timeout protection - bool shutdown_done = false; - bool read_done = false; + // Shutdown phase: client sends close_notify, server reads EOF then closes socket. + // Server closing the socket causes client's shutdown to complete. + bool done = false; + + // Failsafe timer in case of bugs + timer failsafe( ioc ); + failsafe.expires_after( std::chrono::milliseconds( 200 ) ); - auto client_shutdown = [&client, &shutdown_done]() -> capy::task<> + auto client_shutdown = [&client, &done, &failsafe]() -> capy::task<> { auto [ec] = co_await client.shutdown(); - shutdown_done = true; - // Shutdown may return success, canceled, or stream_truncated + done = true; + failsafe.cancel(); BOOST_TEST( !ec || ec == capy::cond::stream_truncated || - ec == capy::cond::canceled ); + ec == capy::cond::eof || ec == capy::cond::canceled ); }; - auto server_read_eof = [&server, &read_done]() -> capy::task<> + auto server_read_then_close = [&server, &s2]() -> capy::task<> { char buf[32]; auto [ec, n] = co_await server.read_some( capy::mutable_buffer( buf, sizeof( buf ) ) ); - read_done = true; - // Should get EOF, stream_truncated, or canceled BOOST_TEST( ec == capy::cond::eof || ec == capy::cond::stream_truncated || ec == capy::cond::canceled ); + // Close socket to unblock client's shutdown + s2.cancel(); + s2.close(); }; - // Timeout to prevent deadlock - timer timeout( ioc ); - timeout.expires_after( std::chrono::milliseconds( 500 ) ); - auto timeout_task = [&timeout, &s1, &s2, &shutdown_done, &read_done]() -> capy::task<> + bool failsafe_hit = false; + auto failsafe_task = [&failsafe, &failsafe_hit, &done, &s1, &s2]() -> capy::task<> { - (void)shutdown_done; - (void)read_done; - auto [ec] = co_await timeout.wait(); - if( !ec ) + auto [ec] = co_await failsafe.wait(); + if( !ec && !done ) { - // Timer expired - cancel pending operations (check if still open) + failsafe_hit = true; if( s1.is_open() ) { s1.cancel(); s1.close(); } if( s2.is_open() ) { s2.cancel(); s2.close(); } } }; capy::run_async( ioc.get_executor() )( client_shutdown() ); - capy::run_async( ioc.get_executor() )( server_read_eof() ); - capy::run_async( ioc.get_executor() )( timeout_task() ); + capy::run_async( ioc.get_executor() )( server_read_then_close() ); + capy::run_async( ioc.get_executor() )( failsafe_task() ); ioc.run(); - - timeout.cancel(); + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit if( s1.is_open() ) s1.close(); if( s2.is_open() ) s2.close(); } @@ -614,34 +1075,38 @@ run_tls_truncation_test( // Truncation test with timeout protection bool read_done = false; + // Timeout to prevent deadlock + timer timeout( ioc ); + timeout.expires_after( std::chrono::milliseconds( 200 ) ); + auto client_close = [&s1]() -> capy::task<> { - // Close underlying socket without TLS shutdown + // Cancel and close underlying socket without TLS shutdown (IOCP needs cancel) + s1.cancel(); s1.close(); co_return; }; - auto server_read_truncated = [&server, &read_done]() -> capy::task<> + auto server_read_truncated = [&server, &read_done, &timeout]() -> capy::task<> { char buf[32]; auto [ec, n] = co_await server.read_some( capy::mutable_buffer( buf, sizeof( buf ) ) ); read_done = true; + timeout.cancel(); // Should get stream_truncated, eof, or canceled BOOST_TEST( ec == capy::cond::stream_truncated || ec == capy::cond::eof || ec == capy::cond::canceled ); }; - // Timeout to prevent deadlock - timer timeout( ioc ); - timeout.expires_after( std::chrono::milliseconds( 500 ) ); - auto timeout_task = [&timeout, &s1, &s2, &read_done]() -> capy::task<> + bool failsafe_hit = false; + auto timeout_task = [&timeout, &failsafe_hit, &s1, &s2]() -> capy::task<> { - (void)read_done; auto [ec] = co_await timeout.wait(); if( !ec ) { + failsafe_hit = true; // Timer expired - cancel pending operations (check if still open) if( s1.is_open() ) { s1.cancel(); s1.close(); } if( s2.is_open() ) { s2.cancel(); s2.close(); } @@ -653,8 +1118,590 @@ run_tls_truncation_test( capy::run_async( ioc.get_executor() )( timeout_task() ); ioc.run(); + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit + if( s1.is_open() ) s1.close(); + if( s2.is_open() ) s2.close(); +} + +//------------------------------------------------------------------------------ +// +// Additional Context Helpers for Extended Tests +// +//------------------------------------------------------------------------------ + +/** Create a server context using chain certificates (signed by intermediate CA). */ +inline context +make_chain_server_context() +{ + context ctx; + ctx.use_certificate( chain_server_cert_pem, file_format::pem ); + ctx.use_private_key( chain_server_key_pem, file_format::pem ); + ctx.set_verify_mode( verify_mode::none ); + return ctx; +} + +/** Create a server context with full certificate chain. + Server sends entity cert + intermediate cert, allowing client to verify + chain up to root CA. Uses use_certificate_chain() which expects the full + chain (entity + intermediates) in a single PEM blob. */ +inline context +make_fullchain_server_context() +{ + context ctx; + // use_certificate_chain expects entity cert followed by intermediate(s) + ctx.use_certificate_chain( server_fullchain_pem ); + ctx.use_private_key( chain_server_key_pem, file_format::pem ); + ctx.set_verify_mode( verify_mode::none ); + return ctx; +} + +/** Create a client context that trusts ONLY the root CA (for chain tests). + Server must send intermediate cert in chain for verification to succeed. */ +inline context +make_rootonly_client_context() +{ + context ctx; + ctx.add_certificate_authority( root_ca_cert_pem ); + ctx.set_verify_mode( verify_mode::peer ); + return ctx; +} + +/** Create a client context that trusts the root CA (for chain tests). */ +inline context +make_chain_client_context() +{ + context ctx; + // Trust both root and intermediate CA for chain verification + ctx.add_certificate_authority( root_ca_cert_pem ); + ctx.add_certificate_authority( intermediate_cert_pem ); + ctx.set_verify_mode( verify_mode::peer ); + return ctx; +} + +/** Create a server context with an EXPIRED certificate. + The certificate expired on Jan 2, 2020. */ +inline context +make_expired_server_context() +{ + context ctx; + ctx.use_certificate( expired_cert_pem, file_format::pem ); + ctx.use_private_key( expired_key_pem, file_format::pem ); + return ctx; +} + +/** Create a client context that trusts the expired cert's self-signed CA. + Used with make_expired_server_context() to test expiry validation. */ +inline context +make_expired_client_context() +{ + context ctx; + // Trust the expired cert as its own CA (self-signed) + ctx.add_certificate_authority( expired_cert_pem ); + ctx.set_verify_mode( verify_mode::peer ); + return ctx; +} + +/** Create a server context with wrong hostname (CN=wrong.example.com). */ +inline context +make_wrong_host_server_context() +{ + context ctx; + ctx.use_certificate( wrong_host_cert_pem, file_format::pem ); + ctx.use_private_key( wrong_host_key_pem, file_format::pem ); + ctx.set_verify_mode( verify_mode::none ); + return ctx; +} + +/** Create a client context for mTLS (with client certificate). */ +inline context +make_mtls_client_context() +{ + context ctx; + ctx.use_certificate( client_cert_pem, file_format::pem ); + ctx.use_private_key( client_key_pem, file_format::pem ); + // Trust both root and intermediate CA for chain verification + ctx.add_certificate_authority( root_ca_cert_pem ); + ctx.add_certificate_authority( intermediate_cert_pem ); + ctx.set_verify_mode( verify_mode::peer ); + return ctx; +} + +/** Create a server context that requires client certificates (mTLS). */ +inline context +make_mtls_server_context() +{ + context ctx; + ctx.use_certificate( chain_server_cert_pem, file_format::pem ); + ctx.use_private_key( chain_server_key_pem, file_format::pem ); + // Trust both root and intermediate CA for chain verification + ctx.add_certificate_authority( root_ca_cert_pem ); + ctx.add_certificate_authority( intermediate_cert_pem ); + ctx.set_verify_mode( verify_mode::require_peer ); + return ctx; +} + +/** Create a client context that trusts the untrusted CA (for verification failures). */ +inline context +make_untrusted_ca_client_context() +{ + context ctx; + ctx.add_certificate_authority( untrusted_ca_cert_pem ); + ctx.set_verify_mode( verify_mode::peer ); + return ctx; +} + +/** Create an mTLS client context with INVALID client certificate. + Uses server_cert_pem (self-signed) which is NOT signed by the + intermediate/root CA that make_mtls_server_context() trusts. */ +inline context +make_invalid_mtls_client_context() +{ + context ctx; + // Use the self-signed server cert as client cert - server won't trust it + ctx.use_certificate( server_cert_pem, file_format::pem ); + ctx.use_private_key( server_key_pem, file_format::pem ); + // Trust the chain CAs so we can verify server + ctx.add_certificate_authority( root_ca_cert_pem ); + ctx.add_certificate_authority( intermediate_cert_pem ); + ctx.set_verify_mode( verify_mode::peer ); + return ctx; +} + +//------------------------------------------------------------------------------ +// +// Connection Reset Test +// +//------------------------------------------------------------------------------ + +/** Run a test for connection reset during handshake. + + Tests that when the underlying socket is closed abruptly during + the TLS handshake, the operation fails with an appropriate error. + + @param ioc The io_context to use + @param client_ctx TLS context for the client + @param server_ctx TLS context for the server + @param make_client Factory: (io_stream&, context) -> TLS stream + @param make_server Factory: (io_stream&, context) -> TLS stream +*/ +template +void +run_connection_reset_test( + io_context& ioc, + context client_ctx, + context server_ctx, + ClientStreamFactory make_client, + ServerStreamFactory make_server ) +{ + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client = make_client( s1, client_ctx ); + auto server = make_server( s2, server_ctx ); + + bool client_failed = false; + + // Timeout protection + timer timeout( ioc ); + timeout.expires_after( std::chrono::milliseconds( 200 ) ); + + auto client_task = [&client, &client_failed, &timeout]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + // Should fail because server closed socket + if( ec ) + client_failed = true; + timeout.cancel(); + }; + + // Server closes socket immediately (simulates connection reset) + auto server_task = [&s2]() -> capy::task<> + { + // Cancel and close socket to simulate connection reset (IOCP needs cancel) + s2.cancel(); + s2.close(); + co_return; + }; + + bool failsafe_hit = false; + auto timeout_task = [&timeout, &failsafe_hit, &s1]() -> capy::task<> + { + auto [ec] = co_await timeout.wait(); + if( !ec && s1.is_open() ) + { + failsafe_hit = true; + s1.cancel(); + s1.close(); + } + }; + + capy::run_async( ioc.get_executor() )( client_task() ); + capy::run_async( ioc.get_executor() )( server_task() ); + capy::run_async( ioc.get_executor() )( timeout_task() ); + + ioc.run(); + + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit + BOOST_TEST( client_failed ); + + if( s1.is_open() ) s1.close(); + if( s2.is_open() ) s2.close(); +} + +//------------------------------------------------------------------------------ +// +// Stop Token Cancellation Test +// +//------------------------------------------------------------------------------ + +/** Run a test for stop token cancellation during handshake. + + Tests that cooperative cancellation via std::stop_token correctly + interrupts a TLS handshake when stop is requested. + + The test is deterministic: the server waits for client to send data + (ClientHello), proving the client has started, then triggers cancellation. + + @param ioc The io_context to use + @param client_ctx TLS context for the client + @param server_ctx TLS context for the server + @param make_client Factory: (io_stream&, context) -> TLS stream + @param make_server Factory: (io_stream&, context) -> TLS stream +*/ +template +void +run_stop_token_handshake_test( + io_context& ioc, + context client_ctx, + context server_ctx, + ClientStreamFactory make_client, + ServerStreamFactory make_server ) +{ + + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client = make_client( s1, client_ctx ); + auto server = make_server( s2, server_ctx ); + + std::stop_source stop_src; + bool client_got_error = false; + + // Failsafe timeout to prevent infinite hang if cancellation doesn't work + // 2000ms allows headroom for CI with coverage instrumentation + timer failsafe( ioc ); + failsafe.expires_after( std::chrono::milliseconds( 2000 ) ); + + // Client handshake - will be cancelled while waiting for ServerHello + auto client_task = [&client, &client_got_error, &failsafe]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + if( ec ) + client_got_error = true; + failsafe.cancel(); + }; + + // Server waits for ClientHello then cancels - deterministic synchronization + auto server_task = [&s2, &stop_src]() -> capy::task<> + { + // Wait for client to send ClientHello (proves client started handshake) + char buf[1]; + (void)co_await s2.read_some( capy::mutable_buffer( buf, 1 ) ); + // Client is now blocked waiting for ServerHello - cancel it + stop_src.request_stop(); + }; + + bool failsafe_hit = false; + auto failsafe_task = [&failsafe, &failsafe_hit, &s1, &s2]() -> capy::task<> + { + auto [ec] = co_await failsafe.wait(); + if( !ec ) + { + failsafe_hit = true; + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } + }; + capy::run_async( ioc.get_executor(), stop_src.get_token() )( client_task() ); + capy::run_async( ioc.get_executor() )( server_task() ); + capy::run_async( ioc.get_executor() )( failsafe_task() ); + ioc.run(); + + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit + BOOST_TEST( client_got_error ); + + if( s1.is_open() ) s1.close(); + if( s2.is_open() ) s2.close(); +} + +/** Run a test for stop token cancellation during read. + + Tests that cooperative cancellation via std::stop_token correctly + interrupts a TLS read operation when stop is requested. + + The test is deterministic: after handshake, the server triggers + cancellation immediately since the client will be blocked waiting + for data the server never sends. +*/ +template +void +run_stop_token_read_test( + io_context& ioc, + context client_ctx, + context server_ctx, + ClientStreamFactory make_client, + ServerStreamFactory make_server ) +{ + + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client = make_client( s1, client_ctx ); + auto server = make_server( s2, server_ctx ); + + // Handshake phase + auto client_hs = [&client]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + BOOST_TEST( !ec ); + }; + + auto server_hs = [&server]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + BOOST_TEST( !ec ); + }; + + capy::run_async( ioc.get_executor() )( client_hs() ); + capy::run_async( ioc.get_executor() )( server_hs() ); + + ioc.run(); + ioc.restart(); + + // Read cancellation phase + std::stop_source stop_src; + bool read_got_error = false; + + // Failsafe timeout - 2000ms allows headroom for CI with coverage instrumentation + timer failsafe( ioc ); + failsafe.expires_after( std::chrono::milliseconds( 2000 ) ); + + auto client_read = [&client, &read_got_error, &failsafe]() -> capy::task<> + { + char buf[32]; + auto [ec, n] = co_await client.read_some( + capy::mutable_buffer( buf, sizeof( buf ) ) ); + if( ec ) + read_got_error = true; + failsafe.cancel(); + }; + + // Server triggers cancellation immediately - client will block on read + // since server never sends data. This is deterministic because the + // client read is queued first and will suspend waiting for socket data. + auto server_cancel = [&stop_src]() -> capy::task<> + { + stop_src.request_stop(); + co_return; + }; + + bool failsafe_hit = false; + auto failsafe_task = [&failsafe, &failsafe_hit, &s1, &s2]() -> capy::task<> + { + auto [ec] = co_await failsafe.wait(); + if( !ec ) + { + failsafe_hit = true; + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } + }; + capy::run_async( ioc.get_executor(), stop_src.get_token() )( client_read() ); + capy::run_async( ioc.get_executor() )( server_cancel() ); + capy::run_async( ioc.get_executor() )( failsafe_task() ); + ioc.run(); + + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit + BOOST_TEST( read_got_error ); + + if( s1.is_open() ) s1.close(); + if( s2.is_open() ) s2.close(); +} + +/** Run a test for stop token cancellation during write. + + Tests that cooperative cancellation via std::stop_token correctly + interrupts a TLS write operation when stop is requested. + + The test is deterministic: after handshake, the server waits for + some data to arrive (proving the client started writing), then + triggers cancellation. +*/ +template +void +run_stop_token_write_test( + io_context& ioc, + context client_ctx, + context server_ctx, + ClientStreamFactory make_client, + ServerStreamFactory make_server ) +{ + + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client = make_client( s1, client_ctx ); + auto server = make_server( s2, server_ctx ); + + // Handshake phase + auto client_hs = [&client]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + BOOST_TEST( !ec ); + }; + + auto server_hs = [&server]() -> capy::task<> + { + auto [ec] = co_await server.handshake( tls_stream::server ); + BOOST_TEST( !ec ); + }; + + capy::run_async( ioc.get_executor() )( client_hs() ); + capy::run_async( ioc.get_executor() )( server_hs() ); + + ioc.run(); + ioc.restart(); + + // Write cancellation phase - fill socket buffer to cause blocking + std::stop_source stop_src; + bool write_got_error = false; + + // Large buffer to fill socket buffer and cause blocking + std::vector large_buf( 1024 * 1024, 'X' ); + + // Failsafe timeout - 2000ms allows headroom for CI with coverage instrumentation + timer failsafe( ioc ); + failsafe.expires_after( std::chrono::milliseconds( 2000 ) ); + + auto client_write = [&client, &large_buf, &write_got_error, &failsafe]() -> capy::task<> + { + // Write in loop until cancelled or error + for( int i = 0; i < 100; ++i ) + { + auto [ec, n] = co_await client.write_some( + capy::const_buffer( large_buf.data(), large_buf.size() ) ); + if( ec ) + { + write_got_error = true; + failsafe.cancel(); + co_return; + } + } + failsafe.cancel(); + }; + + // Server waits for data then cancels - deterministic synchronization + auto server_cancel = [&s2, &stop_src]() -> capy::task<> + { + // Wait for client to send some data (proves client started writing) + char buf[1]; + (void)co_await s2.read_some( capy::mutable_buffer( buf, 1 ) ); + // Client is now writing - cancel it + stop_src.request_stop(); + }; + + bool failsafe_hit = false; + auto failsafe_task = [&failsafe, &failsafe_hit, &s1, &s2]() -> capy::task<> + { + auto [ec] = co_await failsafe.wait(); + if( !ec ) + { + failsafe_hit = true; + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } + }; + capy::run_async( ioc.get_executor(), stop_src.get_token() )( client_write() ); + capy::run_async( ioc.get_executor() )( server_cancel() ); + capy::run_async( ioc.get_executor() )( failsafe_task() ); + ioc.run(); + + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit + BOOST_TEST( write_got_error ); + + if( s1.is_open() ) s1.close(); + if( s2.is_open() ) s2.close(); +} + +//------------------------------------------------------------------------------ +// +// Socket Error Propagation Test +// +//------------------------------------------------------------------------------ + +/** Run a test for socket.cancel() error propagation. + + Tests that calling socket.cancel() while TLS is blocked on socket I/O + correctly propagates the error through the TLS layer. + + The test is deterministic: the server waits for client to send data + (ClientHello), proving the client has started, then cancels the socket. +*/ +template +void +run_socket_cancel_test( + io_context& ioc, + context client_ctx, + context server_ctx, + ClientStreamFactory make_client, + ServerStreamFactory make_server ) +{ + + auto [s1, s2] = corosio::test::make_socket_pair( ioc ); + + auto client = make_client( s1, client_ctx ); + auto server = make_server( s2, server_ctx ); + + bool client_got_error = false; + + // Failsafe timeout - 2000ms allows headroom for CI with coverage instrumentation + timer failsafe( ioc ); + failsafe.expires_after( std::chrono::milliseconds( 2000 ) ); + + // Client starts handshake - will be cancelled + auto client_task = [&client, &client_got_error, &failsafe]() -> capy::task<> + { + auto [ec] = co_await client.handshake( tls_stream::client ); + if( ec ) + client_got_error = true; + failsafe.cancel(); + }; + + // Server waits for ClientHello then cancels - deterministic synchronization + auto server_task = [&s1, &s2]() -> capy::task<> + { + // Wait for client to send ClientHello (proves client started handshake) + char buf[1]; + (void)co_await s2.read_some( capy::mutable_buffer( buf, 1 ) ); + // Client is now blocked waiting for ServerHello - cancel its socket + s1.cancel(); + }; + + bool failsafe_hit = false; + auto failsafe_task = [&failsafe, &failsafe_hit, &s1, &s2]() -> capy::task<> + { + auto [ec] = co_await failsafe.wait(); + if( !ec ) + { + failsafe_hit = true; + if( s1.is_open() ) { s1.cancel(); s1.close(); } + if( s2.is_open() ) { s2.cancel(); s2.close(); } + } + }; + capy::run_async( ioc.get_executor() )( client_task() ); + capy::run_async( ioc.get_executor() )( server_task() ); + capy::run_async( ioc.get_executor() )( failsafe_task() ); + ioc.run(); + + BOOST_TEST( !failsafe_hit ); // failsafe timeout should not be hit + BOOST_TEST( client_got_error ); - timeout.cancel(); if( s1.is_open() ) s1.close(); if( s2.is_open() ) s2.close(); }