diff --git a/doc/design/physical-structure.md b/doc/design/physical-structure.md index 5dfc05332..eba103403 100644 --- a/doc/design/physical-structure.md +++ b/doc/design/physical-structure.md @@ -254,7 +254,7 @@ private: | Type | Base(s) | Key Operations | | ------------ | --------- | ------------------------------------------------------------------ | | tcp_socket | io_stream | connect(endpoint), shutdown(), TCP options, local/remote_endpoint() | -| tcp_acceptor | io_object | listen(endpoint, backlog), accept(tcp_socket&), local_endpoint() | +| tcp_acceptor | io_object | bind(endpoint), listen(backlog), accept(tcp_socket&), local_endpoint() | | resolver | io_object | resolve(host, service), resolve(endpoint), cancel() | **Types table** - Sockets (UDP): diff --git a/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc b/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc index 78dc524d1..4df95dcd1 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc @@ -21,6 +21,7 @@ Code snippets assume: #include #include #include +#include namespace corosio = boost::corosio; namespace capy = boost::capy; @@ -103,7 +104,7 @@ capy::task<> echo_server::worker::do_session() auto [ec, n] = co_await sock_.read_some( capy::mutable_buffer(buf_, sizeof buf_)); - auto [wec, wn] = co_await corosio::write( + auto [wec, wn] = co_await capy::write( sock_, capy::const_buffer(buf_, n)); if (wec || ec) @@ -119,28 +120,37 @@ Notice: * We reuse the worker's buffer across reads * `read_some()` returns when _any_ data arrives — it may deliver bytes alongside an error * We always write before checking the error (advance-then-check); writing zero bytes is a no-op -* `corosio::write()` writes _all_ data (it's a composed operation) +* `capy::write()` writes _all_ data (it's a composed operation) * When the coroutine ends, the launcher returns the worker to the pool == Server Construction -The server constructor populates the worker pool: +A helper builds the worker pool, then the constructor hands it to +`set_workers()`: [source,cpp] ---- + static std::vector> + make_workers(corosio::io_context& ctx, int n) + { + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; + } + public: echo_server(corosio::io_context& ctx, int max_workers) : tcp_server(ctx, ctx.get_executor()) { - wv_.reserve(max_workers); - for (int i = 0; i < max_workers; ++i) - wv_.emplace(ctx); + set_workers(make_workers(ctx, max_workers)); } }; ---- -Workers are stored polymorphically via `wv_.emplace()`, allowing different -worker types if needed. +Workers are owned polymorphically through `std::unique_ptr`, so +`set_workers()` accepts any range of worker types. == Main Function @@ -194,7 +204,7 @@ The `tcp_server` framework provides: === Why Composed Write? -The `corosio::write()` free function ensures all data is sent: +The `capy::write()` free function ensures all data is sent: [source,cpp] ---- @@ -202,7 +212,7 @@ The `corosio::write()` free function ensures all data is sent: auto [ec, n] = co_await sock.write_some(buf); // n might be < buf.size() // write: writes all data or fails -auto [ec, n] = co_await corosio::write(sock, buf); // n == buf.size() or error +auto [ec, n] = co_await capy::write(sock, buf); // n == buf.size() or error ---- For echo servers, we want complete message delivery. @@ -215,7 +225,7 @@ with advance-then-check, we always act on `n` before inspecting `ec`: [source,cpp] ---- auto [ec, n] = co_await sock.read_some(buf); -auto [wec, wn] = co_await corosio::write(sock, const_buffer(buf.data(), n)); +auto [wec, wn] = co_await capy::write(sock, const_buffer(buf.data(), n)); if (wec || ec) break; // Normal termination path ---- @@ -225,7 +235,8 @@ With exceptions, EOF would require a try-catch: [source,cpp] ---- try { - auto n = (co_await sock.read_some(buf)).value(); + auto [ec, n] = co_await sock.read_some(buf); + if (ec) throw std::system_error(ec); } catch (...) { // EOF is an exception here } @@ -254,7 +265,7 @@ World == Next Steps -* xref:3b.http-client.adoc[HTTP Client] — Build an HTTP client +* xref:3.tutorials/3b.http-client.adoc[HTTP Client] — Build an HTTP client * xref:../4.guide/4k.tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server * xref:../4.guide/4d.sockets.adoc[Sockets Guide] — Deep dive into socket operations * xref:../4.guide/4g.composed-operations.adoc[Composed Operations] — Understanding read/write diff --git a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc index 97df9dbcd..93493af82 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc @@ -19,11 +19,14 @@ Code snippets assume: [source,cpp] ---- #include +#include #include #include #include +#include #include -#include +#include +#include namespace corosio = boost::corosio; namespace capy = boost::capy; @@ -40,7 +43,8 @@ Making an HTTP request involves: 4. Reading the response 5. Handling connection close (EOF) -We'll use the exception-based pattern with `.value()` for concise code. +We'll use the exception-based pattern, throwing `std::system_error` on +failure, for concise code. == Building the Request @@ -71,16 +75,18 @@ capy::task do_request( { // Build and send the request std::string request = build_request(host); - (co_await corosio::write( - stream, capy::const_buffer(request.data(), request.size()))).value(); + if (auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); // Read the entire response std::string response; - auto [ec, n] = co_await corosio::read(stream, response); + auto [ec, n] = co_await capy::read( + stream, capy::string_dynamic_buffer(&response)); - // EOF is expected when server closes connection - if (ec && ec != capy::error::eof) - throw boost::system::system_error(ec); + // Reading into a dynamic buffer completes with success at EOF + if (ec && ec != capy::cond::eof) + throw std::system_error(ec); std::cout << response << std::endl; } @@ -88,9 +94,12 @@ capy::task do_request( Key points: -* `.value()` on the write throws if writing fails -* `corosio::read(stream, string)` reads until EOF -* We check for EOF explicitly because it's expected here +* The write throws if writing fails +* `capy::read(stream, capy::string_dynamic_buffer(&response))` reads until EOF +* Reading into a dynamic buffer completes with *success* at end-of-stream — it + returns no error and reports the total bytes read. The `!= capy::cond::eof` + check is a harmless defensive guard, not the thing that signals + end-of-response == The Connection Coroutine @@ -98,21 +107,24 @@ Key points: ---- capy::task run_client( corosio::io_context& ioc, - boost::urls::ipv4_address addr, + corosio::ipv4_address addr, std::uint16_t port) { corosio::tcp_socket s(ioc); s.open(); // Connect (throws on error) - (co_await s.connect(corosio::endpoint(addr, port))).value(); + if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec) + throw std::system_error(ec); co_await do_request(s, addr.to_string()); } ---- The socket must be opened before connecting. We pass the socket as an -`io_stream&` to `do_request`, enabling code reuse with TLS streams later. +`io_stream&` to `do_request`, so the same function works with any plain +socket. TLS streams have a different type and need their own overload, as +shown below. == Main Function @@ -128,8 +140,8 @@ int main(int argc, char* argv[]) } // Parse IP address - auto addr_result = boost::urls::parse_ipv4_address(argv[1]); - if (!addr_result) + corosio::ipv4_address addr; + if (auto ec = corosio::parse_ipv4_address(argv[1], addr); ec) { std::cerr << "Invalid IP address: " << argv[1] << "\n"; return 1; @@ -139,25 +151,28 @@ int main(int argc, char* argv[]) corosio::io_context ioc; capy::run_async(ioc.get_executor())( - run_client(ioc, *addr_result, port)); + run_client(ioc, addr, port)); ioc.run(); } ---- == Reading Until EOF -The `corosio::read(io_stream&, std::string&)` overload reads until EOF: +Wrapping a `std::string` in `capy::string_dynamic_buffer` lets +`capy::read` grow it as data arrives, reading until EOF: [source,cpp] ---- std::string response; -auto [ec, n] = co_await corosio::read(stream, response); +auto [ec, n] = co_await capy::read( + stream, capy::string_dynamic_buffer(&response)); ---- -This function: +This: * Automatically grows the string as needed -* Returns `capy::error::eof` when the connection closes +* Completes with *success* (no error) when the connection closes, since the + dynamic-buffer read treats EOF as the natural end of the stream * Returns the total bytes read in `n` == Error vs. Exception Patterns @@ -183,7 +198,8 @@ With exceptions: [source,cpp] ---- -(co_await s.connect(ep)).value(); // Throws on error +if (auto [ec] = co_await s.connect(ep); ec) // Throw on error + throw std::system_error(ec); ---- Both are valid. Use exceptions when errors are exceptional; use structured @@ -216,36 +232,73 @@ Content-Type: text/html; charset=UTF-8 == Adding TLS Support -To make HTTPS requests, wrap the socket in a `wolfssl_stream`: +To make HTTPS requests, wrap the connected socket in a `wolfssl_stream`. +A `wolfssl_stream` is not an `io_stream`, so it needs its own +`do_request` overload taking `corosio::tls_stream&`: [source,cpp] ---- #include +capy::task do_request( + corosio::tls_stream& stream, + std::string_view host) +{ + std::string request = build_request(host); + if (auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); + + std::string response; + auto [ec, n] = co_await capy::read( + stream, capy::string_dynamic_buffer(&response)); + // As with the plain stream, the dynamic-buffer read completes with + // success at EOF; this check is a defensive guard. + if (ec && ec != capy::cond::eof) + throw std::system_error(ec); + + std::cout << response << std::endl; +} + capy::task run_https_client( corosio::io_context& ioc, - boost::urls::ipv4_address addr, + corosio::ipv4_address addr, std::uint16_t port, std::string_view hostname) { corosio::tcp_socket s(ioc); s.open(); - (co_await s.connect(corosio::endpoint(addr, port))).value(); + if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec) + throw std::system_error(ec); + + // Configure the TLS context + corosio::tls_context ctx; + ctx.set_hostname(hostname); + if (auto ec = ctx.set_default_verify_paths(); ec) + throw std::system_error(ec); + if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec) + throw std::system_error(ec); - // Wrap in TLS - corosio::wolfssl_stream secure(s); - (co_await secure.handshake(corosio::wolfssl_stream::client)).value(); + // Wrap the connected socket without taking ownership (pointer form) + corosio::wolfssl_stream secure(&s, ctx); + if (auto [ec] = co_await secure.handshake( + corosio::wolfssl_stream::client); ec) + throw std::system_error(ec); co_await do_request(secure, hostname); + + if (auto [ec] = co_await secure.shutdown(); ec) + throw std::system_error(ec); } ---- -The `do_request` function works unchanged because both `socket` and -`wolfssl_stream` inherit from `io_stream`. +The TLS overload mirrors the plain one: `capy::read` and `capy::write` +work with `tls_stream` exactly as they do with `io_stream`. Only the +parameter type and the surrounding handshake/shutdown differ. == Next Steps -* xref:3c.dns-lookup.adoc[DNS Lookup] — Resolve hostnames to addresses +* xref:3.tutorials/3c.dns-lookup.adoc[DNS Lookup] — Resolve hostnames to addresses * xref:../4.guide/4l.tls.adoc[TLS Guide] — WolfSSL integration details * xref:../4.guide/4g.composed-operations.adoc[Composed Operations] — How read/write work diff --git a/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc b/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc index 342c1cea1..0f9e9d102 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc @@ -102,12 +102,12 @@ auto ep = entry.get_endpoint(); if (ep.is_v4()) { // IPv4 address - boost::urls::ipv4_address addr = ep.v4_address(); + corosio::ipv4_address addr = ep.v4_address(); } else { // IPv6 address - boost::urls::ipv6_address addr = ep.v6_address(); + corosio::ipv6_address addr = ep.v6_address(); } std::uint16_t port = ep.port(); @@ -189,13 +189,13 @@ capy::task connect_to_host( corosio::resolver r(ioc); auto [resolve_ec, results] = co_await r.resolve(host, service); if (resolve_ec) - throw boost::system::system_error(resolve_ec); + throw std::system_error(resolve_ec); corosio::tcp_socket sock(ioc); sock.open(); // Try each address until one works - boost::system::error_code last_ec; + std::error_code last_ec; for (auto const& entry : results) { auto [ec] = co_await sock.connect(entry.get_endpoint()); @@ -207,7 +207,7 @@ capy::task connect_to_host( last_ec = ec; } - throw boost::system::system_error(last_ec, "all addresses failed"); + throw std::system_error(last_ec, "all addresses failed"); } ---- @@ -241,10 +241,14 @@ Resolver operations support cancellation via `std::stop_token`: r.cancel(); // Cancel pending operation ---- +A cancelled resolution completes with `capy::cond::canceled` (the +underlying value is `capy::error::canceled`), so test for it with +`ec == capy::cond::canceled`. + Or through the affine awaitable protocol when using `capy::jcancellable_task`. == Next Steps * xref:../4.guide/4j.resolver.adoc[Resolver Guide] — Full resolver reference * xref:../4.guide/4f.endpoints.adoc[Endpoints Guide] — Working with addresses -* xref:3b.http-client.adoc[HTTP Client] — Use resolved addresses for connections +* xref:3.tutorials/3b.http-client.adoc[HTTP Client] — Use resolved addresses for connections diff --git a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc index c2454b28f..aecd785fb 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -20,7 +20,8 @@ Code snippets assume: ---- #include -namespace tls = boost::corosio::tls; +namespace corosio = boost::corosio; +using namespace boost::corosio; ---- ==== @@ -39,6 +40,26 @@ handles: Use `tls_context` to configure TLS settings once, then pass it to TLS streams for establishing secure connections. +[WARNING] +.Implementation status +==== +Several settings shown in this tutorial are accepted by the API but are +*not yet wired up* by the OpenSSL or WolfSSL backends in this release; +they are stored and silently ignored: `set_default_verify_paths()`, +`set_verify_callback()` (also fails to link), `set_alpn()`, +`set_min_protocol_version()` / `set_max_protocol_version()`, +`add_verify_path()`, all revocation features (`add_crl*`, +`set_ocsp_staple` / `set_require_ocsp_staple`, `set_revocation_policy`), +and `use_pkcs12*` (returns `std::errc::function_not_supported`). +`set_ciphersuites()` is applied by OpenSSL only. + +**To verify a peer today you must supply CA certificates explicitly** via +`load_verify_file()` or `add_certificate_authority()` — system trust via +`set_default_verify_paths()` does not work yet. The working surface is +`set_verify_mode()`, `set_verify_depth()`, custom trust anchors, +credentials, and `set_hostname()`. +==== + == Construction A `tls_context` is a shared handle to an opaque implementation. Copies share @@ -71,17 +92,17 @@ Most applications follow this pattern: tls_context ctx; // 1. Load credentials (for servers, or clients using client certs) -ctx.use_certificate_chain_file( "server.crt" ).value(); -ctx.use_private_key_file( "server.key", tls_file_format::pem ).value(); +ctx.use_certificate_chain_file( "server.crt" ); +ctx.use_private_key_file( "server.key", tls_file_format::pem ); // 2. Configure trust anchors (for verifying peer certificates) -ctx.set_default_verify_paths().value(); // Use system CA store +ctx.set_default_verify_paths(); // Use system CA store // 3. Set verification mode -ctx.set_verify_mode( tls_verify_mode::peer ).value(); +ctx.set_verify_mode( tls_verify_mode::peer ); // 4. Configure protocol options (optional) -ctx.set_min_protocol_version( tls::version::tls_1_2 ).value(); +ctx.set_min_protocol_version( tls_version::tls_1_2 ); ---- == Credential Loading @@ -98,18 +119,18 @@ from separate PEM files: [source,cpp] ---- // Load certificate chain (leaf + intermediates) -ctx.use_certificate_chain_file( "fullchain.pem" ).value(); +ctx.use_certificate_chain_file( "fullchain.pem" ); // Load the matching private key -ctx.use_private_key_file( "privkey.key", tls_file_format::pem ).value(); +ctx.use_private_key_file( "privkey.key", tls_file_format::pem ); ---- For a single certificate without intermediates: [source,cpp] ---- -ctx.use_certificate_file( "server.crt", tls_file_format::pem ).value(); -ctx.use_private_key_file( "server.key", tls_file_format::pem ).value(); +ctx.use_certificate_file( "server.crt", tls_file_format::pem ); +ctx.use_private_key_file( "server.key", tls_file_format::pem ); ---- === Loading from PKCS#12 Bundles @@ -119,9 +140,13 @@ into a single password-protected file: [source,cpp] ---- -ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ).value(); +ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ); ---- +NOTE: PKCS#12 loading is not yet implemented; `use_pkcs12()` and +`use_pkcs12_file()` return `std::errc::function_not_supported`. Load the +certificate and key separately for now. + === Loading from Memory If credentials are stored in memory (e.g., from a database or secret @@ -132,8 +157,8 @@ manager), use the non-file variants: std::string cert_pem = fetch_certificate_from_vault(); std::string key_pem = fetch_key_from_vault(); -ctx.use_certificate_chain( cert_pem ).value(); -ctx.use_private_key( key_pem, tls_file_format::pem ).value(); +ctx.use_certificate_chain( cert_pem ); +ctx.use_private_key( key_pem, tls_file_format::pem ); ---- === DER Format @@ -142,8 +167,8 @@ For binary DER-encoded files (common in embedded systems): [source,cpp] ---- -ctx.use_certificate_file( "server.der", tls_file_format::der ).value(); -ctx.use_private_key_file( "server.key.der", tls_file_format::der ).value(); +ctx.use_certificate_file( "server.der", tls_file_format::der ); +ctx.use_private_key_file( "server.key.der", tls_file_format::der ); ---- === Encrypted Private Keys @@ -162,7 +187,7 @@ For HTTPS clients connecting to public servers, use the system's CA store: [source,cpp] ---- -ctx.set_default_verify_paths().value(); +ctx.set_default_verify_paths(); ---- This uses the operating system's trusted certificates: @@ -171,6 +196,14 @@ This uses the operating system's trusted certificates: * macOS: System Keychain * Windows: Windows Certificate Store +[WARNING] +==== +`set_default_verify_paths()` is *not yet wired up*: it is a no-op and the +OS trust store is never loaded, so a client relying on it cannot verify a +public server. Until this is implemented, supply trust anchors explicitly +with `load_verify_file()` or `add_certificate_authority()` (below). +==== + === Custom CA Bundle For internal PKI or testing, load a custom CA bundle: @@ -178,18 +211,27 @@ For internal PKI or testing, load a custom CA bundle: [source,cpp] ---- // Load CA bundle file (may contain multiple CAs) -ctx.load_verify_file( "/path/to/ca-bundle.crt" ).value(); +ctx.load_verify_file( "/path/to/ca-bundle.crt" ); ---- +NOTE: `load_verify_file()` and `add_certificate_authority()` are the +currently working way to establish trust anchors. With OpenSSL, +`load_verify_file()` registers only the *first* certificate from a +multi-cert bundle (WolfSSL handles multi-cert files); add the rest with +repeated `add_certificate_authority()` calls. + === CA Directory On systems with hashed certificate directories (created by `c_rehash`): [source,cpp] ---- -ctx.add_verify_path( "/etc/ssl/certs" ).value(); +ctx.add_verify_path( "/etc/ssl/certs" ); ---- +NOTE: `add_verify_path()` is not yet applied; the directory is accepted +but never loaded. + === Individual CA Certificates Add CA certificates one at a time: @@ -198,11 +240,11 @@ Add CA certificates one at a time: ---- // From memory std::string internal_ca = load_ca_from_config(); -ctx.add_certificate_authority( internal_ca ).value(); +ctx.add_certificate_authority( internal_ca ); // Multiple CAs -ctx.add_certificate_authority( root_ca_pem ).value(); -ctx.add_certificate_authority( intermediate_ca_pem ).value(); +ctx.add_certificate_authority( root_ca_pem ); +ctx.add_certificate_authority( intermediate_ca_pem ); ---- === Combining Trust Sources @@ -212,12 +254,15 @@ You can combine multiple trust sources: [source,cpp] ---- // Start with system trust store -ctx.set_default_verify_paths().value(); +ctx.set_default_verify_paths(); // Add an internal CA for corporate servers -ctx.add_certificate_authority( corporate_ca_pem ).value(); +ctx.add_certificate_authority( corporate_ca_pem ); ---- +NOTE: The `set_default_verify_paths()` call above contributes nothing in +this release. Only the explicitly added CA is actually trusted. + == Protocol Configuration Control which TLS versions and cipher suites are allowed for connections. @@ -229,13 +274,16 @@ Set minimum and/or maximum TLS versions: [source,cpp] ---- // Require TLS 1.2 or newer (default) -ctx.set_min_protocol_version( tls::version::tls_1_2 ).value(); +ctx.set_min_protocol_version( tls_version::tls_1_2 ); // Require TLS 1.3 only -ctx.set_min_protocol_version( tls::version::tls_1_3 ).value(); -ctx.set_max_protocol_version( tls::version::tls_1_3 ).value(); +ctx.set_min_protocol_version( tls_version::tls_1_3 ); +ctx.set_max_protocol_version( tls_version::tls_1_3 ); ---- +NOTE: Protocol version bounds are not yet applied by the backends. The +negotiated range is whatever the native default method provides. + === Cipher Suites Configure allowed cipher suites using OpenSSL-style syntax: @@ -243,12 +291,15 @@ Configure allowed cipher suites using OpenSSL-style syntax: [source,cpp] ---- // Strong cipher suites only -ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ).value(); +ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ); // Disable weak ciphers -ctx.set_ciphersuites( "HIGH:!aNULL:!MD5:!RC4" ).value(); +ctx.set_ciphersuites( "HIGH:!aNULL:!MD5:!RC4" ); ---- +NOTE: `set_ciphersuites()` is applied by the OpenSSL backend only; the +WolfSSL backend accepts the string but silently ignores it. + === ALPN (Application-Layer Protocol Negotiation) ALPN negotiates the application protocol over TLS. Common uses: @@ -256,15 +307,18 @@ ALPN negotiates the application protocol over TLS. Common uses: [source,cpp] ---- // HTTP/2 with HTTP/1.1 fallback -ctx.set_alpn( { "h2", "http/1.1" } ).value(); +ctx.set_alpn( { "h2", "http/1.1" } ); // gRPC -ctx.set_alpn( { "h2" } ).value(); +ctx.set_alpn( { "h2" } ); // Custom protocol -ctx.set_alpn( { "my-protocol/1.0" } ).value(); +ctx.set_alpn( { "my-protocol/1.0" } ); ---- +NOTE: ALPN is not yet wired up; the protocol list is accepted but never +negotiated by either backend. + The server selects from the client's list based on its own preferences. == Certificate Verification @@ -276,13 +330,13 @@ Configure how peer certificates are verified during the TLS handshake. [source,cpp] ---- // Don't verify peer (not recommended for production) -ctx.set_verify_mode( tls_verify_mode::none ).value(); +ctx.set_verify_mode( tls_verify_mode::none ); // Verify peer if certificate is presented -ctx.set_verify_mode( tls_verify_mode::peer ).value(); +ctx.set_verify_mode( tls_verify_mode::peer ); // Require and verify peer certificate (mTLS server-side) -ctx.set_verify_mode( tls_verify_mode::require_peer ).value(); +ctx.set_verify_mode( tls_verify_mode::require_peer ); ---- Typical usage: @@ -313,7 +367,7 @@ Limit how many intermediate certificates are allowed: [source,cpp] ---- // Allow up to 3 intermediates (leaf -> 3 intermediates -> root) -ctx.set_verify_depth( 3 ).value(); +ctx.set_verify_depth( 3 ); ---- The default (typically 100) is sufficient for most chains. @@ -322,6 +376,13 @@ The default (typically 100) is sufficient for most chains. For advanced use cases, install a custom verification callback: +[WARNING] +==== +`set_verify_callback()` is not yet implemented: the template is declared +but never defined, so code that instantiates it *fails to link*. Use +`set_verify_mode()` with explicitly supplied trust anchors instead. +==== + [source,cpp] ---- ctx.set_verify_callback( @@ -338,7 +399,7 @@ ctx.set_verify_callback( // Additional custom checks... return true; // accept - }).value(); + }); ---- == Revocation Checking @@ -346,6 +407,16 @@ ctx.set_verify_callback( Certificate revocation checking verifies that certificates haven't been invalidated by the issuing CA. Two mechanisms are supported: CRL and OCSP. +[WARNING] +==== +None of the revocation features in this section are wired up yet. +`set_revocation_policy()`, CRLs (`add_crl()` / `add_crl_file()`), and OCSP +stapling (`set_ocsp_staple()` / `set_require_ocsp_staple()`) are all +accepted but inert. In particular, `set_require_ocsp_staple(true)` does +*not* fail the handshake when no staple is present, and `soft_fail` / +`hard_fail` do not change verification behavior. +==== + === Revocation Policy Set the overall revocation checking behavior: @@ -353,13 +424,13 @@ Set the overall revocation checking behavior: [source,cpp] ---- // Don't check revocation (default) -ctx.set_revocation_policy( tls::revocation_policy::disabled ); +ctx.set_revocation_policy( tls_revocation_policy::disabled ); // Check but allow if status is unknown (lenient) -ctx.set_revocation_policy( tls::revocation_policy::soft_fail ); +ctx.set_revocation_policy( tls_revocation_policy::soft_fail ); // Require successful revocation check (strict) -ctx.set_revocation_policy( tls::revocation_policy::hard_fail ); +ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); ---- === CRL (Certificate Revocation Lists) @@ -369,11 +440,11 @@ Load CRLs from the CA that issued the certificates you're verifying: [source,cpp] ---- // From file -ctx.add_crl_file( "/path/to/issuer.crl" ).value(); +ctx.add_crl_file( "/path/to/issuer.crl" ); // From memory (e.g., fetched via HTTP) std::string crl_data = fetch_crl_from_url( crl_url ); -ctx.add_crl( crl_data ).value(); +ctx.add_crl( crl_data ); ---- CRLs must be refreshed periodically as they expire. @@ -391,7 +462,7 @@ large CRLs. std::string ocsp_response = fetch_ocsp_response(); // Provide to clients during handshake -ctx.set_ocsp_staple( ocsp_response ).value(); +ctx.set_ocsp_staple( ocsp_response ); ---- **Client-side** (require server to staple): @@ -406,20 +477,29 @@ ctx.set_require_ocsp_staple( true ); A common pattern uses two context configurations: +[WARNING] +==== +Both contexts below rely on `set_default_verify_paths()` (a no-op here) for +trust and on revocation policy / CRLs for the "hardened" path — none of +which are applied yet. As written, neither context verifies anything. To +verify a peer today, load a CA bundle explicitly with +`load_verify_file()`; the revocation hardening is not yet available. +==== + [source,cpp] ---- // Bootstrap context: for fetching revocation data tls_context bootstrap_ctx; -bootstrap_ctx.set_default_verify_paths().value(); -bootstrap_ctx.set_verify_mode( tls_verify_mode::peer ).value(); -bootstrap_ctx.set_revocation_policy( tls::revocation_policy::disabled ); +bootstrap_ctx.set_default_verify_paths(); +bootstrap_ctx.set_verify_mode( tls_verify_mode::peer ); +bootstrap_ctx.set_revocation_policy( tls_revocation_policy::disabled ); // Hardened context: for sensitive connections tls_context hardened_ctx; -hardened_ctx.set_default_verify_paths().value(); -hardened_ctx.set_verify_mode( tls_verify_mode::peer ).value(); -hardened_ctx.add_crl_file( "cached.crl" ).value(); -hardened_ctx.set_revocation_policy( tls::revocation_policy::hard_fail ); +hardened_ctx.set_default_verify_paths(); +hardened_ctx.set_verify_mode( tls_verify_mode::peer ); +hardened_ctx.add_crl_file( "cached.crl" ); +hardened_ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); ---- == Password Handling @@ -433,14 +513,14 @@ Set a password callback before loading encrypted material. ---- // Set callback before loading encrypted key ctx.set_password_callback( - []( std::size_t max_length, tls::password_purpose purpose ) + []( std::size_t max_length, tls_password_purpose purpose ) { // purpose: for_reading (decrypt) or for_writing (encrypt) return std::string( "my-secret-password" ); }); // Now load encrypted private key -ctx.use_private_key_file( "encrypted.key", tls_file_format::pem ).value(); +ctx.use_private_key_file( "encrypted.key", tls_file_format::pem ); ---- === Secure Password Handling @@ -450,7 +530,7 @@ In production, don't hardcode passwords: [source,cpp] ---- ctx.set_password_callback( - []( std::size_t max_length, tls::password_purpose purpose ) + []( std::size_t max_length, tls_password_purpose purpose ) { // Read from environment if( auto* pw = std::getenv( "TLS_KEY_PASSWORD" ) ) @@ -467,14 +547,14 @@ PKCS#12 files take the password directly (no callback needed): [source,cpp] ---- -ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ).value(); +ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ); ---- For PKCS#12 loaded from memory: [source,cpp] ---- -ctx.use_pkcs12( pkcs12_data, "bundle-password" ).value(); +ctx.use_pkcs12( pkcs12_data, "bundle-password" ); ---- == Complete Examples @@ -484,6 +564,15 @@ scenarios. === HTTPS Client Context +[WARNING] +==== +This example trusts public CAs via `set_default_verify_paths()`, which is a +no-op in this release, so it verifies nothing. Replace it with an explicit +CA bundle to actually verify the server, for example +`ctx.load_verify_file( "/etc/ssl/certs/ca-certificates.crt" );`. The +`set_min_protocol_version()` call is also inert. +==== + [source,cpp] ---- tls_context make_https_client_context() @@ -491,13 +580,13 @@ tls_context make_https_client_context() tls_context ctx; // Trust system CAs for public websites - ctx.set_default_verify_paths().value(); + ctx.set_default_verify_paths(); // Verify server certificates - ctx.set_verify_mode( tls_verify_mode::peer ).value(); + ctx.set_verify_mode( tls_verify_mode::peer ); // Modern TLS only - ctx.set_min_protocol_version( tls::version::tls_1_2 ).value(); + ctx.set_min_protocol_version( tls_version::tls_1_2 ); return ctx; } @@ -505,8 +594,10 @@ tls_context make_https_client_context() // Usage with TLS stream tls_context ctx = make_https_client_context(); ctx.set_hostname("api.example.com"); // Set before creating stream -corosio::wolfssl_stream secure( sock, ctx ); -co_await secure.handshake( corosio::tls_stream::client ); + +// Pointer form wraps the connected socket without taking ownership +corosio::wolfssl_stream secure( &sock, ctx ); +co_await secure.handshake( corosio::wolfssl_stream::client ); ---- === TLS Server Context @@ -518,11 +609,11 @@ tls_context make_server_context() tls_context ctx; // Load server credentials - ctx.use_certificate_chain_file( "fullchain.pem" ).value(); - ctx.use_private_key_file( "privkey.pem", tls_file_format::pem ).value(); + ctx.use_certificate_chain_file( "fullchain.pem" ); + ctx.use_private_key_file( "privkey.pem", tls_file_format::pem ); // Don't verify client certificates (no mTLS) - ctx.set_verify_mode( tls_verify_mode::none ).value(); + ctx.set_verify_mode( tls_verify_mode::none ); return ctx; } @@ -537,12 +628,12 @@ tls_context make_mtls_client_context() tls_context ctx; // Client credentials for mTLS - ctx.use_certificate_chain_file( "client.crt" ).value(); - ctx.use_private_key_file( "client.key", tls_file_format::pem ).value(); + ctx.use_certificate_chain_file( "client.crt" ); + ctx.use_private_key_file( "client.key", tls_file_format::pem ); // Trust specific CA for server verification - ctx.load_verify_file( "server-ca.crt" ).value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); + ctx.load_verify_file( "server-ca.crt" ); + ctx.set_verify_mode( tls_verify_mode::peer ); return ctx; } @@ -577,4 +668,4 @@ Common errors include: == Next Steps * xref:../4.guide/4l.tls.adoc[TLS Encryption] — Using TLS streams -* xref:3b.http-client.adoc[HTTP Client Tutorial] — HTTPS example +* xref:3.tutorials/3b.http-client.adoc[HTTP Client Tutorial] — HTTPS example diff --git a/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc b/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc index 6c8b3a42d..e8dd15744 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc @@ -49,7 +49,7 @@ This tutorial demonstrates: * Accepting connections with `tcp_acceptor` * Spawning independent session coroutines with `run_async` * Switching executors with `capy::run()` for CPU-bound work -* The dispatch trampoline that returns the coroutine to its home executor +* The trampoline that returns the coroutine to its home executor == The Hash Function @@ -125,7 +125,7 @@ Three things happen in sequence, but on two different executors: the coroutine until data arrives from the kernel. 2. **Hash** — `capy::run( pool.get_executor() )` posts `compute_fnv1a` to the thread pool. The coroutine suspends on the `io_context` and resumes on a - pool thread. When the task completes, a dispatch trampoline posts the + pool thread. When the task completes, a trampoline posts the coroutine back to the `io_context`. 3. **Write** — back on the `io_context` thread, the hex result is sent to the client. @@ -146,14 +146,14 @@ auto hash = co_await capy::run( pool.get_executor() )( Behind the scenes: 1. `run()` creates an awaitable that stores the pool executor. -2. On `co_await`, the awaitable's `await_suspend` dispatches the inner task - through `pool_executor.dispatch(task_handle)`. For a thread pool, dispatch - always posts — the task is queued for a worker thread. +2. On `co_await`, the awaitable's `await_suspend` posts the inner task + through `pool_executor.post(task_handle)` — the task is queued for a worker + thread. 3. The calling coroutine suspends (the `io_context` is free to process other connections). 4. A pool thread picks up the task and runs it to completion. -5. The task's `final_suspend` resumes a dispatch trampoline, which calls - `io_context_executor.dispatch(caller_handle)` to post the caller back +5. The task's `final_suspend` resumes a trampoline, which calls + `io_context_executor.post(caller_handle)` to post the caller back to the `io_context`. 6. The caller resumes on the `io_context` thread with the hash result. diff --git a/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc b/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc index 5f592e732..02a544948 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc @@ -207,7 +207,11 @@ stop_src.request_stop(); When the stop source is signaled: -1. The timer's `wait()` returns `cond::canceled`. +1. The loop only inspects the timer's `wait()` for cancellation, so that is + where the stop is observed: `wait()` returns `cond::canceled`. A stop + requested mid-`connect()` is not checked there; it simply falls through to + the next timer `wait()`, which then returns `cond::canceled`. Either way + the coroutine exits cleanly. 2. The coroutine checks the error and executes `co_return`. 3. Local variables (`sock`, `delay`) are destroyed through normal unwinding. 4. With no more outstanding work, `run()` returns. @@ -228,7 +232,7 @@ executes the coroutine's own cleanup code. | Mechanism | Coroutine sees cancellation? | Use case | `stop_token` -| Yes — operations return `cond::canceled` +| Yes — the awaited operation returns `cond::canceled` (here, the timer `wait()`) | Graceful shutdown | `stop()` + `restart()` @@ -281,9 +285,10 @@ int main(int argc, char* argv[]) ---- The event loop runs on a background thread. After five seconds the main thread -signals cancellation. The coroutine observes `cond::canceled`, unwinds, the -work count reaches zero, and `run()` returns. The `jthread` destructor joins -automatically. +signals cancellation. The coroutine observes `cond::canceled` on its timer +`wait()`, unwinds, the work count reaches zero, and `run()` returns. The +`jthread` destructor joins automatically. The shipped example uses a plain +`std::thread` with an explicit `join()`; the two are equivalent here. == Testing diff --git a/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc index cdeea66c6..cec210845 100644 --- a/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc +++ b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc @@ -11,7 +11,7 @@ This chapter introduces the networking concepts you need to understand before using Corosio. If you're already comfortable with TCP/IP, sockets, and the -client-server model, you can skip to xref:4c.io-context.adoc[I/O Context]. +client-server model, you can skip to xref:4.guide/4c.io-context.adoc[I/O Context]. == What is a Network? @@ -474,7 +474,7 @@ the checksum and discards corrupted packets. Unlike TCP, UDP doesn't retransmit—the data is simply lost. Corosio supports TCP, UDP, and Unix domain sockets. See -xref:4p.unix-sockets.adoc[Unix Domain Sockets] for local inter-process +xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets] for local inter-process communication without the TCP/IP stack overhead. == Ports and Sockets @@ -675,9 +675,12 @@ The `TCP_NODELAY` socket option disables Nagle's algorithm, sending data immediately regardless of size. Use this for latency-sensitive applications where you're sending small packets that shouldn't be delayed. +Corosio exposes this through `socket_option::no_delay`, set via +`tcp_socket::set_option`: + [source,cpp] ---- -// Note: Corosio doesn't currently expose TCP_NODELAY directly +sock.set_option(corosio::socket_option::no_delay(true)); ---- === SO_REUSEADDR @@ -729,7 +732,7 @@ exact amounts: auto [ec, n] = co_await sock.read_some(buf); // Right: reads until buffer is full or EOF -auto [ec, n] = co_await corosio::read(sock, buf); +auto [ec, n] = co_await capy::read(sock, buf); ---- === Connection Refused @@ -758,6 +761,6 @@ For a deeper understanding of TCP/IP: == Next Steps -* xref:4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands -* xref:4c.io-context.adoc[I/O Context] — The event loop -* xref:4d.sockets.adoc[Sockets] — Socket operations in detail +* xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands +* xref:4.guide/4c.io-context.adoc[I/O Context] — The event loop +* xref:4.guide/4d.sockets.adoc[Sockets] — Socket operations in detail diff --git a/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc index 8beabf054..77a5362ae 100644 --- a/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc +++ b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc @@ -534,7 +534,7 @@ for (int i = 0; i < max_workers; ++i) ---- Corosio's `tcp_server` class implements this pattern—see -xref:4k.tcp-server.adoc[TCP Server] for details. +xref:4.guide/4k.tcp-server.adoc[TCP Server] for details. === Pipelines @@ -630,5 +630,5 @@ provides excellent performance with simple, race-free code. == Next Steps -* xref:4c.io-context.adoc[I/O Context] — The event loop in detail +* xref:4.guide/4c.io-context.adoc[I/O Context] — The event loop in detail * xref:../3.tutorials/3a.echo-server.adoc[Echo Server] — Practical concurrency example diff --git a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc index 07e788e56..ba8509e1c 100644 --- a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc @@ -52,9 +52,10 @@ The `io_context`: corosio::io_context ioc; ---- -Creates an `io_context` with a concurrency hint equal to -`std::thread::hardware_concurrency()`. If more than one thread is available, -thread-safe synchronization is enabled. +Creates an `io_context` with a concurrency hint of +`std::max(2u, std::thread::hardware_concurrency())` — the default constructor +never selects single-threaded mode. Single-threaded (lockless) mode is keyed +on a `concurrency_hint` of exactly `1`; pass `1` explicitly to opt into it. === With Concurrency Hint @@ -67,7 +68,10 @@ corosio::io_context ioc(4); // Up to 4 threads, thread-safe The concurrency hint affects: * Internal synchronization strategy -* IOCP thread pool size on Windows +* On Windows, the hint is passed to `CreateIoCompletionPort` as + `NumberOfConcurrentThreads`, bounding how many completion threads may run + concurrently. It is not a library-managed thread pool, and it is distinct + from `thread_pool_size`. Use `1` for single-threaded programs to avoid synchronization overhead. @@ -88,6 +92,7 @@ This function: * Blocks until all work completes or `stop()` is called * Returns the number of handlers executed * Automatically stops when no outstanding work remains +* Returns immediately with `0` if there is no outstanding work when called === run_one() @@ -190,18 +195,21 @@ if (ex.running_in_this_thread()) ---- auto ex = ioc.get_executor(); -// Dispatch: symmetric transfer if inside run(), otherwise post -ex(handle); +// Dispatch a continuation: symmetric transfer if inside run(), +// otherwise post. Returns a handle to resume. +std::coroutine_handle<> next = ex.dispatch(cont); -// Post: always queue for later execution -ex.post(handle); +// Post a continuation: always queue for later execution +ex.post(cont); -// Defer: same as post (conveys continuation intent) -ex.defer(handle); +// Post a bare coroutine handle for later execution +ex.post(handle); ---- -The dispatch operation `ex(handle)` enables symmetric transfer when already -running inside `run()`. This is how child coroutines resume parents +The dispatch operation `ex.dispatch(cont)` enables symmetric transfer when +already running inside `run()`: it returns `cont.h` directly so the caller can +resume it inline. Otherwise it posts the continuation for later execution and +returns `std::noop_coroutine()`. This is how child coroutines resume parents efficiently. === Work Tracking @@ -310,6 +318,6 @@ On macOS and FreeBSD, the `io_context` uses kqueue: == Next Steps -* xref:4d.sockets.adoc[Sockets] — I/O with TCP sockets -* xref:4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections -* xref:4h.timers.adoc[Timers] — Async delays and timeouts +* xref:4.guide/4d.sockets.adoc[Sockets] — I/O with TCP sockets +* xref:4.guide/4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections +* xref:4.guide/4h.timers.adoc[Timers] — Async delays and timeouts diff --git a/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc b/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc index 19c95c71b..5e60a2ac2 100644 --- a/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc @@ -2,9 +2,21 @@ :navtitle: Configuration The `io_context_options` struct provides runtime tuning knobs for the -I/O context and its backend scheduler. All defaults match the -library's built-in values, so an unconfigured context behaves -identically to previous releases. +I/O context and its backend scheduler. The defaults listed in the +table below are the struct field defaults — the values you get from a +freshly default-constructed `io_context_options`. + +[IMPORTANT] +==== +The inline budget defaults in the table are *not* what a +default-constructed `io_context` runs with on a multi-core machine. +When `concurrency_hint > 1` and the inline budgets are still at their +struct defaults, the constructor overrides them to `(0, 0, 0)`, +disabling the inline fast path (see the note under +<>). Pass an explicit +`io_context_options` with non-default budgets to keep the fast path +enabled in multi-threaded mode. +==== [source,cpp] ---- @@ -84,6 +96,9 @@ corosio::native_io_context ioc(opts); |=== Options that do not apply to the active backend are silently ignored. +The one exception is `thread_pool_size`, which is always validated: +a value less than `1` causes construction to throw +`std::invalid_argument`. == Tuning Guidelines @@ -97,6 +112,7 @@ The event buffer controls how many I/O events are fetched in a single * *Many idle connections* (chat servers, WebSocket hubs): keep at 128 or lower for better fairness. +[#inline-completion-budget] === Inline Completion Budget The inline budget controls how many I/O completions the reactor @@ -117,11 +133,12 @@ a re-queue through the scheduler. [NOTE] ==== When `io_context` is constructed with `concurrency_hint > 1` and all -three budget fields are at their defaults `(2, 16, 4)`, the -constructor overrides them to `(0, 0, 0)`. Multi-thread workloads -benefit from cross-thread work-stealing, which "post-everything" -mode enables. Setting any budget field to a non-default value -disables the override. +three budget fields are at their struct defaults `(2, 16, 4)` — which +is the case for a default-constructed context on a multi-core machine +— the constructor overrides them to `(0, 0, 0)`, disabling the inline +fast path. Multi-thread workloads benefit from cross-thread +work-stealing, which "post-everything" mode enables. Setting any +budget field to a non-default value disables the override. ==== === IOCP Timeout (`gqcs_timeout_ms`) diff --git a/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc b/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc index 15046aecf..527eb929d 100644 --- a/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc +++ b/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc @@ -37,7 +37,7 @@ corosio::tcp_socket s(ioc); s.open(); auto [ec] = co_await s.connect( - corosio::endpoint(boost::urls::ipv4_address::loopback(), 8080)); + corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); char buf[1024]; auto [read_ec, n] = co_await s.read_some( @@ -68,7 +68,8 @@ Creates the underlying TCP socket: [source,cpp] ---- -s.open(); // Creates IPv4 TCP socket, associates with IOCP +s.open(); // Creates IPv4 TCP socket, associates with the platform + // reactor (IOCP on Windows, epoll/kqueue/select on POSIX) ---- This allocates a socket handle and registers it with the I/O backend. @@ -83,7 +84,8 @@ Releases socket resources: s.close(); // Cancels pending ops, closes socket ---- -Any pending operations complete with `operation_canceled`. +Any pending operations complete with `capy::error::canceled`; test for it +portably with `ec == capy::cond::canceled`. === is_open() @@ -123,8 +125,8 @@ Common error conditions: | `network_unreachable` | No route to the host -| `operation_canceled` -| Cancelled via `cancel()` or stop token +| `capy::error::canceled` +| Cancelled via `cancel()` or stop token (test with `ec == capy::cond::canceled`) |=== === Exception Pattern @@ -133,7 +135,8 @@ For simpler code when errors are fatal: [source,cpp] ---- -(co_await s.connect(endpoint)).value(); // Throws on error +if (auto [ec] = co_await s.connect(endpoint); ec) + throw std::system_error(ec); // Throws on error ---- === Range-Based Connect @@ -232,24 +235,25 @@ When the peer closes the connection: ---- auto [ec, n] = co_await s.read_some(buf); -if (ec == capy::error::eof) +if (ec == capy::cond::eof) // Connection closed normally - -if (n == 0 && !ec) - // Also indicates EOF in some cases ---- +EOF is signaled only by `ec == capy::cond::eof`. A successful read with +`n == 0 && !ec` is not EOF: it occurs when the buffer has zero length, and +the request simply completes with nothing to do. + === Reading Exact Amounts -Use the `corosio::read()` free function to fill a buffer completely: +Use the `capy::read()` free function to fill a buffer completely: [source,cpp] ---- -auto [ec, n] = co_await corosio::read(s, buf); +auto [ec, n] = co_await capy::read(s, buf); // n == buffer_size(buf) or error occurred ---- -See xref:4g.composed-operations.adoc[Composed Operations] for details. +See xref:4.guide/4g.composed-operations.adoc[Composed Operations] for details. == Writing Data @@ -269,11 +273,11 @@ than the buffer size. === Writing All Data -Use the `corosio::write()` free function: +Use the `capy::write()` free function: [source,cpp] ---- -auto [ec, n] = co_await corosio::write(s, buf); +auto [ec, n] = co_await capy::write(s, buf); // n == buffer_size(buf) or error occurred ---- @@ -288,7 +292,8 @@ Cancel pending operations: s.cancel(); ---- -All outstanding operations complete with `operation_canceled`. +All outstanding operations complete with `capy::error::canceled`; test for it +portably with `ec == capy::cond::canceled`. === Stop Token Cancellation @@ -345,7 +350,7 @@ This enables polymorphic use: ---- capy::task send_data(corosio::io_stream& stream) { - co_await corosio::write(stream, some_buffer); + co_await capy::write(stream, some_buffer); } // Works with socket, wolfssl_stream, or any io_stream @@ -371,7 +376,7 @@ std::array bufs = { co_await s.read_some(bufs); ---- -See xref:4n.buffers.adoc[Buffer Sequences] for details. +See xref:4.guide/4n.buffers.adoc[Buffer Sequences] for details. == Thread Safety @@ -398,12 +403,14 @@ capy::task echo_client(corosio::io_context& ioc) corosio::tcp_socket s(ioc); s.open(); - (co_await s.connect( - corosio::endpoint(boost::urls::ipv4_address::loopback(), 8080))).value(); + if (auto [ec] = co_await s.connect( + corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); ec) + throw std::system_error(ec); std::string msg = "Hello, server!"; - (co_await corosio::write( - s, capy::const_buffer(msg.data(), msg.size()))).value(); + if (auto [ec, n] = co_await capy::write( + s, capy::const_buffer(msg.data(), msg.size())); ec) + throw std::system_error(ec); char buf[1024]; auto [ec, n] = co_await s.read_some( @@ -417,7 +424,7 @@ capy::task echo_client(corosio::io_context& ioc) == Next Steps -* xref:4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections -* xref:4f.endpoints.adoc[Endpoints] — IP addresses and ports -* xref:4g.composed-operations.adoc[Composed Operations] — read() and write() +* xref:4.guide/4e.tcp-acceptor.adoc[Acceptors] — Accept incoming connections +* xref:4.guide/4f.endpoints.adoc[Endpoints] — IP addresses and ports +* xref:4.guide/4g.composed-operations.adoc[Composed Operations] — read() and write() * xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Server example diff --git a/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc b/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc index 9597b7cd2..7b0802f74 100644 --- a/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc +++ b/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc @@ -31,9 +31,8 @@ A tcp_acceptor binds to a local endpoint and waits for clients to connect: [source,cpp] ---- -corosio::tcp_acceptor acc(ioc); -if (auto ec = acc.listen(corosio::endpoint(8080))) // Listen on port 8080 - return ec; +// Convenience constructor: open + SO_REUSEADDR + bind + listen on port 8080 +corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); corosio::tcp_socket peer(ioc); auto [ec] = co_await acc.accept(peer); @@ -58,53 +57,75 @@ auto ex = ioc.get_executor(); corosio::tcp_acceptor acc2(ex); ---- -The tcp_acceptor doesn't own system resources until `listen()` is called. +A default-constructed acceptor doesn't own system resources until it is +opened, bound, and set to listen. == Listening -=== listen() +Setting up an acceptor involves three operations: opening a socket, binding it +to a local endpoint, and marking it as passive (listening). You can do all +three in one expression with the convenience constructor, or perform them +separately for fine-grained control. -The `listen()` method creates a socket, binds to an endpoint, and begins -listening for connections: +=== Convenience Constructor + +The simplest way to get a listening acceptor is the convenience constructor, +which opens, sets `SO_REUSEADDR`, binds, and listens in a single step: [source,cpp] ---- -if (auto ec = acc.listen(corosio::endpoint(8080))) +// open + SO_REUSEADDR + bind + listen; address family deduced from the endpoint +corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); +---- + +This throws `std::system_error` if binding or listening fails. Unlike the +standalone `open()`, the convenience constructor enables `SO_REUSEADDR` before +binding, so the listening port can be reused immediately after a restart. + +=== bind() and listen() + +For explicit error handling, construct the acceptor, then bind and listen as +separate steps. Both return a `std::error_code` and are marked `[[nodiscard]]` +to prevent accidentally ignoring errors: + +[source,cpp] +---- +corosio::tcp_acceptor acc(ioc); +acc.open(); // create an IPv4 TCP socket + +if (auto ec = acc.bind(corosio::endpoint(8080))) +{ + std::cerr << "Bind failed: " << ec.message() << "\n"; + return ec; +} + +if (auto ec = acc.listen()) { std::cerr << "Listen failed: " << ec.message() << "\n"; return ec; } ---- -This performs three operations: - -1. Creates an IPv4 TCP socket -2. Binds to the specified endpoint -3. Marks the socket as passive (listening) - -Returns a `std::error_code` indicating success or failure. The return value -is marked `[[nodiscard]]` to prevent accidentally ignoring errors. - -=== Parameters +The `listen()` method accepts an optional `backlog` parameter: [source,cpp] ---- -[[nodiscard]] std::error_code listen(endpoint ep, int backlog = 128); +[[nodiscard]] std::error_code listen(int backlog = 128); ---- The `backlog` parameter specifies the maximum queue length for pending -connections. When the queue is full, new connection attempts receive -`ECONNREFUSED`. The default of 128 works for most applications. +connections. When the queue is full, the kernel may drop or refuse new +connection attempts. The default of 128 works for most applications. === Binding to All Interfaces -To accept connections on any network interface: +To accept connections on any network interface, bind to port only, which uses +`0.0.0.0` (all IPv4 interfaces): [source,cpp] ---- // Port only - binds to 0.0.0.0 (all IPv4 interfaces) -if (auto ec = acc.listen(corosio::endpoint(8080))) - return ec; +corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); ---- === Binding to a Specific Interface @@ -114,9 +135,8 @@ To accept connections only on a specific interface: [source,cpp] ---- // Localhost only -if (auto ec = acc.listen(corosio::endpoint( - boost::urls::ipv4_address::loopback(), 8080))) - return ec; +corosio::tcp_acceptor acc(ioc, corosio::endpoint( + corosio::ipv4_address::loopback(), 8080)); ---- == Accepting Connections @@ -161,13 +181,14 @@ Common accept errors: | `operation_canceled` | Cancelled via `cancel()` or stop token -| `bad_file_descriptor` -| Acceptor not listening - | Resource errors | System limit reached (file descriptors, memory) |=== +Calling `accept()` on an acceptor that is not listening is a precondition +violation: it throws `std::logic_error` rather than completing with an error +code. + === Preconditions * The tcp_acceptor must be listening (`is_open() == true`) @@ -196,7 +217,7 @@ protocol: ---- // Inside a cancellable task: auto [ec] = co_await acc.accept(peer); -if (ec == make_error_code(system::errc::operation_canceled)) +if (ec == std::errc::operation_canceled) std::cout << "Accept cancelled\n"; ---- @@ -276,7 +297,7 @@ capy::task accept_loop( if (ec) { - if (ec == make_error_code(system::errc::operation_canceled)) + if (ec == std::errc::operation_canceled) break; // Shutdown requested std::cerr << "Accept error: " << ec.message() << "\n"; @@ -306,7 +327,13 @@ Coordinate shutdown with signal handling: capy::task run_server(corosio::io_context& ioc) { corosio::tcp_acceptor acc(ioc); - if (auto ec = acc.listen(corosio::endpoint(8080))) + acc.open(); + if (auto ec = acc.bind(corosio::endpoint(8080))) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + co_return; + } + if (auto ec = acc.listen()) { std::cerr << "Listen failed: " << ec.message() << "\n"; co_return; @@ -330,7 +357,7 @@ capy::task run_server(corosio::io_context& ioc) == Relationship to tcp_server -For production servers, consider using xref:4k.tcp-server.adoc[tcp_server] which +For production servers, consider using xref:4.guide/4k.tcp-server.adoc[tcp_server] which provides: * Worker pool management @@ -343,6 +370,6 @@ upon. == Next Steps -* xref:4d.sockets.adoc[Sockets] — Using accepted connections -* xref:4k.tcp-server.adoc[TCP Server] — Higher-level server framework +* xref:4.guide/4d.sockets.adoc[Sockets] — Using accepted connections +* xref:4.guide/4k.tcp-server.adoc[TCP Server] — Higher-level server framework * xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Complete example diff --git a/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc b/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc index f839ac0f6..cc4c6aa28 100644 --- a/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc +++ b/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc @@ -19,8 +19,8 @@ Code snippets assume: [source,cpp] ---- #include -#include -#include +#include +#include namespace corosio = boost::corosio; ---- @@ -33,10 +33,10 @@ An endpoint combines an address and port: [source,cpp] ---- // IPv4 endpoint -corosio::endpoint ep4(boost::urls::ipv4_address::loopback(), 8080); +corosio::endpoint ep4(corosio::ipv4_address::loopback(), 8080); // IPv6 endpoint -corosio::endpoint ep6(boost::urls::ipv6_address::loopback(), 8080); +corosio::endpoint ep6(corosio::ipv6_address::loopback(), 8080); // Port only (binds to all interfaces) corosio::endpoint bind_ep(8080); @@ -48,7 +48,7 @@ corosio::endpoint bind_ep(8080); [source,cpp] ---- -auto addr = boost::urls::ipv4_address::loopback(); // 127.0.0.1 +auto addr = corosio::ipv4_address::loopback(); // 127.0.0.1 corosio::endpoint ep(addr, 8080); ---- @@ -56,7 +56,7 @@ corosio::endpoint ep(addr, 8080); [source,cpp] ---- -auto addr = boost::urls::ipv6_address::loopback(); // ::1 +auto addr = corosio::ipv6_address::loopback(); // ::1 corosio::endpoint ep(addr, 8080); ---- @@ -102,7 +102,7 @@ std::uint16_t port = ep.port(); // Host byte order ---- if (ep.is_v4()) { - boost::urls::ipv4_address addr = ep.v4_address(); + corosio::ipv4_address addr = ep.v4_address(); std::cout << addr.to_string() << "\n"; } ---- @@ -113,7 +113,7 @@ if (ep.is_v4()) ---- if (ep.is_v6()) { - boost::urls::ipv6_address addr = ep.v6_address(); + corosio::ipv6_address addr = ep.v6_address(); std::cout << addr.to_string() << "\n"; } ---- @@ -125,10 +125,10 @@ if (ep.is_v6()) [source,cpp] ---- // IPv4 loopback: 127.0.0.1 -auto v4_loop = boost::urls::ipv4_address::loopback(); +auto v4_loop = corosio::ipv4_address::loopback(); // IPv6 loopback: ::1 -auto v6_loop = boost::urls::ipv6_address::loopback(); +auto v6_loop = corosio::ipv6_address::loopback(); ---- === Any Address @@ -136,10 +136,10 @@ auto v6_loop = boost::urls::ipv6_address::loopback(); [source,cpp] ---- // IPv4 any: 0.0.0.0 (all interfaces) -auto v4_any = boost::urls::ipv4_address::any(); +auto v4_any = corosio::ipv4_address::any(); // IPv6 any: :: (all interfaces) -auto v6_any = boost::urls::ipv6_address::any(); +auto v6_any = corosio::ipv6_address::any(); ---- === Broadcast @@ -147,27 +147,41 @@ auto v6_any = boost::urls::ipv6_address::any(); [source,cpp] ---- // IPv4 broadcast: 255.255.255.255 -auto v4_bcast = boost::urls::ipv4_address::broadcast(); +auto v4_bcast = corosio::ipv4_address::broadcast(); ---- == Parsing Addresses -Parse addresses from strings using Boost.URL: +Parse addresses from strings. Parsing reports failure through an +`std::error_code` out-parameter: [source,cpp] ---- // IPv4 -auto result = boost::urls::parse_ipv4_address("192.168.1.1"); -if (result) +corosio::ipv4_address addr; +if (auto ec = corosio::parse_ipv4_address("192.168.1.1", addr); !ec) { - corosio::endpoint ep(*result, 8080); + corosio::endpoint ep(addr, 8080); } // IPv6 -auto result6 = boost::urls::parse_ipv6_address("2001:db8::1"); -if (result6) +corosio::ipv6_address addr6; +if (auto ec = corosio::parse_ipv6_address("2001:db8::1", addr6); !ec) { - corosio::endpoint ep(*result6, 8080); + corosio::endpoint ep(addr6, 8080); +} +---- + +You can also parse a full `address:port` string directly into an endpoint +using `parse_endpoint()`, or the `endpoint` constructor that accepts a +`std::string_view`: + +[source,cpp] +---- +corosio::endpoint ep; +if (auto ec = corosio::parse_endpoint("192.168.1.1:8080", ep); !ec) +{ + // Use ep... } ---- @@ -177,9 +191,9 @@ Endpoints support equality comparison: [source,cpp] ---- -corosio::endpoint ep1(boost::urls::ipv4_address::loopback(), 8080); -corosio::endpoint ep2(boost::urls::ipv4_address::loopback(), 8080); -corosio::endpoint ep3(boost::urls::ipv4_address::loopback(), 9090); +corosio::endpoint ep1(corosio::ipv4_address::loopback(), 8080); +corosio::endpoint ep2(corosio::ipv4_address::loopback(), 8080); +corosio::endpoint ep3(corosio::ipv4_address::loopback(), 9090); assert(ep1 == ep2); // Same address and port assert(ep1 != ep3); // Different port @@ -195,7 +209,7 @@ corosio::tcp_socket s(ioc); s.open(); corosio::endpoint target( - boost::urls::ipv4_address::loopback(), 8080); + corosio::ipv4_address::loopback(), 8080); auto [ec] = co_await s.connect(target); ---- @@ -204,9 +218,8 @@ auto [ec] = co_await s.connect(target); [source,cpp] ---- -corosio::tcp_acceptor acc(ioc); -if (auto ec = acc.listen(corosio::endpoint(8080))) // Bind to all interfaces - return ec; +// Convenience constructor: open + SO_REUSEADDR + bind + listen +corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); // bind to all interfaces ---- == From Resolver Results @@ -256,6 +269,6 @@ use from any thread. == Next Steps -* xref:4d.sockets.adoc[Sockets] — Connect to endpoints -* xref:4j.resolver.adoc[Name Resolution] — Convert hostnames to endpoints +* xref:4.guide/4d.sockets.adoc[Sockets] — Connect to endpoints +* xref:4.guide/4j.resolver.adoc[Name Resolution] — Convert hostnames to endpoints * xref:../3.tutorials/3c.dns-lookup.adoc[DNS Lookup Tutorial] — Practical resolution diff --git a/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc b/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc index 161d1e247..f915e4004 100644 --- a/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc +++ b/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc @@ -143,25 +143,28 @@ write( 2. If an error occurs, returns immediately with bytes written so far 3. On success, returns total bytes (equals `buffer_size(buffers)`) -== consuming_buffers Helper +== buffer_slice Helper -Both `read()` and `write()` use `consuming_buffers` internally to track -progress through a buffer sequence: +Both `read()` and `write()` use `capy::buffer_slice` internally to track +progress through a buffer sequence. `buffer_slice` returns a view over a +byte range of the underlying sequence. It exposes the bytes not yet +transferred via `data()` and advances past completed bytes with +`remove_prefix(n)`: [source,cpp] ---- -#include +#include std::array bufs = { capy::mutable_buffer(header, 16), capy::mutable_buffer(body, 1024) }; -capy::consuming_buffers consuming(bufs); +auto slice = capy::buffer_slice(bufs); // After reading 20 bytes: -consuming.consume(20); -// Now consuming represents: 4 bytes of header remaining + full body +slice.remove_prefix(20); +// slice.data() now represents: 4 bytes of header remaining + full body ---- === Interface @@ -169,19 +172,23 @@ consuming.consume(20); [source,cpp] ---- template -class consuming_buffers -{ -public: - explicit consuming_buffers(BufferSequence const& bufs); - - void consume(std::size_t n); - - const_iterator begin() const; - const_iterator end() const; -}; + requires MutableBufferSequence + || ConstBufferSequence +auto +buffer_slice( + BufferSequence const& seq, + std::size_t offset = 0, + std::size_t length = std::numeric_limits::max()); ---- -The iterator returns adjusted buffers accounting for consumed bytes. +The returned object satisfies the `Slice` concept (and `MutableSlice` +when `seq` is a mutable buffer sequence). Its `data()` member returns a +buffer sequence covering the remaining bytes, and `remove_prefix(n)` +advances the slice past `n` consumed bytes. + +NOTE: `seq` must outlive the slice and any buffer sequence obtained from +its `data()`. Passing a temporary buffer sequence is rejected at compile +time; hoist it into a named variable first. == Error Handling Patterns @@ -277,6 +284,6 @@ capy::task read_http_response(corosio::io_stream& stream) == Next Steps -* xref:4d.sockets.adoc[Sockets] — The underlying stream interface -* xref:4n.buffers.adoc[Buffer Sequences] — Working with buffers +* xref:4.guide/4d.sockets.adoc[Sockets] — The underlying stream interface +* xref:4.guide/4n.buffers.adoc[Buffer Sequences] — Working with buffers * xref:../3.tutorials/3b.http-client.adoc[HTTP Client Tutorial] — Practical example diff --git a/doc/modules/ROOT/pages/4.guide/4h.timers.adoc b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc index 3c2bc6a70..049666fd0 100644 --- a/doc/modules/ROOT/pages/4.guide/4h.timers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc @@ -102,7 +102,7 @@ The cancelled wait completes with an error: [source,cpp] ---- auto [ec] = co_await t.wait(); -if (ec == capy::error::canceled) +if (ec == capy::cond::canceled) std::cout << "Timer was cancelled\n"; ---- @@ -134,7 +134,7 @@ std::size_t n = t.expires_after(5s); // Resets to 5s, cancels previous waits Multiple coroutines can wait on the same timer concurrently. When the timer expires, all waiters complete with success. When cancelled, all -waiters complete with `capy::error::canceled`: +waiters complete with `capy::cond::canceled`: [source,cpp] ---- @@ -252,7 +252,7 @@ Timer waits support stop token cancellation through the affine protocol: ---- // Inside a cancellable task: auto [ec] = co_await t.wait(); -// Completes with capy::error::canceled if stop requested +// Completes with capy::cond::canceled if stop requested ---- == Move Semantics @@ -308,7 +308,7 @@ capy::task heartbeat( // Send heartbeat std::string ping = "PING\r\n"; - auto [wec, n] = co_await corosio::write( + auto [wec, n] = co_await capy::write( sock, capy::const_buffer(ping.data(), ping.size())); if (wec) @@ -319,6 +319,6 @@ capy::task heartbeat( == Next Steps -* xref:4i.signals.adoc[Signal Handling] — Respond to OS signals -* xref:4c.io-context.adoc[I/O Context] — The event loop -* xref:4m.error-handling.adoc[Error Handling] — Cancellation patterns +* xref:4.guide/4i.signals.adoc[Signal Handling] — Respond to OS signals +* xref:4.guide/4c.io-context.adoc[I/O Context] — The event loop +* xref:4.guide/4m.error-handling.adoc[Error Handling] — Cancellation patterns diff --git a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc index 6f953a821..bb2ab63fb 100644 --- a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc +++ b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc @@ -180,9 +180,8 @@ Remove a signal from the set: ---- signals.remove(SIGINT); -// With error code -boost::system::error_code ec; -signals.remove(SIGINT, ec); +// remove() returns an error_code instead of throwing +std::error_code ec = signals.remove(SIGINT); ---- Removing a signal that's not in the set has no effect. @@ -195,9 +194,8 @@ Remove all signals from the set: ---- signals.clear(); -// With error code -boost::system::error_code ec; -signals.clear(ec); +// clear() returns an error_code instead of throwing +std::error_code ec = signals.clear(); ---- == Waiting for Signals @@ -233,12 +231,12 @@ Cancel pending wait operations: signals.cancel(); ---- -The wait completes with `capy::error::canceled`: +The wait completes with `capy::cond::canceled`: [source,cpp] ---- auto [ec, signum] = co_await signals.wait(); -if (ec == capy::error::canceled) +if (ec == capy::cond::canceled) std::cout << "Wait was cancelled\n"; ---- @@ -391,9 +389,7 @@ capy::task run_server(corosio::io_context& ioc) }(ioc, running)); // Accept loop - corosio::acceptor acc(ioc); - if (auto ec = acc.listen(corosio::endpoint(8080))) - co_return; + corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); while (running) { @@ -428,6 +424,6 @@ The `restart` flag is particularly useful—without it, blocking calls like == Next Steps -* xref:4h.timers.adoc[Timers] — Timed operations -* xref:4c.io-context.adoc[I/O Context] — The event loop +* xref:4.guide/4h.timers.adoc[Timers] — Timed operations +* xref:4.guide/4c.io-context.adoc[I/O Context] — The event loop * xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Server example diff --git a/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc b/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc index 37811a511..8116aea9f 100644 --- a/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc +++ b/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc @@ -19,6 +19,8 @@ Code snippets assume: [source,cpp] ---- #include +#include +#include namespace corosio = boost::corosio; ---- ==== @@ -98,12 +100,21 @@ auto [ec, results] = co_await r.resolve( | Only return IPv4 if the system has IPv4 configured, same for IPv6 | `v4_mapped` -| If no IPv6 addresses found, return IPv4-mapped IPv6 addresses +| Intended to return IPv4-mapped IPv6 addresses when no IPv6 addresses are +found. Has no effect in the current implementation (see note below). | `all_matching` -| With `v4_mapped`, return all matching IPv4 and IPv6 addresses +| Intended to combine with `v4_mapped` to return all matching IPv4 and IPv6 +addresses. Has no effect in the current implementation (see note below). |=== +[NOTE] +==== +`v4_mapped` and `all_matching` are currently inert. The resolver always +queries with `ai_family = AF_UNSPEC`, but the underlying `AI_V4MAPPED` and +`AI_ALL` behavior only takes effect for `AF_INET6`-family queries. +==== + Flags can be combined: [source,cpp] @@ -183,7 +194,7 @@ capy::task connect_to_service( auto [resolve_ec, results] = co_await r.resolve(host, service); if (resolve_ec) - throw boost::system::system_error(resolve_ec); + throw std::system_error(resolve_ec); if (results.empty()) throw std::runtime_error("No addresses found"); @@ -191,7 +202,7 @@ capy::task connect_to_service( corosio::tcp_socket sock(ioc); sock.open(); - boost::system::error_code last_error; + std::error_code last_error; for (auto const& entry : results) { auto [ec] = co_await sock.connect(entry.get_endpoint()); @@ -203,7 +214,7 @@ capy::task connect_to_service( sock.open(); } - throw boost::system::system_error(last_error); + throw std::system_error(last_error); } ---- @@ -218,7 +229,10 @@ Cancel pending resolution: r.cancel(); ---- -The resolution completes with `operation_canceled`. +The in-flight resolution completes with `capy::cond::canceled` (the +underlying value is `capy::error::canceled`). Match it portably with +`ec == capy::cond::canceled` rather than comparing against a specific +enumerator. === Stop Token Cancellation @@ -227,25 +241,35 @@ protocol. == Error Handling -Common resolution errors: +Resolution failures do not surface as named DNS error codes. The +underlying `getaddrinfo()` `EAI_*` errors are mapped to generic +`std::errc` values: [cols="1,2"] |=== -| Error | Meaning +| Failure | Resulting `std::error_code` -| `host_not_found` -| Hostname doesn't exist +| Host not found (`EAI_NONAME`) +| `std::errc::no_such_device_or_address` -| `no_data` -| Hostname exists but has no addresses +| Unknown service (`EAI_SERVICE`) +| `std::errc::invalid_argument` -| `service_not_found` -| Unknown service name - -| `operation_canceled` -| Resolution was cancelled +| Temporary failure (`EAI_AGAIN`) +| `std::errc::resource_unavailable_try_again` |=== +Cancellation is reported through capy's category rather than `std::errc`. +Match it with: + +[source,cpp] +---- +if (ec == capy::cond::canceled) +{ + // resolution was cancelled +} +---- + == Move Semantics Resolvers are move-only: @@ -341,12 +365,13 @@ capy::task http_get( "Connection: close\r\n" "\r\n"; - (co_await corosio::write( - sock, capy::const_buffer(request.data(), request.size()))).value(); + if (auto [ec, n] = co_await capy::write( + sock, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); // Read response std::string response; - co_await corosio::read(sock, response); + co_await capy::read(sock, response); std::cout << response << "\n"; } @@ -358,8 +383,16 @@ The resolver uses the system's `getaddrinfo()` function. On most platforms, this is a blocking call executed on a thread pool to avoid blocking the I/O context. +[IMPORTANT] +==== +Because resolution runs on a thread pool, it requires a multi-threaded +`io_context`. When the `io_context` is constructed single-threaded (a +`concurrency_hint` of 1), `resolve()` completes with +`std::errc::operation_not_supported` and never performs a lookup. +==== + == Next Steps -* xref:4f.endpoints.adoc[Endpoints] — Working with resolved addresses -* xref:4d.sockets.adoc[Sockets] — Connecting to endpoints +* xref:4.guide/4f.endpoints.adoc[Endpoints] — Working with resolved addresses +* xref:4.guide/4d.sockets.adoc[Sockets] — Connecting to endpoints * xref:../3.tutorials/3c.dns-lookup.adoc[DNS Lookup Tutorial] — Complete example diff --git a/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc b/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc index d5b220482..81738dc4d 100644 --- a/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc +++ b/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc @@ -40,38 +40,47 @@ framework handles: [source,cpp] ---- -class echo_server : public corosio::tcp_server +class echo_worker : public corosio::tcp_server::worker_base { - struct worker : worker_base + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf; + +public: + explicit echo_worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf; + buf.reserve(4096); + } - explicit worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf.reserve(4096); - } + corosio::tcp_socket& socket() override { return sock_; } - corosio::tcp_socket& socket() override { return sock_; } + void run(launcher launch) override + { + launch(ctx_.get_executor(), do_echo()); + } - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_echo()); - } + capy::task do_echo(); +}; - capy::task do_echo(); - }; +// Build the worker pool as a range of pointer-like objects. +auto make_echo_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} +class echo_server : public corosio::tcp_server +{ public: - echo_server(corosio::io_context& ioc) + echo_server(corosio::io_context& ioc, int max_workers) : tcp_server(ioc, ioc.get_executor()) { - wv_.reserve(100); - for (int i = 0; i < 100; ++i) - wv_.emplace(ioc); + set_workers(make_echo_workers(ioc, max_workers)); } }; ---- @@ -83,14 +92,20 @@ a socket and any state needed for a session. === worker_base -The `worker_base` class is the foundation: +The `worker_base` class is the foundation. It declares a constructor and a +virtual destructor, holds private intrusive-list bookkeeping used by the +server's idle and active pools, and exposes two pure virtuals you must +override: [source,cpp] ---- class worker_base { + // Private list/bookkeeping members managed by tcp_server. public: - virtual ~worker_base() = default; + worker_base(); + virtual ~worker_base(); + virtual void run(launcher launch) = 0; virtual corosio::tcp_socket& socket() = 0; }; @@ -101,13 +116,14 @@ required methods: [source,cpp] ---- -struct my_worker : tcp_server::worker_base +class my_worker : public corosio::tcp_server::worker_base { corosio::io_context& ctx_; corosio::tcp_socket sock_; std::string request_buf; std::string response_buf; +public: explicit my_worker(corosio::io_context& ctx) : ctx_(ctx) , sock_(ctx) @@ -128,37 +144,45 @@ struct my_worker : tcp_server::worker_base }; ---- -=== The workers Container +=== Providing Workers -The `workers` class manages the worker pool: +The worker pool is installed with `set_workers()`. It accepts any forward +range of pointer-like objects convertible to `worker_base*`, such as a +`std::vector>`. The server takes ownership of the +range and reuses each worker across connections: [source,cpp] ---- -class workers -{ -public: - template - T& emplace(Args&&... args); - - void reserve(std::size_t n); - std::size_t size() const noexcept; -}; +template +void set_workers(Range&& workers); ---- -Use `emplace()` to add workers during construction: +A small helper that builds the range keeps construction tidy: [source,cpp] ---- -my_server(corosio::io_context& ioc) - : tcp_server(ioc, ioc.get_executor()) +auto make_workers(corosio::io_context& ctx, int n) { - wv_.reserve(max_workers); - for (int i = 0; i < max_workers; ++i) - wv_.emplace(ioc); + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; } + +class my_server : public corosio::tcp_server +{ +public: + my_server(corosio::io_context& ioc, int max_workers) + : tcp_server(ioc, ioc.get_executor()) + { + set_workers(make_workers(ioc, max_workers)); + } +}; ---- -Workers are stored polymorphically, allowing different worker types if needed. +Because the range holds `unique_ptr`, workers are stored +polymorphically, allowing different worker types in the same pool if needed. == The Launcher @@ -229,6 +253,9 @@ Begin accepting connections: server.start(); ---- +Workers must have been provided via `set_workers()` before calling `start()`, +and at least one endpoint must be bound. + After `start()`, the server: 1. Listens on all bound ports @@ -236,7 +263,9 @@ After `start()`, the server: 3. Assigns connections to available workers 4. Calls each worker's `run()` method -The accept loop runs until the `io_context` stops. +The accept loop runs until `tcp_server::stop()` is called, which requests the +workers' stop token. Note that `stop()` does not close the acceptors, so a +suspended accept completes once more before the loop exits. == Complete Example @@ -244,66 +273,76 @@ The accept loop runs until the `io_context` stops. ---- #include #include -#include #include #include +#include #include +#include +#include namespace corosio = boost::corosio; namespace capy = boost::capy; -class echo_server : public corosio::tcp_server +class echo_worker : public corosio::tcp_server::worker_base { - struct worker : worker_base + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf; + +public: + explicit echo_worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf; + buf.reserve(4096); + } - explicit worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf.reserve(4096); - } + corosio::tcp_socket& socket() override { return sock_; } - corosio::tcp_socket& socket() override { return sock_; } + void run(launcher launch) override + { + launch(ctx_.get_executor(), do_session()); + } - void run(launcher launch) override + capy::task do_session() + { + for (;;) { - launch(ctx_.get_executor(), do_session()); - } + buf.resize(4096); + auto [ec, n] = co_await sock_.read_some( + capy::mutable_buffer(buf.data(), buf.size())); - capy::task do_session() - { - for (;;) - { - buf.resize(4096); - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf.data(), buf.size())); + if (ec || n == 0) + break; - if (ec || n == 0) - break; + buf.resize(n); + auto [wec, wn] = co_await capy::write( + sock_, capy::const_buffer(buf.data(), buf.size())); - buf.resize(n); - auto [wec, wn] = co_await corosio::write( - sock_, capy::const_buffer(buf.data(), buf.size())); + if (wec) + break; + } - if (wec) - break; - } + sock_.close(); + } +}; - sock_.close(); - } - }; +auto make_echo_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} +class echo_server : public corosio::tcp_server +{ public: echo_server(corosio::io_context& ctx, int max_workers) : tcp_server(ctx, ctx.get_executor()) { - wv_.reserve(max_workers); - for (int i = 0; i < max_workers; ++i) - wv_.emplace(ctx); + set_workers(make_echo_workers(ctx, max_workers)); } }; @@ -389,6 +428,6 @@ synchronization. == Next Steps -* xref:4d.sockets.adoc[Sockets] — Socket operations -* xref:4b.concurrent-programming.adoc[Concurrent Programming] — Coroutine patterns +* xref:4.guide/4d.sockets.adoc[Sockets] — Socket operations +* xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutine patterns * xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Simpler approach diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index d85efd01e..06d298051 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -21,9 +21,12 @@ Code snippets assume: #include #include #include +#include +#include namespace corosio = boost::corosio; -namespace tls = corosio::tls; +namespace capy = boost::capy; +using namespace boost::corosio; ---- ==== @@ -37,24 +40,59 @@ confidentiality, integrity, and authentication. Corosio supports TLS through: * **wolfssl_stream** — TLS implementation using WolfSSL * **openssl_stream** — TLS implementation using OpenSSL +[WARNING] +.Implementation status +==== +Several `tls_context` settings are accepted by the API but are *not yet +wired up* by the OpenSSL or WolfSSL backends in this release. They are +stored and silently ignored: + +* `set_default_verify_paths()` — the OS trust store is never loaded. A + client that relies on it has an *empty trust store* and cannot verify a + public server. +* `set_verify_callback()` — declared but not defined; instantiating it + *fails to link*. +* `set_alpn()` — ALPN is never negotiated. +* `set_min_protocol_version()` / `set_max_protocol_version()` — version + bounds are never applied; the range is the native default. +* `add_crl()` / `add_crl_file()`, `set_ocsp_staple()` / + `set_require_ocsp_staple()`, `set_revocation_policy()` — all + revocation checking is inert. +* `add_verify_path()` — directory of CAs is never loaded. +* `use_pkcs12()` / `use_pkcs12_file()` — return + `std::errc::function_not_supported`. +* `set_ciphersuites()` — applied by OpenSSL only; WolfSSL ignores it. + +**To verify a peer today you must supply CA certificates explicitly** via +`load_verify_file()` or `add_certificate_authority()`. The working +configuration surface is: `set_verify_mode()`, `set_verify_depth()`, +custom trust anchors (`add_certificate_authority()` / `load_verify_file()`), +credentials (`use_certificate*` / `use_private_key*`), and `set_hostname()` +for SNI plus client-side hostname verification. +==== + The typical flow: [source,cpp] ---- // 1. Configure a context tls_context ctx; -ctx.set_default_verify_paths().value(); -ctx.set_verify_mode(tls_verify_mode::peer).value(); ctx.set_hostname("api.example.com"); +if (auto ec = ctx.set_default_verify_paths(); ec) + throw std::system_error(ec); +if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) + throw std::system_error(ec); // 2. Connect a socket corosio::tcp_socket sock(ioc); sock.open(); -(co_await sock.connect(endpoint)).value(); +if (auto [ec] = co_await sock.connect(endpoint); ec) + throw std::system_error(ec); -// 3. Wrap in TLS stream -corosio::wolfssl_stream secure(sock, ctx); -(co_await secure.handshake(tls::role::client)).value(); +// 3. Wrap the connected socket (pointer form; does not take ownership) +corosio::wolfssl_stream secure(&sock, ctx); +if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) + throw std::system_error(ec); // 4. Use encrypted I/O auto [ec, n] = co_await secure.read_some(buffer); @@ -90,14 +128,14 @@ key. [source,cpp] ---- // From file -ctx.use_certificate_file("server.crt", tls_file_format::pem).value(); +ctx.use_certificate_file("server.crt", tls_file_format::pem); // From memory std::string cert_data = /* ... */; -ctx.use_certificate(cert_data, tls_file_format::pem).value(); +ctx.use_certificate(cert_data, tls_file_format::pem); // Certificate chain (cert + intermediates) -ctx.use_certificate_chain_file("fullchain.pem").value(); +ctx.use_certificate_chain_file("fullchain.pem"); ---- ==== Loading Private Keys @@ -105,10 +143,10 @@ ctx.use_certificate_chain_file("fullchain.pem").value(); [source,cpp] ---- // From file -ctx.use_private_key_file("server.key", tls_file_format::pem).value(); +ctx.use_private_key_file("server.key", tls_file_format::pem); // From memory -ctx.use_private_key(key_data, tls_file_format::pem).value(); +ctx.use_private_key(key_data, tls_file_format::pem); ---- For encrypted private keys, set a password callback first: @@ -116,10 +154,10 @@ For encrypted private keys, set a password callback first: [source,cpp] ---- ctx.set_password_callback( - [](std::size_t max_len, tls::password_purpose purpose) { + [](std::size_t max_len, tls_password_purpose purpose) { return std::string("my-key-password"); }); -ctx.use_private_key_file("encrypted.key", tls_file_format::pem).value(); +ctx.use_private_key_file("encrypted.key", tls_file_format::pem); ---- ==== PKCS#12 Bundles @@ -128,9 +166,13 @@ PKCS#12 (`.pfx` or `.p12`) files bundle certificate, key, and chain together: [source,cpp] ---- -ctx.use_pkcs12_file("credentials.pfx", "bundle-password").value(); +ctx.use_pkcs12_file("credentials.pfx", "bundle-password"); ---- +NOTE: PKCS#12 loading is not yet implemented; `use_pkcs12()` and +`use_pkcs12_file()` return `std::errc::function_not_supported`. Load the +certificate and key separately for now. + === Trust Anchors Configure which Certificate Authorities (CAs) to trust for peer verification. @@ -141,25 +183,39 @@ Use the operating system's default CA certificates: [source,cpp] ---- -ctx.set_default_verify_paths().value(); +ctx.set_default_verify_paths(); ---- -This is the recommended approach for HTTPS clients connecting to public servers. +[WARNING] +==== +`set_default_verify_paths()` is *not yet wired up*: it is a no-op and the +OS trust store is never loaded. A client relying on it has an empty trust +store and cannot verify a public server. Until this is implemented, load a +CA bundle explicitly with `load_verify_file()` or +`add_certificate_authority()`. +==== ==== Custom CA Certificates [source,cpp] ---- // Single CA from memory -ctx.add_certificate_authority(ca_pem).value(); +ctx.add_certificate_authority(ca_pem); // CA file (may contain multiple certs) -ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt").value(); +ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); // Directory of hashed CA files -ctx.add_verify_path("/etc/ssl/certs").value(); +ctx.add_verify_path("/etc/ssl/certs"); ---- +NOTE: `add_certificate_authority()` and `load_verify_file()` are the +currently working way to establish trust anchors. With OpenSSL, +`load_verify_file()` registers only the *first* certificate from a +multi-cert bundle (WolfSSL handles multi-cert files); add others with +repeated `add_certificate_authority()` calls. `add_verify_path()` is not +yet applied — the directory is never loaded. + === Protocol Configuration ==== TLS Version @@ -167,22 +223,25 @@ ctx.add_verify_path("/etc/ssl/certs").value(); [source,cpp] ---- // Require TLS 1.3 minimum -ctx.set_min_protocol_version(tls::version::tls_1_3).value(); +ctx.set_min_protocol_version(tls_version::tls_1_3); // Cap at TLS 1.2 (unusual, but possible) -ctx.set_max_protocol_version(tls::version::tls_1_2).value(); +ctx.set_max_protocol_version(tls_version::tls_1_2); ---- +NOTE: Protocol version bounds are not yet applied by the backends. The +negotiated range is whatever the native default method provides. + Available versions: [cols="1,2"] |=== | Version | Description -| `tls::version::tls_1_2` +| `tls_version::tls_1_2` | TLS 1.2 (RFC 5246) -| `tls::version::tls_1_3` +| `tls_version::tls_1_3` | TLS 1.3 (RFC 8446) |=== @@ -191,9 +250,12 @@ Available versions: [source,cpp] ---- // OpenSSL-style cipher string -ctx.set_ciphersuites("ECDHE+AESGCM:ECDHE+CHACHA20").value(); +ctx.set_ciphersuites("ECDHE+AESGCM:ECDHE+CHACHA20"); ---- +NOTE: `set_ciphersuites()` is applied by the OpenSSL backend only; the +WolfSSL backend accepts the string but silently ignores it. + ==== ALPN Application-Layer Protocol Negotiation selects the application protocol: @@ -201,9 +263,12 @@ Application-Layer Protocol Negotiation selects the application protocol: [source,cpp] ---- // Prefer HTTP/2, fall back to HTTP/1.1 -ctx.set_alpn({"h2", "http/1.1"}).value(); +ctx.set_alpn({"h2", "http/1.1"}); ---- +NOTE: ALPN is not yet wired up; the protocol list is accepted but never +negotiated by either backend. + === Certificate Verification ==== Verification Mode @@ -211,13 +276,13 @@ ctx.set_alpn({"h2", "http/1.1"}).value(); [source,cpp] ---- // Don't verify peer (not recommended for clients) -ctx.set_verify_mode(tls_verify_mode::none).value(); +ctx.set_verify_mode(tls_verify_mode::none); // Verify if peer presents certificate -ctx.set_verify_mode(tls_verify_mode::peer).value(); +ctx.set_verify_mode(tls_verify_mode::peer); // Require peer certificate (fail if not presented) -ctx.set_verify_mode(tls_verify_mode::require_peer).value(); +ctx.set_verify_mode(tls_verify_mode::require_peer); ---- For HTTPS clients, use `peer`. For servers requiring client certificates @@ -244,13 +309,20 @@ Limit the certificate chain depth: [source,cpp] ---- -ctx.set_verify_depth(10).value(); // Max 10 intermediate certs +ctx.set_verify_depth(10); // Max 10 intermediate certs ---- ==== Custom Verification Callback For advanced verification logic: +[WARNING] +==== +`set_verify_callback()` is not yet implemented: the template is declared +but never defined, so code that instantiates it *fails to link*. Use +`set_verify_mode()` with explicitly supplied trust anchors instead. +==== + [source,cpp] ---- ctx.set_verify_callback( @@ -266,15 +338,22 @@ ctx.set_verify_callback( === Revocation Checking +WARNING: None of the revocation features below are wired up yet. CRLs +(`add_crl()` / `add_crl_file()`), OCSP stapling (`set_ocsp_staple()` / +`set_require_ocsp_staple()`), and `set_revocation_policy()` are all accepted +but inert. In particular, `set_require_ocsp_staple(true)` does *not* fail the +handshake when no staple is present, and `soft_fail` / `hard_fail` do not +change verification behavior. + ==== Certificate Revocation Lists [source,cpp] ---- // Load CRL from file -ctx.add_crl_file("issuer.crl").value(); +ctx.add_crl_file("issuer.crl"); // Load CRL from memory -ctx.add_crl(crl_data).value(); +ctx.add_crl(crl_data); ---- ==== OCSP Stapling @@ -283,7 +362,7 @@ For servers, provide a stapled OCSP response: [source,cpp] ---- -ctx.set_ocsp_staple(ocsp_response_data).value(); +ctx.set_ocsp_staple(ocsp_response_data); ---- For clients, require the server to staple: @@ -298,41 +377,55 @@ ctx.set_require_ocsp_staple(true); [source,cpp] ---- // Don't check revocation (default) -ctx.set_revocation_policy(tls::revocation_policy::disabled); +ctx.set_revocation_policy(tls_revocation_policy::disabled); // Check but allow if status unknown -ctx.set_revocation_policy(tls::revocation_policy::soft_fail); +ctx.set_revocation_policy(tls_revocation_policy::soft_fail); // Fail if revocation status can't be determined -ctx.set_revocation_policy(tls::revocation_policy::hard_fail); +ctx.set_revocation_policy(tls_revocation_policy::hard_fail); ---- == TLS Streams -TLS streams wrap an underlying `io_stream` (like `socket`) to provide encrypted -I/O. +TLS streams wrap an underlying stream (like a connected `tcp_socket`) to +provide encrypted I/O. === tls_stream Base Class -The `tls_stream` class inherits from `io_stream` and adds: +`tls_stream` is a standalone, coroutine-based abstract base class. It does +*not* derive from `io_stream`: unlike OS-level I/O completed by the kernel, +its operations are coroutines that orchestrate reads and writes on the +underlying stream. Its `read_some`/`write_some` template wrappers satisfy +the `capy::Stream` concept, so composed operations like `capy::read` and +`capy::write` work with it. [source,cpp] ---- -class tls_stream : public io_stream +class tls_stream { public: enum handshake_type { client, server }; - auto handshake(handshake_type type); // Perform TLS handshake - auto shutdown(); // Graceful TLS shutdown + template + auto read_some(B const& buffers); // Decrypt and read + + template + auto write_some(B const& buffers); // Encrypt and write + + virtual capy::io_task<> handshake(handshake_type type) = 0; + virtual capy::io_task<> shutdown() = 0; - io_stream& next_layer(); // Access underlying stream + virtual capy::any_stream& next_layer() noexcept = 0; // Underlying stream }; ---- === wolfssl_stream -The WolfSSL-based implementation: +The WolfSSL-based implementation. Two construction modes are available: +the *reference* form takes a pointer and does not own the stream (the caller +keeps it alive), while the *owning* form takes the stream by value and moves +it. To wrap an already-connected socket, use the pointer form: [source,cpp] ---- @@ -344,18 +437,22 @@ corosio::tcp_socket sock(ioc); tls_context ctx; // ... configure ctx ... -corosio::wolfssl_stream secure(sock, ctx); +// Reference form: sock must outlive secure +corosio::wolfssl_stream secure(&sock, ctx); + +// Or owning form: secure takes ownership of the socket +corosio::wolfssl_stream owned(std::move(sock), ctx); ---- === openssl_stream -The OpenSSL-based implementation: +The OpenSSL-based implementation, with the same construction modes: [source,cpp] ---- #include -corosio::openssl_stream secure(sock, ctx); +corosio::openssl_stream secure(&sock, ctx); ---- Both implementations provide the same interface through `tls_stream`. @@ -406,7 +503,8 @@ Common handshake failures: == Reading and Writing -After handshake, use the stream like any `io_stream`: +After handshake, read and write through the TLS stream just as you would +any `capy::Stream`: [source,cpp] ---- @@ -423,15 +521,15 @@ auto [wec, wn] = co_await secure.write_some( === Composed Operations -The `read()` and `write()` free functions work with TLS streams: +The `capy::read()` and `capy::write()` free functions work with TLS streams: [source,cpp] ---- // Read until buffer full -auto [ec, n] = co_await corosio::read(secure, large_buffer); +auto [ec, n] = co_await capy::read(secure, large_buffer); // Write all data -auto [wec, wn] = co_await corosio::write(secure, data_buffer); +auto [wec, wn] = co_await capy::write(secure, data_buffer); ---- == Shutdown @@ -448,29 +546,43 @@ sock.close(); Shutdown is optional but recommended. Without it, the peer can't distinguish between a graceful close and a truncation attack. -== Polymorphic Use +== Plain and Encrypted Connections -Because TLS streams inherit from `io_stream`, you can write code that works -with both encrypted and unencrypted connections: +A `tls_stream` is *not* an `io_stream`, so a function taking `io_stream&` +will not accept a TLS stream. Provide a separate overload taking +`tls_stream&` for the encrypted case. The bodies are identical because +`capy::read` and `capy::write` accept either stream: [source,cpp] ---- capy::task send_request(corosio::io_stream& stream) { std::string request = "GET / HTTP/1.1\r\n\r\n"; - (co_await corosio::write( - stream, capy::const_buffer(request.data(), request.size()))).value(); + if (auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); + + std::string response; + co_await capy::read(stream, capy::string_dynamic_buffer(&response)); +} + +capy::task send_request(corosio::tls_stream& stream) +{ + std::string request = "GET / HTTP/1.1\r\n\r\n"; + if (auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); std::string response; - co_await corosio::read(stream, response); + co_await capy::read(stream, capy::string_dynamic_buffer(&response)); } -// Works with plain socket +// Plain socket uses the io_stream overload corosio::tcp_socket sock(ioc); co_await send_request(sock); -// Also works with TLS stream -corosio::wolfssl_stream secure(sock, ctx); +// TLS stream uses the tls_stream overload +corosio::wolfssl_stream secure(&sock, ctx); co_await send_request(secure); ---- @@ -478,6 +590,19 @@ co_await send_request(secure); Complete example connecting to an HTTPS server: +[WARNING] +==== +This example uses `set_default_verify_paths()`, which does *not* currently +load any trust anchors, so it verifies nothing against a real public server. +To verify the server today, replace that call with an explicit CA bundle, +for example: + +[source,cpp] +---- +ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); +---- +==== + [source,cpp] ---- capy::task https_get( @@ -490,7 +615,7 @@ capy::task https_get( auto [resolve_ec, results] = co_await resolver.resolve( hostname, std::to_string(port)); if (resolve_ec) - throw boost::system::system_error(resolve_ec); + throw std::system_error(resolve_ec); // Connect TCP socket corosio::tcp_socket sock(ioc); @@ -505,13 +630,16 @@ capy::task https_get( // Configure TLS tls_context ctx; - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode(tls_verify_mode::peer).value(); ctx.set_hostname(hostname); + if (auto ec = ctx.set_default_verify_paths(); ec) + throw std::system_error(ec); + if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) + throw std::system_error(ec); - // Wrap in TLS and handshake - corosio::wolfssl_stream secure(sock, ctx); - (co_await secure.handshake(tls_stream::client)).value(); + // Wrap the connected socket (pointer form) and handshake + corosio::wolfssl_stream secure(&sock, ctx); + if (auto [ec] = co_await secure.handshake(wolfssl_stream::client); ec) + throw std::system_error(ec); // Send HTTP request std::string request = @@ -520,16 +648,18 @@ capy::task https_get( "Connection: close\r\n" "\r\n"; - (co_await corosio::write( - secure, capy::const_buffer(request.data(), request.size()))).value(); + if (auto [ec, n] = co_await capy::write( + secure, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); // Read response std::string response; - auto [ec, n] = co_await corosio::read(secure, response); + auto [ec, n] = co_await capy::read( + secure, capy::string_dynamic_buffer(&response)); // EOF expected when server closes - if (ec && ec != capy::error::eof) - throw boost::system::system_error(ec); + if (ec && ec != capy::cond::eof) + throw std::system_error(ec); std::cout << response << "\n"; @@ -550,13 +680,11 @@ capy::task tls_server( { // Configure server TLS context tls_context ctx; - ctx.use_certificate_chain_file("server-fullchain.pem").value(); - ctx.use_private_key_file("server.key", tls_file_format::pem).value(); + ctx.use_certificate_chain_file("server-fullchain.pem"); + ctx.use_private_key_file("server.key", tls_file_format::pem); // Set up acceptor - corosio::acceptor acc(ioc); - if (auto ec = acc.listen(corosio::endpoint(port))) - co_return; + corosio::tcp_acceptor acc(ioc, corosio::endpoint(port)); for (;;) { @@ -574,9 +702,10 @@ capy::task handle_tls_client( corosio::tcp_socket sock, tls_context ctx) { - corosio::wolfssl_stream secure(sock, ctx); + // Owning form: the handler owns the socket, so move it in + corosio::wolfssl_stream secure(std::move(sock), ctx); - auto [ec] = co_await secure.handshake(tls_stream::server); + auto [ec] = co_await secure.handshake(wolfssl_stream::server); if (ec) co_return; @@ -599,26 +728,29 @@ For client certificate authentication: [source,cpp] ---- tls_context server_ctx; -server_ctx.use_certificate_chain_file("server.pem").value(); -server_ctx.use_private_key_file("server.key", tls_file_format::pem).value(); +server_ctx.use_certificate_chain_file("server.pem"); +server_ctx.use_private_key_file("server.key", tls_file_format::pem); // Require client certificate -server_ctx.set_verify_mode(tls_verify_mode::require_peer).value(); -server_ctx.load_verify_file("client-ca.pem").value(); +server_ctx.set_verify_mode(tls_verify_mode::require_peer); +server_ctx.load_verify_file("client-ca.pem"); ---- === Client Side +NOTE: `set_default_verify_paths()` below is a no-op in this release; supply +the server's CA explicitly with `load_verify_file()` to actually verify it. + [source,cpp] ---- tls_context client_ctx; -client_ctx.set_default_verify_paths().value(); -client_ctx.set_verify_mode(tls_verify_mode::peer).value(); +client_ctx.set_default_verify_paths(); +client_ctx.set_verify_mode(tls_verify_mode::peer); client_ctx.set_hostname("server.example.com"); // Provide client certificate -client_ctx.use_certificate_file("client.crt", tls_file_format::pem).value(); -client_ctx.use_private_key_file("client.key", tls_file_format::pem).value(); +client_ctx.use_certificate_file("client.crt", tls_file_format::pem); +client_ctx.use_private_key_file("client.key", tls_file_format::pem); ---- == Thread Safety @@ -663,6 +795,6 @@ target_link_libraries(my_target PRIVATE OpenSSL::SSL OpenSSL::Crypto) == Next Steps -* xref:4d.sockets.adoc[Sockets] — The underlying stream -* xref:4g.composed-operations.adoc[Composed Operations] — read() and write() +* xref:4.guide/4d.sockets.adoc[Sockets] — The underlying stream +* xref:4.guide/4g.composed-operations.adoc[Composed Operations] — read() and write() * xref:../3.tutorials/3d.tls-context.adoc[TLS Context Tutorial] — Step-by-step configuration diff --git a/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc index e2e77e4ad..49fa63b46 100644 --- a/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc +++ b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc @@ -9,8 +9,8 @@ = Error Handling -Corosio provides flexible error handling through the `io_result` type, which -supports both error-code and exception-based patterns. +Corosio reports I/O errors through the `io_result` type, which carries an +error code alongside any values produced by the operation. [NOTE] ==== @@ -19,7 +19,7 @@ Code snippets assume: ---- #include #include -#include +#include namespace corosio = boost::corosio; namespace capy = boost::capy; @@ -28,10 +28,14 @@ namespace capy = boost::capy; == The io_result Type -Every I/O operation returns an `io_result` that contains: +Every I/O operation returns an `io_result<...>` with two public members: -* An error code (always present) -* Additional values depending on the operation +* `ec` — the error code (always present) +* `values` — a tuple of any additional values produced by the operation + +`io_result` also models the tuple protocol, so it can be destructured with +structured bindings. It has no `value()` member and no conversion to `bool`; +check for errors by testing `ec`. [source,cpp] ---- @@ -66,42 +70,40 @@ else This pattern gives you full control over error handling. -== Exception Pattern +== Accessing Members Directly -Call `.value()` to throw on error: +You can also bind the whole result and read its members: [source,cpp] ---- -// Throws system_error if connect fails -(co_await sock.connect(endpoint)).value(); - -// Returns bytes transferred, throws on error -auto n = (co_await sock.read_some(buffer)).value(); +auto result = co_await sock.connect(endpoint); +if (!result.ec) + std::cout << "Connected successfully\n"; +else + std::cerr << "Failed: " << result.ec.message() << "\n"; ---- -The `.value()` method: +The payload lives in `result.values`; for single-value results, prefer +structured bindings, which name the value for you. -* Returns the value(s) if no error -* Throws `boost::system::system_error` if `ec` is set +== Throwing on Error -== Boolean Conversion - -`io_result` is contextually convertible to `bool`: +`io_result` never throws on its own. To turn an error into an exception, +test `ec` and throw explicitly: [source,cpp] ---- -auto result = co_await sock.connect(endpoint); -if (result) - std::cout << "Connected successfully\n"; -else - std::cerr << "Failed: " << result.ec.message() << "\n"; +auto [ec, n] = co_await sock.read_some(buffer); +if (ec) + throw std::system_error(ec); +// 'n' bytes were read ---- -Returns `true` if the operation succeeded (no error). +This keeps error handling explicit and avoids hidden control flow. -== Choosing a Pattern +== Structured Bindings vs. Explicit Throwing -=== Use Structured Bindings When: +=== Inspect ec When: * Errors are expected and need handling (EOF, timeout) * You want to log errors without throwing @@ -111,14 +113,14 @@ Returns `true` if the operation succeeded (no error). [source,cpp] ---- auto [ec, n] = co_await sock.read_some(buf); -if (ec == capy::error::eof) +if (ec == capy::cond::eof) { std::cout << "End of stream after " << n << " bytes\n"; // Not an exceptional condition } ---- -=== Use Exceptions When: +=== Throw When: * Errors are truly exceptional * You want concise, linear code @@ -127,10 +129,15 @@ if (ec == capy::error::eof) [source,cpp] ---- -(co_await sock.connect(endpoint)).value(); -(co_await corosio::write(sock, request)).value(); -auto response = (co_await corosio::read(sock, buffer)).value(); -// Any error throws immediately +auto throw_on_error = [](auto result) { + if (result.ec) + throw std::system_error(result.ec); + return result; +}; + +throw_on_error(co_await sock.connect(endpoint)); +throw_on_error(co_await capy::write(sock, request)); +auto [ec, n] = throw_on_error(co_await capy::read(sock, buffer)); ---- == Common Error Codes @@ -141,7 +148,7 @@ auto response = (co_await corosio::read(sock, buffer)).value(); |=== | Error | Meaning -| `capy::error::eof` +| `capy::cond::eof` | End of stream reached | `connection_refused` @@ -162,18 +169,18 @@ auto response = (co_await corosio::read(sock, buffer)).value(); === Cancellation -[cols="1,2"] -|=== -| Error | Meaning +Cancellation does not map deterministically to a single category or +value per trigger. Depending on the path, a cancelled operation may +surface as `capy::error::canceled` (capy's category) or as +`std::errc::operation_canceled` (the generic category). For example, a +stop token that is already requested when the operation is awaited tends +to produce `std::errc::operation_canceled`, while `cancel()`, an +in-flight stop-token cancel, and a syscall reporting `ECANCELED` tend to +produce `capy::error::canceled`. Do not rely on the specific category or +value. -| `capy::error::canceled` -| Cancelled via `cancel()` method - -| `operation_canceled` -| Cancelled via stop token -|=== - -Check cancellation portably: +Always test cancellation portably with the `capy::cond::canceled` +condition, which matches both: [source,cpp] ---- @@ -183,12 +190,12 @@ if (ec == capy::cond::canceled) == EOF Handling -End-of-stream is signaled by `capy::error::eof`: +End-of-stream is signaled by the `capy::cond::eof` condition: [source,cpp] ---- -auto [ec, n] = co_await corosio::read(stream, buffer); -if (ec == capy::error::eof) +auto [ec, n] = co_await capy::read(stream, buffer); +if (ec == capy::cond::eof) { std::cout << "Stream ended, read " << n << " bytes total\n"; // This is often expected, not an error @@ -199,14 +206,13 @@ else if (ec) } ---- -When using `.value()` on read operations, EOF throws an exception. Filter -it if expected: +When you throw on read errors, filter out EOF if it is expected: [source,cpp] ---- -auto [ec, n] = co_await corosio::read(stream, response); -if (ec && ec != capy::error::eof) - throw boost::system::system_error(ec); +auto [ec, n] = co_await capy::read(stream, response); +if (ec && ec != capy::cond::eof) + throw std::system_error(ec); // EOF is expected when server closes connection ---- @@ -216,7 +222,7 @@ Some operations may partially succeed before an error: [source,cpp] ---- -auto [ec, n] = co_await corosio::write(stream, large_buffer); +auto [ec, n] = co_await capy::write(stream, large_buffer); if (ec) { std::cerr << "Error after writing " << n << " of " @@ -230,20 +236,20 @@ transferred even when returning an error. == Error Categories -Corosio uses Boost.System error codes, which support categories: +Corosio uses `std::error_code`, which supports categories: [source,cpp] ---- -if (ec.category() == boost::system::system_category()) +if (ec.category() == std::system_category()) // Operating system error -if (ec.category() == boost::system::generic_category()) +if (ec.category() == std::generic_category()) // Portable POSIX-style error - -if (ec.category() == capy::error_category()) - // Capy-specific error (eof, canceled, etc.) ---- +Capy's own errors (`eof`, `canceled`) don't expose a public category accessor; +match them by condition instead, as shown next. + == Comparing Errors Use error conditions for portable comparison: @@ -251,7 +257,7 @@ Use error conditions for portable comparison: [source,cpp] ---- // Specific error (platform-dependent) -if (ec == make_error_code(system::errc::connection_refused)) +if (ec == std::errc::connection_refused) // ... // Error condition (portable) @@ -272,9 +278,11 @@ capy::task safe_operation() { try { - (co_await sock.connect(endpoint)).value(); + auto [ec] = co_await sock.connect(endpoint); + if (ec) + throw std::system_error(ec); } - catch (boost::system::system_error const& e) + catch (std::system_error const& e) { std::cerr << "Connect failed: " << e.what() << "\n"; // Exception handled here, doesn't propagate @@ -321,5 +329,5 @@ capy::task connect_with_retry( == Next Steps -* xref:4d.sockets.adoc[Sockets] — Socket operations -* xref:4g.composed-operations.adoc[Composed Operations] — read() and write() +* xref:4.guide/4d.sockets.adoc[Sockets] — Socket operations +* xref:4.guide/4g.composed-operations.adoc[Composed Operations] — read() and write() diff --git a/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc b/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc index 3b16ae17d..d17b40066 100644 --- a/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc @@ -119,11 +119,11 @@ Corosio uses concepts from Capy: [source,cpp] ---- // Readable buffers (for writing to sockets) -template +template auto write_some(ConstBufferSequence const& buffers); // Writable buffers (for reading from sockets) -template +template auto read_some(MutableBufferSequence const& buffers); ---- @@ -139,29 +139,33 @@ std::array bufs = {...}; std::size_t total = capy::buffer_size(bufs); ---- -== consuming_buffers +== buffer_slice -The `consuming_buffers` wrapper tracks progress through a buffer sequence: +`capy::buffer_slice` returns a view over a byte range of a buffer sequence. +It exposes the bytes not yet transferred via `data()` and advances past +completed bytes with `remove_prefix(n)`: [source,cpp] ---- -#include +#include std::array bufs = { capy::mutable_buffer(header, 16), capy::mutable_buffer(body, 1024) }; -capy::consuming_buffers consuming(bufs); +auto slice = capy::buffer_slice(bufs); // After reading 20 bytes: -auto [ec, n] = co_await sock.read_some(consuming); -consuming.consume(n); // Advance by bytes read +auto [ec, n] = co_await sock.read_some(slice.data()); +slice.remove_prefix(n); // Advance past the bytes read -// Now consuming represents the remaining unread portion +// slice.data() now represents the remaining unread portion ---- This is used internally by `read()` and `write()` but can be used directly. +The underlying sequence must outlive the slice and any sequence obtained from +its `data()`. == buffer_param @@ -169,7 +173,7 @@ The `buffer_param` class type-erases buffer sequences: [source,cpp] ---- -#include +#include void accept_any_buffer(corosio::buffer_param buffers) { @@ -284,11 +288,11 @@ struct packet_header capy::task read_header(corosio::io_stream& stream) { packet_header header; - auto [ec, n] = co_await corosio::read( + auto [ec, n] = co_await capy::read( stream, capy::mutable_buffer(&header, sizeof(header))); if (ec) - throw boost::system::system_error(ec); + throw std::system_error(ec); return header; } @@ -296,6 +300,6 @@ capy::task read_header(corosio::io_stream& stream) == Next Steps -* xref:4g.composed-operations.adoc[Composed Operations] — Using buffers with read/write -* xref:4d.sockets.adoc[Sockets] — Socket I/O operations +* xref:4.guide/4g.composed-operations.adoc[Composed Operations] — Using buffers with read/write +* xref:4.guide/4d.sockets.adoc[Sockets] — Socket I/O operations * xref:../3.tutorials/3a.echo-server.adoc[Echo Server Tutorial] — Practical usage diff --git a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc index c24c1aee0..866c3fb65 100644 --- a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc +++ b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc @@ -200,8 +200,11 @@ std::string_view path = ep.path(); bool bound = !ep.empty(); ---- -The maximum path length is 107 bytes (the `sun_path` field in `sockaddr_un` -minus the null terminator). Paths longer than this throw +The maximum path length is 103 bytes. This is the portable minimum across +platforms: the `sun_path` field in `sockaddr_un` is 108 bytes on Linux but +only 104 on macOS and FreeBSD. Corosio uses a 104-byte buffer with a 103-char +limit (one byte reserved for the null terminator) so a `local_endpoint` is +portable across all three. Paths longer than this throw `std::errc::filename_too_long`. === Abstract Sockets (Linux Only) @@ -249,22 +252,25 @@ assert(ep.is_abstract()); | Platform | All platforms -| POSIX only (Linux, macOS, BSD) +| Linux, macOS, BSD, Windows (stream); POSIX only for datagram |=== == Platform Support -Unix domain sockets are available on all POSIX platforms: +Unix domain **stream** sockets and acceptors are available on every supported +platform: * **Linux** — Full support including abstract sockets * **macOS** — Full support (no abstract sockets) * **FreeBSD** — Full support (no abstract sockets) +* **Windows** — Stream sockets and acceptors via IOCP (AF_UNIX, Windows 10 + 1803 and later; no abstract sockets) -Windows has limited AF_UNIX support (since Windows 10 1803) but Corosio -does not currently support Unix sockets on Windows. +Unix domain **datagram** sockets are POSIX only — they are not available on +Windows. == Next Steps -* xref:4d.sockets.adoc[TCP Sockets] — TCP socket operations -* xref:4e.tcp-acceptor.adoc[TCP Acceptors] — TCP listener operations -* xref:4f.endpoints.adoc[IP Endpoints] — IP address and port endpoints +* xref:4.guide/4d.sockets.adoc[TCP Sockets] — TCP socket operations +* xref:4.guide/4e.tcp-acceptor.adoc[TCP Acceptors] — TCP listener operations +* xref:4.guide/4f.endpoints.adoc[IP Endpoints] — IP address and port endpoints diff --git a/doc/modules/ROOT/pages/4.guide/4q.udp.adoc b/doc/modules/ROOT/pages/4.guide/4q.udp.adoc index 47bb77e83..d4c0bb500 100644 --- a/doc/modules/ROOT/pages/4.guide/4q.udp.adoc +++ b/doc/modules/ROOT/pages/4.guide/4q.udp.adoc @@ -173,9 +173,7 @@ If the socket is not yet open when `connect()` is called, it is opened automatically using the address family of the destination endpoint. This makes a connect-then-send client a two-line affair. -You can call `connect()` again at any time to switch peers, or call it with -an unspecified endpoint (`AF_UNSPEC`) on platforms that support it to -dissolve the association. +You can call `connect()` again at any time to switch peers. Connected mode is useful for two reasons: @@ -249,7 +247,7 @@ Options commonly relevant to UDP: platforms; enable for IPv6-only services. |=== -See xref:4d.sockets.adoc[Sockets] for the full list of generic socket +See xref:4.guide/4d.sockets.adoc[Sockets] for the full list of generic socket options. == Multicast @@ -290,8 +288,8 @@ Related options: | Set the multicast TTL (IPv4) / hop limit (IPv6). Default is `1` — datagrams stay on the local subnet unless you raise it. -| `multicast_interface_v6` -| Choose the outgoing interface for IPv6 multicast. +| `multicast_interface_v4` / `multicast_interface_v6` +| Choose the outgoing interface for IPv4 / IPv6 multicast. |=== == Cancellation @@ -318,7 +316,7 @@ ss.request_stop(); // unblocks any in-flight recv_from inside my_task For portable error comparison, check against `capy::cond::canceled` rather than a platform-specific `errc` value. See -xref:4m.error-handling.adoc[Error Handling]. +xref:4.guide/4m.error-handling.adoc[Error Handling]. == Concurrent Operations @@ -370,6 +368,6 @@ cost in the kernel, so a server can run several receivers in parallel. * xref:2.networking-tutorial/2g.udp.adoc[UDP: Fast, Simple, Unreliable] — protocol-level background -* xref:4d.sockets.adoc[Sockets] — generic socket options and lifetime rules -* xref:4f.endpoints.adoc[Endpoints] — IP address and port construction -* xref:4m.error-handling.adoc[Error Handling] — the `io_result` pattern +* xref:4.guide/4d.sockets.adoc[Sockets] — generic socket options and lifetime rules +* xref:4.guide/4f.endpoints.adoc[Endpoints] — IP address and port construction +* xref:4.guide/4m.error-handling.adoc[Error Handling] — the `io_result` pattern diff --git a/doc/modules/ROOT/pages/4.guide/4r.wait.adoc b/doc/modules/ROOT/pages/4.guide/4r.wait.adoc index 81f535b11..edb12839c 100644 --- a/doc/modules/ROOT/pages/4.guide/4r.wait.adoc +++ b/doc/modules/ROOT/pages/4.guide/4r.wait.adoc @@ -143,6 +143,6 @@ platforms. == See Also -* xref:4d.sockets.adoc[Sockets] -* xref:4e.tcp-acceptor.adoc[Acceptors] -* xref:4q.udp.adoc[UDP Sockets] +* xref:4.guide/4d.sockets.adoc[Sockets] +* xref:4.guide/4e.tcp-acceptor.adoc[Acceptors] +* xref:4.guide/4q.udp.adoc[UDP Sockets] diff --git a/doc/modules/ROOT/pages/5.testing/5.intro.adoc b/doc/modules/ROOT/pages/5.testing/5.intro.adoc index 26d737fb7..cb9b7c32c 100644 --- a/doc/modules/ROOT/pages/5.testing/5.intro.adoc +++ b/doc/modules/ROOT/pages/5.testing/5.intro.adoc @@ -21,12 +21,12 @@ that make thorough testing of I/O code practical and repeatable. == What's in this section -* xref:5a.mocket.adoc[Mock Sockets] — `mocket`, `make_mocket_pair`, and +* xref:5.testing/5a.mocket.adoc[Mock Sockets] — `mocket`, `make_mocket_pair`, and the staging API for byte-level deterministic tests. -* xref:5b.socket-pair.adoc[Socket Pairs] — `make_socket_pair` for tests +* xref:5.testing/5b.socket-pair.adoc[Socket Pairs] — `make_socket_pair` for tests that need real socket semantics (TLS, `set_option`, `shutdown`, true EOF). -* xref:5c.patterns.adoc[Testing Patterns] — recipes that combine the two +* xref:5.testing/5c.patterns.adoc[Testing Patterns] — recipes that combine the two facilities. == Choosing the right tool @@ -43,5 +43,5 @@ ordering, EOF | `socket_pair` | Combine the two (e.g., framing on top of TLS over a real socket) -| See xref:5c.patterns.adoc[Patterns] +| See xref:5.testing/5c.patterns.adoc[Patterns] |=== diff --git a/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc index c0e943b6d..926f042cc 100644 --- a/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc +++ b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc @@ -95,6 +95,9 @@ expect buffer. If they match, the matched prefix is consumed; if they don't, `write_some` returns `capy::error::test_failure` and writes zero bytes. Once the expect buffer is empty, subsequent writes pass through. +On a match, the reported byte count is the full size of the write (capped +at `max_write_size`), not the length of the matched prefix. + == Chunked I/O `make_mocket_pair` accepts `max_read_size` and `max_write_size` to cap @@ -143,6 +146,11 @@ Always call `close()` at the end of a test that uses `provide` / `expect` and assert that the result is empty. This is what catches "the test passed because the code under test did nothing." +The leftover-data check only runs on the *first* `close()` of a +still-open mocket. `close()` on an already-closed mocket returns success +without inspecting the staging buffers, so the verifying `close()` must be +the first one. + == Templated over Socket `basic_mocket` is a template; the default alias only specializes it for @@ -174,7 +182,16 @@ corosio::tcp_socket& under = m.socket(); // Pass `under` into a TLS stream, a custom framing layer, etc. ---- -See xref:5c.patterns.adoc[Testing Patterns] for a TLS-over-mocket +[IMPORTANT] +==== +I/O performed through the returned raw socket *bypasses* the mocket's +`provide()` / `expect()` scripting. Those stages run only inside the +mocket's own `read_some` / `write_some`; a stream built on `socket()` +talks to the underlying socket directly. To script bytes for a +higher-level stream, drive the mocket directly instead. +==== + +See xref:5.testing/5c.patterns.adoc[Testing Patterns] for a TLS-over-mocket example. == Thread Safety @@ -196,9 +213,9 @@ example. == Next Steps -* xref:5b.socket-pair.adoc[Socket Pairs] — when you need real socket +* xref:5.testing/5b.socket-pair.adoc[Socket Pairs] — when you need real socket semantics instead of staged bytes. -* xref:5c.patterns.adoc[Testing Patterns] — recipes that combine mocket, +* xref:5.testing/5c.patterns.adoc[Testing Patterns] — recipes that combine mocket, socket_pair, and chunked I/O. * xref:../4.guide/4d.sockets.adoc[Sockets Guide] — the underlying socket interface. diff --git a/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc b/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc index 2f994ab42..2bb93e852 100644 --- a/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc +++ b/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc @@ -12,7 +12,7 @@ `make_socket_pair` creates two `tcp_socket` objects connected via loopback TCP. Use it when a test needs real socket semantics — TLS handshakes, `set_option`, `shutdown` ordering, true EOF — that the -byte-level staging in xref:5a.mocket.adoc[`mocket`] cannot reproduce. +byte-level staging in xref:5.testing/5a.mocket.adoc[`mocket`] cannot reproduce. [NOTE] ==== @@ -122,7 +122,7 @@ auto [s1, s2] = corosio::test::make_socket_pair< For tests that need byte-level determinism *and* a real socket underneath (e.g., framing on top of TLS), see the layering recipe in -xref:5c.patterns.adoc[Testing Patterns]. +xref:5.testing/5c.patterns.adoc[Testing Patterns]. == Caveats @@ -138,8 +138,8 @@ xref:5c.patterns.adoc[Testing Patterns]. == Next Steps -* xref:5a.mocket.adoc[Mock Sockets] — for byte-level deterministic +* xref:5.testing/5a.mocket.adoc[Mock Sockets] — for byte-level deterministic tests. -* xref:5c.patterns.adoc[Testing Patterns] — recipes that combine both. +* xref:5.testing/5c.patterns.adoc[Testing Patterns] — recipes that combine both. * xref:../4.guide/4l.tls.adoc[TLS Encryption] — common consumer of `make_socket_pair`. diff --git a/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc b/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc index 3eb986da1..30a1c401e 100644 --- a/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc +++ b/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc @@ -9,8 +9,8 @@ = Testing Patterns -This page collects recipes that combine xref:5a.mocket.adoc[`mocket`] -and xref:5b.socket-pair.adoc[`make_socket_pair`] in realistic test +This page collects recipes that combine xref:5.testing/5a.mocket.adoc[`mocket`] +and xref:5.testing/5b.socket-pair.adoc[`make_socket_pair`] in realistic test scenarios. Each recipe is a small standalone block; copy and adapt. [NOTE] @@ -92,9 +92,14 @@ The same applies to `max_write_size` for write-loop testing. == Layering Streams on a Mocket -`m.socket()` returns the underlying `tcp_socket`. Stack any stream that -wraps a TCP socket on top of it; the staging buffers still apply at the -TCP layer, with the higher-level stream's wire format flowing through: +`m.socket()` returns the underlying `tcp_socket`. You can stack any stream +that wraps a TCP socket on top of it, but be aware that doing so *bypasses* +the mocket's `provide()` / `expect()` scripting: those stages run only +inside the mocket's own `read_some` / `write_some`. A stream built on +`m.socket()` calls the underlying socket's `read_some` / `write_some` +directly, so staged bytes do not apply. If you need to script bytes for a +higher-level stream, drive the mocket directly instead of layering on +`m.socket()`: [source,cpp] ---- @@ -102,7 +107,7 @@ auto [m, peer] = corosio::test::make_mocket_pair(ioc); // Pass m.socket() into a TLS stream or other layer in production code: corosio::tcp_socket& under = m.socket(); -// e.g., openssl_stream tls(&under, tls_ctx); +// e.g., openssl_stream tls(&under, tls_ctx); ---- This is the right tool when the bytes you want to stage are below a @@ -148,9 +153,15 @@ auto ec = m.close(); This is the line that catches "the test passed because the code under test silently did nothing." Treat it as a test-suite convention. +The leftover-data check only runs on the *first* `close()` of a +still-open mocket. If the mocket is already closed, `close()` returns +success without inspecting the staging buffers, so leftover `provide()` / +`expect()` data goes undetected. Make the verifying `close()` the first +one. + == See Also -* xref:5a.mocket.adoc[Mock Sockets] -* xref:5b.socket-pair.adoc[Socket Pairs] +* xref:5.testing/5a.mocket.adoc[Mock Sockets] +* xref:5.testing/5b.socket-pair.adoc[Socket Pairs] * xref:../4.guide/4d.sockets.adoc[Sockets Guide] * xref:../4.guide/4l.tls.adoc[TLS Encryption] diff --git a/doc/modules/ROOT/pages/glossary.adoc b/doc/modules/ROOT/pages/glossary.adoc index 5e6569acf..7a257eaa1 100644 --- a/doc/modules/ROOT/pages/glossary.adoc +++ b/doc/modules/ROOT/pages/glossary.adoc @@ -18,8 +18,9 @@ An I/O object that listens for and accepts incoming TCP connections. See `corosio::tcp_acceptor` and xref:4.guide/4e.tcp-acceptor.adoc[Acceptors Guide]. Affine Awaitable:: -An awaitable type that implements the affine protocol, receiving a dispatcher -parameter in `await_suspend` to ensure correct executor affinity. +An awaitable type that implements the affine protocol, receiving an +`io_env const*` in `await_suspend` (carrying the executor, stop token, and +frame allocator) to ensure correct executor affinity. Affinity:: The binding of a coroutine to a specific executor. A coroutine with affinity @@ -29,9 +30,9 @@ buffer_param:: A type-erased buffer sequence parameter, allowing non-template code to work with any buffer type. -any_dispatcher:: -A type-erased wrapper for dispatchers, enabling runtime polymorphism for -executor types. +any_executor:: +A type-erased wrapper for executors, enabling runtime polymorphism for +executor types. See `capy::any_executor`. Awaitable:: A type that can be used with `co_await`. Must provide `await_ready()`, @@ -59,7 +60,7 @@ with an error. Composed Operation:: An operation built from multiple primitive operations. For example, -`corosio::read()` repeatedly calls `read_some()` until the buffer is full. +`capy::read()` repeatedly calls `read_some()` until the buffer is full. Concurrency Hint:: A value passed to `io_context` indicating how many threads may call `run()`. @@ -77,12 +78,6 @@ Coroutine Handle:: A low-level handle to a suspended coroutine, represented by `std::coroutine_handle<>`. -== D - -Dispatcher:: -An object that can dispatch coroutine handles for execution. Satisfies the -`capy::dispatcher` concept. - == E Endpoint:: @@ -91,14 +86,14 @@ See `corosio::endpoint`. EOF (End of File/Stream):: A condition indicating no more data is available. Signaled by -`capy::error::eof`. +`capy::cond::eof`. Error Category:: -A grouping of related error codes. Boost.System uses categories to -distinguish different error sources. +A grouping of related error codes. The standard library uses categories +to distinguish different error sources (system, generic, and capy). Error Code:: -A lightweight error indicator. See `boost::system::error_code`. +A lightweight error indicator. See `std::error_code`. Error Condition:: A portable error classification. Enables comparing errors across categories. @@ -108,8 +103,8 @@ An environment where work runs. Provides service management and an executor. See `capy::execution_context`. Executor:: -An object that can dispatch work for execution. Satisfies the `capy::executor` -concept. +An object that can dispatch or post coroutine handles and work for execution. +Satisfies the `capy::Executor` concept. == H @@ -174,7 +169,10 @@ outstanding work exists. == P Platform Backend:: -The operating system-specific implementation (IOCP, io_uring, kqueue). +The operating system-specific I/O implementation. Corosio provides five: +epoll, kqueue, IOCP, io_uring, and select. The default selection is IOCP +(Windows), epoll (Linux), kqueue (BSD/macOS), and select (fallback); io_uring +is available but not selected by default. Poll:: Processing ready work without blocking. See `io_context::poll()`. @@ -218,9 +216,11 @@ Stop Token:: A mechanism for requesting cancellation. See `std::stop_token`. Strand:: -A serialization mechanism that ensures handlers don't run concurrently. -Operations posted to a strand execute one at a time, eliminating data -races without mutexes. See xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming]. +A serialization mechanism that ensures handlers don't run concurrently, so +operations execute one at a time, eliminating data races without mutexes. +Corosio does not provide a strand type; the equivalent is executor affinity +or a single-threaded `io_context`. +See xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming]. Stream:: A sequence of bytes that can be read or written incrementally. diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index c950e36d2..29ce277a3 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -23,13 +23,15 @@ executor without manual dispatch. * **tcp_socket** — Asynchronous TCP socket with connect, read, and write * **tcp_acceptor** — TCP listener for accepting incoming connections * **tcp_server** — Server framework with worker pools -* **udp_socket** — Asynchronous UDP socket for datagrams +* **udp_socket** — Asynchronous UDP socket for datagrams (including multicast) * **local_stream_socket** — Unix domain stream socket for local IPC +* **local_stream_acceptor** — Unix domain stream listener * **local_datagram_socket** — Unix domain datagram socket for local IPC * **resolver** — Asynchronous DNS resolution * **timer** — Asynchronous timer for delays and timeouts * **signal_set** — Asynchronous signal handling -* **wolfssl_stream** — TLS encryption using WolfSSL +* **stream_file** / **random_access_file** — Asynchronous file I/O +* **openssl_stream** / **wolfssl_stream** — TLS encryption (OpenSSL or WolfSSL) == What This Library Does Not Do @@ -38,7 +40,7 @@ Corosio focuses on coroutine-first I/O primitives. It does not include: * General-purpose executor abstractions (use Boost.Capy) * The sender/receiver execution model (P2300) * HTTP, WebSocket, or other application protocols (use Boost.Http or Boost.Beast2) -* UDP multicast or raw sockets +* Raw sockets Corosio works with Boost.Capy for task management and execution contexts. @@ -52,8 +54,8 @@ parameters, not through thread-local storage. When an I/O operation completes, it resumes your coroutine through the dispatcher you provided. **Structured bindings.** Results use `io_result` which supports structured -bindings: `auto [ec, n] = co_await s.read_some(buf)`. Call `.value()` to throw -on error instead. +bindings: `auto [ec, n] = co_await s.read_some(buf)`. `io_result` has no +`value()`; to throw on error, check `ec` and `throw std::system_error(ec)`. **Type erasure at I/O boundaries.** Socket implementations use type-erased dispatchers internally. The indirection cost is negligible compared to I/O @@ -74,7 +76,7 @@ and xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] for back == Requirements * C++20 compiler with coroutine support -* Boost libraries: Capy, URL, System +* Boost libraries: Capy === Tested Compilers @@ -125,7 +127,7 @@ capy::task connect_example(corosio::io_context& ioc) // Connect using structured bindings auto [ec] = co_await s.connect( - corosio::endpoint(boost::urls::ipv4_address::loopback(), 8080)); + corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); if (ec) { diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index 6993878c2..cbc3b3e70 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -21,6 +21,7 @@ Code snippets assume: #include #include #include +#include namespace corosio = boost::corosio; namespace capy = boost::capy; @@ -53,39 +54,48 @@ Derive from it and define a worker class: [source,cpp] ---- -class echo_server : public corosio::tcp_server +class worker : public corosio::tcp_server::worker_base { - class worker : public worker_base + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf_; + +public: + worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf_; + buf_.reserve(4096); + } - public: - worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf_.reserve(4096); - } + corosio::tcp_socket& socket() override { return sock_; } - corosio::tcp_socket& socket() override { return sock_; } + void run(corosio::tcp_server::launcher launch) override + { + launch(ctx_.get_executor(), do_session()); + } - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_session()); - } + capy::task<> do_session(); +}; - capy::task<> do_session(); - }; +// Build a pool of workers as a range of unique_ptr. +inline auto +make_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} +class echo_server : public corosio::tcp_server +{ public: echo_server(corosio::io_context& ctx, int max_workers) : tcp_server(ctx, ctx.get_executor()) { - wv_.reserve(max_workers); - for (int i = 0; i < max_workers; ++i) - wv_.emplace(ctx); + set_workers(make_workers(ctx, max_workers)); } }; ---- @@ -102,7 +112,7 @@ The session coroutine reads data and echoes it back: [source,cpp] ---- -capy::task<> echo_server::worker::do_session() +capy::task<> worker::do_session() { for (;;) { @@ -118,7 +128,7 @@ capy::task<> echo_server::worker::do_session() buf_.resize(n); // Echo it back - auto [wec, wn] = co_await corosio::write( + auto [wec, wn] = co_await capy::write( sock_, capy::const_buffer(buf_.data(), buf_.size())); if (wec) @@ -132,7 +142,7 @@ capy::task<> echo_server::worker::do_session() Key points: * `read_some()` returns when _any_ data is available -* `write()` (the free function) writes _all_ data or fails +* `capy::write()` (the free function) writes _all_ data or fails * Structured bindings extract the error code and byte count * When the coroutine ends, the worker automatically returns to the pool @@ -192,12 +202,12 @@ if (ec) [source,cpp] ---- -auto n = (co_await sock.read_some(buf)).value(); -// Throws system_error if read fails +auto [ec, n] = co_await sock.read_some(buf); +if (ec) throw std::system_error(ec); // Throws if read fails ---- -The `.value()` method throws `boost::system::system_error` if the operation -failed. +Checking `ec` and throwing `std::system_error(ec)` is the explicit exception +style for handling a failed operation. == Next Steps diff --git a/include/boost/corosio/io/io_stream.hpp b/include/boost/corosio/io/io_stream.hpp index 5eb3bfe58..afe389216 100644 --- a/include/boost/corosio/io/io_stream.hpp +++ b/include/boost/corosio/io/io_stream.hpp @@ -59,11 +59,11 @@ namespace boost::corosio { while( total < buf.size() ) { auto [ec, n] = co_await stream.read_some( - capy::buffer( buf.data() + total, buf.size() - total ) ); + capy::mutable_buffer( buf.data() + total, buf.size() - total ) ); if( ec == capy::cond::eof ) break; - if( ec.failed() ) - capy::detail::throw_system_error( ec ); + if( ec ) + throw std::system_error( ec ); total += n; } } diff --git a/include/boost/corosio/resolver.hpp b/include/boost/corosio/resolver.hpp index 062022e2e..e702fe5b6 100644 --- a/include/boost/corosio/resolver.hpp +++ b/include/boost/corosio/resolver.hpp @@ -183,8 +183,10 @@ operator&=(reverse_flags& a, reverse_flags b) noexcept for (auto const& entry : results) std::cout << entry.get_endpoint().port() << std::endl; - // Or using exceptions - auto results = (co_await r.resolve("www.example.com", "https")).value(); + // Or, to convert errors into exceptions: + auto [ec2, results2] = co_await r.resolve("www.example.com", "https"); + if (ec2) + throw std::system_error(ec2); @endcode */ class BOOST_COROSIO_DECL resolver : public io_object diff --git a/include/boost/corosio/tcp_acceptor.hpp b/include/boost/corosio/tcp_acceptor.hpp index 841fefceb..c2e37d6f6 100644 --- a/include/boost/corosio/tcp_acceptor.hpp +++ b/include/boost/corosio/tcp_acceptor.hpp @@ -466,17 +466,17 @@ class BOOST_COROSIO_DECL tcp_acceptor : public io_object Returns the local address and port to which the acceptor is bound. This is useful when binding to port 0 (ephemeral port) to discover - the OS-assigned port number. The endpoint is cached when listen() + the OS-assigned port number. The endpoint is cached when bind() is called. @return The local endpoint, or a default endpoint (0.0.0.0:0) if - the acceptor is not listening. + the acceptor is not open. @par Thread Safety - The cached endpoint value is set during listen() and cleared + The cached endpoint value is set during bind() and cleared during close(). This function may be called concurrently with accept operations, but must not be called concurrently with - listen() or close(). + bind() or close(). */ endpoint local_endpoint() const noexcept; diff --git a/include/boost/corosio/tcp_server.hpp b/include/boost/corosio/tcp_server.hpp index 9fdce0a51..b0fd02b9c 100644 --- a/include/boost/corosio/tcp_server.hpp +++ b/include/boost/corosio/tcp_server.hpp @@ -728,16 +728,20 @@ class BOOST_COROSIO_DECL tcp_server /** Stop accepting connections. - Signals all listening ports to stop accepting new connections - and requests cancellation of active workers via their stop tokens. - + Requests the accept loops' stop token and requests cancellation + of active workers via their stop tokens. The acceptors are not + closed; a suspended accept completes once more before its loop + observes the stop token and ends. + This function returns immediately; it does not wait for workers to finish. Pending I/O operations complete asynchronously. Calling `stop()` on a non-running server has no effect. @par Effects - - Closes all acceptors (pending accepts complete with error). + - Requests stop on the accept loops' stop token. The acceptors + are not closed; a pending accept completes once more before + the accept loop ends. - Requests stop on each active worker's stop token. - Workers observing their stop token should exit promptly. diff --git a/include/boost/corosio/timer.hpp b/include/boost/corosio/timer.hpp index 7f8ac6660..ecd43d1dc 100644 --- a/include/boost/corosio/timer.hpp +++ b/include/boost/corosio/timer.hpp @@ -42,9 +42,10 @@ namespace boost::corosio { Shared objects: Unsafe. @par Semantics - Wraps platform timer facilities via the io_context reactor. - Operations dispatch to OS timer APIs (timerfd, IOCP timers, - kqueue EVFILT_TIMER). + Timers are not backed by per-timer kernel objects. The io_context's + timer service keeps a process-side min-heap of pending expirations; + the nearest expiry drives the reactor's poll timeout, and expirations + are processed in the run loop. */ class BOOST_COROSIO_DECL timer : public io_timer { diff --git a/include/boost/corosio/tls_context.hpp b/include/boost/corosio/tls_context.hpp index 2149df29d..510fd851a 100644 --- a/include/boost/corosio/tls_context.hpp +++ b/include/boost/corosio/tls_context.hpp @@ -175,7 +175,7 @@ tls_context_data const& get_tls_context_data(tls_context const&) noexcept; ctx.set_hostname( "example.com" ); // Use with a TLS stream - corosio::openssl_stream secure( sock, ctx ); + corosio::openssl_stream secure( &sock, ctx ); co_await secure.handshake( corosio::tls_stream::client ); @endcode @@ -405,6 +405,11 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the bundle could not be parsed or the passphrase is incorrect. + @note Not yet implemented in this release; returns + `std::errc::function_not_supported`. Load the certificate + and key separately via `use_certificate_chain()` and + `use_private_key()` instead. + @see use_pkcs12_file */ std::error_code @@ -424,6 +429,11 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the file could not be read, parsed, or the passphrase is incorrect. + @note Not yet implemented in this release; returns + `std::errc::function_not_supported`. Load the certificate + and key separately via `use_certificate_chain_file()` and + `use_private_key_file()` instead. + @par Example @code ctx.use_pkcs12_file( "credentials.pfx", "secret" ); @@ -484,6 +494,11 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the directory is invalid. + @note Not yet applied by the backends in this release; the + directory is accepted but never loaded. Use + `load_verify_file()` or `add_certificate_authority()` to + supply trust anchors. + @par Example @code ctx.add_verify_path( "/etc/ssl/certs" ); @@ -508,6 +523,13 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the system store could not be loaded. + @note Not yet applied by the backends in this release. This is a + no-op: the OS trust store is never loaded, so a client that + relies on it has an empty trust store and cannot verify a + public server. To verify a peer today, supply CA certificates + explicitly via `load_verify_file()` or + `add_certificate_authority()`. + @par Example @code // Trust the same CAs as the system @@ -533,6 +555,10 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the version is not supported by the backend. + @note Not yet applied by the backends in this release; the bound + is accepted but has no effect. The negotiated range is + whatever the native default method provides. + @par Example @code // Require TLS 1.3 minimum @@ -553,6 +579,10 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the version is not supported by the backend. + @note Not yet applied by the backends in this release; the bound + is accepted but has no effect. The negotiated range is + whatever the native default method provides. + @see set_min_protocol_version */ std::error_code set_max_protocol_version(tls_version v); @@ -575,6 +605,9 @@ class BOOST_COROSIO_DECL tls_context @note For TLS 1.3, use `set_ciphersuites_tls13()` on backends that distinguish between TLS 1.2 and 1.3 cipher configuration. + + @note Applied by the OpenSSL backend only in this release; the + WolfSSL backend accepts the string but silently ignores it. */ std::error_code set_ciphersuites(std::string_view ciphers); @@ -591,6 +624,9 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if ALPN configuration fails. + @note Not yet applied by the backends in this release; the + protocol list is accepted but ALPN is never negotiated. + @par Example @code // Prefer HTTP/2, fall back to HTTP/1.1 @@ -655,6 +691,11 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the callback could not be set. + @note Not yet implemented in this release. This template is + declared but not defined; code that instantiates it fails to + link. Use `set_verify_mode()` with explicitly supplied trust + anchors instead. + @note The `verify_context` type provides access to the certificate and chain information. Its exact interface depends on the TLS backend. @@ -740,6 +781,9 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the CRL could not be parsed. + @note Not yet applied by the backends in this release; the CRL is + accepted but never used during verification. + @see add_crl_file @see set_revocation_policy */ @@ -755,6 +799,9 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the file could not be read or the CRL is invalid. + @note Not yet applied by the backends in this release; the CRL is + accepted but never used during verification. + @par Example @code ctx.add_crl_file( "issuer.crl" ); @@ -779,6 +826,9 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the response is invalid. + @note Not yet applied by the backends in this release; the + response is accepted but never stapled into the handshake. + @note This is a server-side operation. Clients use `set_require_ocsp_staple()` to require stapled responses. */ @@ -793,6 +843,10 @@ class BOOST_COROSIO_DECL tls_context @param require Whether to require OCSP stapling. + @note Not yet applied by the backends in this release; the flag + is accepted but has no effect. Setting it to `true` does not + make the handshake fail when no staple is present. + @note Not all servers support OCSP stapling. Enable this only when connecting to servers known to support it. */ @@ -814,6 +868,10 @@ class BOOST_COROSIO_DECL tls_context ctx.set_revocation_policy( tls_revocation_policy::soft_fail ); @endcode + @note Not yet applied by the backends in this release; the policy + is accepted but has no effect. `soft_fail` and `hard_fail` do + not change verification behavior. + @see tls_revocation_policy @see add_crl */