diff --git a/README.md b/README.md index 1cff616..872abfc 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ One line to make a request. cross-origin hops. - **Content encodings** — transparent `gzip`, `deflate`, and `br` decoding when the corresponding decode service is installed. -- **Cookies** — RFC 6265 jar with optional public-suffix validation (libpsl). +- **Cookies** — RFC 6265bis jar with optional public-suffix validation (libpsl). - **Authentication** — Basic and Bearer, per client or per request. - **Proxies** — `http`, `socks5`, with credentials. - **Timeouts** — connect, per-I/O, and whole-operation, overridable per diff --git a/cmake/FindLibpsl.cmake b/cmake/FindLibpsl.cmake index ed685d4..0947c60 100644 --- a/cmake/FindLibpsl.cmake +++ b/cmake/FindLibpsl.cmake @@ -4,7 +4,7 @@ # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -# Official repository: https://github.com/cppalliance/beast2 +# Official repository: https://github.com/cppalliance/burl # # Provides imported targets: diff --git a/doc/modules/ROOT/pages/2.guide/2a.making-requests.adoc b/doc/modules/ROOT/pages/2.guide/2a.making-requests.adoc index 29b8b29..ed9f2ff 100644 --- a/doc/modules/ROOT/pages/2.guide/2a.making-requests.adoc +++ b/doc/modules/ROOT/pages/2.guide/2a.making-requests.adoc @@ -66,7 +66,7 @@ configure the request and return the builder again, so calls chain: [source,cpp] ---- -auto r = co_await client.get("https://example.com/search") +auto [ec, r] = co_await client.get("https://example.com/search") .query("q", "boost") .header(http::field::accept, "application/json") .timeout(std::chrono::seconds(10)) diff --git a/doc/modules/ROOT/pages/2.guide/2e.headers.adoc b/doc/modules/ROOT/pages/2.guide/2e.headers.adoc index b4e4ed5..5a13944 100644 --- a/doc/modules/ROOT/pages/2.guide/2e.headers.adoc +++ b/doc/modules/ROOT/pages/2.guide/2e.headers.adoc @@ -31,7 +31,7 @@ single request. It comes in two overloads: [source,cpp] ---- -auto r = co_await client.get("https://example.com") +auto [ec, r] = co_await client.get("https://example.com") .header(http::field::accept_language, "en") .header("X-Trace-Id", "abc123") .send(); @@ -47,7 +47,7 @@ name on the client: client.headers().set(http::field::accept, "application/json"); // this one request asks for XML instead -auto r = co_await client.get("https://example.com") +auto [ec, r] = co_await client.get("https://example.com") .header(http::field::accept, "application/xml") .send(); ---- diff --git a/doc/modules/ROOT/pages/2.guide/2f.query-parameters.adoc b/doc/modules/ROOT/pages/2.guide/2f.query-parameters.adoc index 5b5e698..7579ee2 100644 --- a/doc/modules/ROOT/pages/2.guide/2f.query-parameters.adoc +++ b/doc/modules/ROOT/pages/2.guide/2f.query-parameters.adoc @@ -21,7 +21,7 @@ query string, percent-encoding both: [source,cpp] ---- -auto r = co_await client.get("https://example.com/search") +auto [ec, r] = co_await client.get("https://example.com/search") .query("category", "shoes") .query("color", "blue") .send(); @@ -55,7 +55,7 @@ cpp:request_builder::query[] appends to whatever the URL already carries: [source,cpp] ---- -auto r = co_await client.get("https://example.com/search?category=shoes") +auto [ec, r] = co_await client.get("https://example.com/search?category=shoes") .query("color", "blue") .send(); // GET /search?category=shoes&color=blue diff --git a/doc/modules/ROOT/pages/2.guide/2h.redirects.adoc b/doc/modules/ROOT/pages/2.guide/2h.redirects.adoc index ee67ab5..0f0e8d0 100644 --- a/doc/modules/ROOT/pages/2.guide/2h.redirects.adoc +++ b/doc/modules/ROOT/pages/2.guide/2h.redirects.adoc @@ -40,7 +40,7 @@ auto [ec, r] = co_await client.get("https://example.com/old") .followlocation(false) .send(); -std::cout << r.status_int() << '\n'; // e.g. 301 +std::cout << r.status_int() << '\n'; // e.g. 301 std::cout << r.headers().value_or(http::field::location, "") << '\n'; ---- diff --git a/doc/modules/ROOT/pages/2.guide/2i.cookies.adoc b/doc/modules/ROOT/pages/2.guide/2i.cookies.adoc index e449683..5dcf6a3 100644 --- a/doc/modules/ROOT/pages/2.guide/2i.cookies.adoc +++ b/doc/modules/ROOT/pages/2.guide/2i.cookies.adoc @@ -27,8 +27,8 @@ cfg.cookies = true; burl::client client(co_await capy::this_coro::executor, tls_ctx, cfg); // the cookie set here is stored, and returned on the next request to the host -co_await client.get("https://example.com/login").send(); -auto r = co_await client.get("https://example.com/account").send(); +auto [ec1, r1] = co_await client.get("https://example.com/login").send(); +auto [ec2, r2] = co_await client.get("https://example.com/account").send(); ---- == The Cookie Jar diff --git a/doc/modules/ROOT/pages/2.guide/2j.timeouts.adoc b/doc/modules/ROOT/pages/2.guide/2j.timeouts.adoc index 0bd8e94..e39fbfd 100644 --- a/doc/modules/ROOT/pages/2.guide/2j.timeouts.adoc +++ b/doc/modules/ROOT/pages/2.guide/2j.timeouts.adoc @@ -30,8 +30,9 @@ whole-operation timeout can be overridden per request. | cpp:client::config[config::timeout] | The whole operation, from connecting through receipt of the response - headers. Whatever time is left then bounds reading the body in place. Off by - default. + headers. Whatever time is left then bounds a whole-body read + (cpp:response::as[as] or cpp:response::as_view[as_view]), but not the + streaming sources you drive yourself. Off by default. |=== They address different failure modes and are meant to be combined. The connect @@ -48,7 +49,7 @@ cpp:client::config[config::timeout] for one request: [source,cpp] ---- -auto r = co_await client.get("https://example.com/report") +auto [ec, r] = co_await client.get("https://example.com/report") .timeout(std::chrono::seconds(3)) .send(); ---- @@ -68,4 +69,4 @@ if(ec == capy::error::timeout) * xref:2.guide/2d.error-handling.adoc[] — Timeouts among transport failures * xref:2.guide/2k.connection-pool.adoc[] — The idle timeout, a separate setting -* xref:2.guide/2c.responses.adoc[] — Which body reads the timeout covers +* xref:2.guide/2c.responses.adoc[] — The body-reading methods in detail diff --git a/doc/modules/ROOT/pages/2.guide/2l.proxies.adoc b/doc/modules/ROOT/pages/2.guide/2l.proxies.adoc index 7a4db5e..e3ae992 100644 --- a/doc/modules/ROOT/pages/2.guide/2l.proxies.adoc +++ b/doc/modules/ROOT/pages/2.guide/2l.proxies.adoc @@ -25,7 +25,7 @@ cfg.proxy = urls::url("socks5h://user:pass@localhost:1080"); burl::client client(co_await capy::this_coro::executor, tls_ctx, cfg); // established through the proxy -auto r = co_await client.get("https://example.com").send(); +auto [ec, r] = co_await client.get("https://example.com").send(); ---- == Supported Schemes diff --git a/doc/modules/ROOT/pages/2.guide/2n.extending.adoc b/doc/modules/ROOT/pages/2.guide/2n.extending.adoc index f3faf00..3d94908 100644 --- a/doc/modules/ROOT/pages/2.guide/2n.extending.adoc +++ b/doc/modules/ROOT/pages/2.guide/2n.extending.adoc @@ -23,7 +23,7 @@ cpp:RequestBody[] concept: [source,cpp] ---- -struct RequestBody +struct MyRequestBody { std::optional content_type() const; std::optional content_length() const; @@ -52,7 +52,7 @@ like any built-in: [source,cpp] ---- -auto r = co_await client.post("https://example.com/post") +auto [ec, r] = co_await client.post("https://example.com/post") .body({ { "user", "John" } }) .send(); ---- diff --git a/include/boost/burl/client.hpp b/include/boost/burl/client.hpp index 7deef4e..189b746 100644 --- a/include/boost/burl/client.hpp +++ b/include/boost/burl/client.hpp @@ -202,10 +202,13 @@ class client within this duration, from connection establishment through receipt of the response headers. The remaining time - also applies to reading the body with - @ref response::try_as_view and - @ref response::as_view. Can be - overridden per request with + then bounds a whole-body read with + @ref response::as or + @ref response::as_view (and their + `try_` forms), but not the streaming + sources @ref response::as_buffer_source + and @ref response::as_read_source. Can + be overridden per request with @ref request_builder::timeout. */ std::optional timeout; @@ -395,9 +398,7 @@ class client The jar stores cookies received in responses and supplies them for subsequent requests - when @ref config::cookies is enabled. It can - be persisted and restored using its stream - operators. + when @ref config::cookies is enabled. */ burl::cookie_jar& cookie_jar() noexcept @@ -409,9 +410,7 @@ class client The jar stores cookies received in responses and supplies them for subsequent requests - when @ref config::cookies is enabled. It can - be persisted and restored using its stream - operators. + when @ref config::cookies is enabled. */ const burl::cookie_jar& cookie_jar() const noexcept diff --git a/include/boost/burl/cookie.hpp b/include/boost/burl/cookie.hpp index ffb7fb8..19ba708 100644 --- a/include/boost/burl/cookie.hpp +++ b/include/boost/burl/cookie.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance/beast2 +// Official repository: https://github.com/cppalliance/burl // #ifndef BOOST_BURL_COOKIE_HPP @@ -32,8 +32,8 @@ namespace burl stored in a @ref cookie_jar. @par Specification - @li HTTP State Management Mechanism (rfc6265) + @li Cookies: HTTP State Management Mechanism (rfc6265bis) @see @ref parse_cookie, @@ -143,8 +143,8 @@ struct cookie @endcode @par Specification - @li 5.2. The Set-Cookie Header (rfc6265) + @li 5.6. The Set-Cookie Header Field (rfc6265bis) @param sv The string to parse. diff --git a/include/boost/burl/cookie_jar.hpp b/include/boost/burl/cookie_jar.hpp index 5943f28..cd4ae27 100644 --- a/include/boost/burl/cookie_jar.hpp +++ b/include/boost/burl/cookie_jar.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance/beast2 +// Official repository: https://github.com/cppalliance/burl // #ifndef BOOST_BURL_COOKIE_JAR_HPP @@ -44,7 +44,7 @@ namespace burl burl::client c(co_await capy::this_coro::executor, tls_ctx, cfg); - auto r = co_await c.get("https://example.com/login").send(); + auto [ec, r] = co_await c.get("https://example.com/login").send(); // Print the stored cookies in Netscape format std::cout << c.cookie_jar().to_netscape(); @@ -125,7 +125,7 @@ class cookie_jar by domain, path, and the `Secure` attribute, and returned as `name=value` pairs separated by `"; "`, ordered with longer paths first - (RFC 6265bis 5.4). Expired cookies encountered + (RFC 6265bis 5.8). Expired cookies encountered during matching are removed from the jar. @param url The URL of the request. diff --git a/include/boost/burl/file.hpp b/include/boost/burl/file.hpp index ee283b7..f2751bd 100644 --- a/include/boost/burl/file.hpp +++ b/include/boost/burl/file.hpp @@ -38,7 +38,7 @@ namespace burl @par Example @code - auto r = co_await c.put("https://example.com/put") + auto [ec, r] = co_await c.put("https://example.com/put") .body("./report.log") .send(); @endcode diff --git a/include/boost/burl/multipart_form.hpp b/include/boost/burl/multipart_form.hpp index 5392a13..731b11e 100644 --- a/include/boost/burl/multipart_form.hpp +++ b/include/boost/burl/multipart_form.hpp @@ -44,7 +44,7 @@ namespace burl form.file("attachment", "./crash_report.log"); form.text("priority", "high"); - auto r = co_await c.post("https://example.com/post") + auto [ec, r] = co_await c.post("https://example.com/post") .body(form) .send(); @endcode diff --git a/include/boost/burl/request_builder.hpp b/include/boost/burl/request_builder.hpp index 2c63f52..531681c 100644 --- a/include/boost/burl/request_builder.hpp +++ b/include/boost/burl/request_builder.hpp @@ -88,7 +88,7 @@ class request_builder @par Example @code - auto r = co_await c.get("https://example.com/get") + auto [ec, r] = co_await c.get("https://example.com/get") .query("category", "shoes") .query("color", "blue") .send(); @@ -237,7 +237,7 @@ class request_builder @par Example @code - auto r = co_await c.post("https://example.com/post") + auto [ec, r] = co_await c.post("https://example.com/post") .body(json::value({ "key", "value" })) .send(); @endcode diff --git a/include/boost/burl/string.hpp b/include/boost/burl/string.hpp index 69ac74d..2ed58ba 100644 --- a/include/boost/burl/string.hpp +++ b/include/boost/burl/string.hpp @@ -35,7 +35,7 @@ namespace burl @par Example @code - auto r = co_await c.post(url) + auto [ec, r] = co_await c.post(url) .body(std::string("payload")) .send(); @endcode @@ -77,7 +77,7 @@ tag_invoke(body_from_tag, std::string_view body); @par Example @code - auto r = co_await c.post(url) + auto [ec, r] = co_await c.post(url) .body("payload") .send(); @endcode diff --git a/include/boost/burl/urlencoded_form.hpp b/include/boost/burl/urlencoded_form.hpp index a554b86..630bd8f 100644 --- a/include/boost/burl/urlencoded_form.hpp +++ b/include/boost/burl/urlencoded_form.hpp @@ -36,7 +36,7 @@ namespace burl @par Example @code - auto r = co_await c.post("https://example.com/post") + auto [ec, r] = co_await c.post("https://example.com/post") .body(burl::urlencoded_form() .append("user", "John") .append("lang", "En")) @@ -101,7 +101,7 @@ class urlencoded_form { "user", "John" }, { "lang", "En" } }; - auto r = co_await c.post("https://example.com/post") + auto [ec, r] = co_await c.post("https://example.com/post") .body(fields) .send(); @endcode diff --git a/src/cookie.cpp b/src/cookie.cpp index 01cae56..d327ed6 100644 --- a/src/cookie.cpp +++ b/src/cookie.cpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance/beast2 +// Official repository: https://github.com/cppalliance/burl // #include diff --git a/src/cookie_jar.cpp b/src/cookie_jar.cpp index 4eb3bf7..dc6aa10 100644 --- a/src/cookie_jar.cpp +++ b/src/cookie_jar.cpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance/beast2 +// Official repository: https://github.com/cppalliance/burl // #include @@ -57,7 +57,7 @@ domain_match( bool path_match(core::string_view r_path, core::string_view c_path) noexcept { - // RFC 6265 5.1.4: an empty request path defaults to "/" + // RFC 6265bis 5.1.4: an empty request path defaults to "/" if(r_path.empty()) r_path = "/"; @@ -208,13 +208,13 @@ cookie_jar::add(const urls::url_view& url, cookie c) auto& c_domain = c.domain.value(); normalize_host(c_domain); - // RFC 6265 5.2.3: a leading dot in the Domain attribute is ignored + // RFC 6265bis 5.6.3: a leading dot in the Domain attribute is ignored if(c_domain.starts_with('.')) c_domain.erase(0, 1); if(is_public_suffix(c_domain)) { - // RFC 6265 5.3 step 5: a public-suffix Domain is rejected, unless + // RFC 6265bis 5.7 step 9: a public-suffix Domain is rejected, unless // it equals the request host, which makes the cookie host-only. if(c_domain != r_host) return; @@ -277,7 +277,7 @@ cookie_jar::add(const urls::url_view& url, cookie c) return; } - // RFC 6265bis 5.3: replacing keeps the old cookie's position so creation + // RFC 6265bis 5.7 step 23: replacing keeps the old cookie's position so creation // order (used for header ordering) is retained. if(it != cookies_.end()) *it = std::move(c); @@ -315,7 +315,7 @@ cookie_jar::cookie_header(const urls::url_view& url) ++it; } - // RFC 6265 5.4: longer paths first; stable_sort keeps creation order as + // RFC 6265bis 5.8: longer paths first; stable_sort keeps creation order as // the tiebreaker. std::stable_sort( matched.begin(), diff --git a/src/detail/util.cpp b/src/detail/util.cpp index 98d46b9..ddd9a23 100644 --- a/src/detail/util.cpp +++ b/src/detail/util.cpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance/beast2 +// Official repository: https://github.com/cppalliance/burl // #include "util.hpp" diff --git a/src/multipart_form.cpp b/src/multipart_form.cpp index 87311b7..ded3a06 100644 --- a/src/multipart_form.cpp +++ b/src/multipart_form.cpp @@ -175,36 +175,30 @@ class multipart_form::body capy::io_task<> write(capy::any_buffer_sink& sink) const { + using capy::make_buffer; + for(auto const& p : form_.parts_) { - if(auto [ec, n] = co_await sink.write(capy::make_buffer(p.header)); - ec) + if(auto [ec, n] = co_await sink.write(make_buffer(p.header)); ec) co_return { ec }; if(p.is_file) { - if(auto [ec] = co_await detail::send_file(sink, p.path, p.size); - ec) + if(auto [ec] = co_await detail::send_file(sink, p.path, p.size); ec) co_return { ec }; } else { - if(auto [ec, n] = - co_await sink.write(capy::make_buffer(p.text)); - ec) + if(auto [ec, n] = co_await sink.write(make_buffer(p.text)); ec) co_return { ec }; } - if(auto [ec, n] = co_await sink.write( - capy::make_buffer(std::string_view("\r\n"))); - ec) + if(auto [ec, n] = co_await sink.write(make_buffer("\r\n", 2)); ec) co_return { ec }; } - auto trailer = "--" + form_.boundary_ + "--\r\n"; - if(auto [ec, n] = co_await sink.write( - capy::make_buffer(std::string_view(trailer))); - ec) + auto const trailer = "--" + form_.boundary_ + "--\r\n"; + if(auto [ec, n] = co_await sink.write(make_buffer(trailer)); ec) co_return { ec }; co_return {}; diff --git a/test/unit/cookie_jar.cpp b/test/unit/cookie_jar.cpp index a7b37c5..7e8fcc1 100644 --- a/test/unit/cookie_jar.cpp +++ b/test/unit/cookie_jar.cpp @@ -77,7 +77,7 @@ struct cookie_jar_test BOOST_TEST_EQ(jar.cookie_header(url), ""); } - // RFC 6265 5.2.3: a leading dot is ignored — accepted on a domain, + // RFC 6265bis 5.6.3: a leading dot is ignored — accepted on a domain, // rejected on a bare TLD. { cookie_jar jar; @@ -131,7 +131,7 @@ struct cookie_jar_test urls::url("https://example.com/app"), parse_cookie("k=1; Path=/app").value()); - // RFC 6265 5.1.4: the cookie path is a prefix ending on a boundary. + // RFC 6265bis 5.1.4: the cookie path is a prefix ending on a boundary. BOOST_TEST_EQ(jar.cookie_header(urls::url("https://example.com/app")), "k=1"); BOOST_TEST_EQ( jar.cookie_header(urls::url("https://example.com/app/x")), "k=1"); @@ -140,11 +140,11 @@ struct cookie_jar_test BOOST_TEST_EQ( jar.cookie_header(urls::url("https://example.com/application")), ""); - // RFC 6265 5.1.4: a no-path request defaults to "/", not matching /app. + // RFC 6265bis 5.1.4: a no-path request defaults to "/", not matching /app. BOOST_TEST_EQ( jar.cookie_header(urls::url("https://example.com")), ""); - // RFC 6265 5.1.4: the defaulted "/" does match a root cookie. + // RFC 6265bis 5.1.4: the defaulted "/" does match a root cookie. cookie_jar root; root.add( urls::url("https://example.com/"), parse_cookie("a=1").value()); @@ -155,7 +155,7 @@ struct cookie_jar_test void testOrdering() { - // RFC 6265 5.4: cookies with longer paths come first. + // RFC 6265bis 5.8: cookies with longer paths come first. { cookie_jar jar; jar.add( @@ -183,7 +183,7 @@ struct cookie_jar_test BOOST_TEST_EQ(jar.cookie_header(url), "a=1; b=2; c=3"); } - // RFC 6265 5.3: an updated cookie keeps its original position. + // RFC 6265bis 5.7: an updated cookie keeps its original position. { cookie_jar jar; urls::url url("https://example.com/"); @@ -330,7 +330,7 @@ struct cookie_jar_test BOOST_TEST_EQ(jar.cookie_header(url), "a=1"); } - // RFC 6265 5.1.3: suffix matching does not apply to IP hosts, so a + // RFC 6265bis 5.1.3: suffix matching does not apply to IP hosts, so a // cookie set on one address is not sent to a different address that // shares a textual suffix. { @@ -426,7 +426,7 @@ struct cookie_jar_test void testPublicSuffixHostOnly() { - // RFC 6265 5.3 step 5: a public-suffix Domain equal to the host is + // RFC 6265bis 5.7 step 9: a public-suffix Domain equal to the host is // accepted as host-only (a bare label is a public suffix either way). { cookie_jar jar; diff --git a/test/unit/detail/connection_pool.cpp b/test/unit/detail/connection_pool.cpp index 292e557..3b2970d 100644 --- a/test/unit/detail/connection_pool.cpp +++ b/test/unit/detail/connection_pool.cpp @@ -10,10 +10,20 @@ // Test that header file is self-contained. #include "src/detail/connection_pool.hpp" +#include +#include #include +#include +#include +#include +#include #include #include #include +#include +#include +#include +#include #include #include "scripted_net.hpp" @@ -26,7 +36,29 @@ namespace burl namespace detail { +namespace +{ + +constexpr char const* server_cert = + "-----BEGIN CERTIFICATE-----\n" + "MIIBPjCB8aADAgECAhQyKw1vpx1dMpM1RZvVFSZk8CblDDAFBgMrZXAwFDESMBAG\n" + "A1UEAwwJbG9jYWxob3N0MCAXDTI2MDYxOTE4MDczMVoYDzIxMjYwNTI2MTgwNzMx\n" + "WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwKjAFBgMrZXADIQD6Ie6FvisdU/00Erdj\n" + "Kz3XgJ+aVBHnnNwkg2JXScdwvKNTMFEwHQYDVR0OBBYEFMbHkWhq1pG06IKlS0dK\n" + "VLFb8sQwMB8GA1UdIwQYMBaAFMbHkWhq1pG06IKlS0dKVLFb8sQwMA8GA1UdEwEB\n" + "/wQFMAMBAf8wBQYDK2VwA0EA5y94Q4N61m5ISAhPt2f80Z/9oU/3sxyOZuovbawi\n" + "HwYpdjOgATpTJ3ZeuA5tL3rwQ6+ATVR4O43zzGy6bPs2Cw==\n" + "-----END CERTIFICATE-----\n"; + +constexpr char const* server_key = + "-----BEGIN PRIVATE KEY-----\n" + "MC4CAQAwBQYDK2VwBCIEICAS/dH8KdK3z1Z9ju+7WkYMc35VIhaYsq57d32GdW+l\n" + "-----END PRIVATE KEY-----\n"; + +} // namespace + using namespace std::chrono_literals; +using capy::make_buffer; class connection_pool_test { @@ -47,6 +79,72 @@ class connection_pool_test } }; + class loopback_server + { + corosio::io_context& ioc_; + corosio::tcp_acceptor acceptor_{ ioc_ }; + + public: + explicit loopback_server(corosio::io_context& ioc) + : ioc_{ ioc } + { + acceptor_.open(); + acceptor_.set_option( + corosio::socket_option::reuse_address(true)); + if(auto ec = acceptor_.bind({})) + throw std::system_error(ec); + if(auto ec = acceptor_.listen()) + throw std::system_error(ec); + } + + urls::url + url(std::string_view scheme) const + { + urls::url u; + u.set_host_ipv4(urls::ipv4_address::loopback()); + u.set_port_number(acceptor_.local_endpoint().port()); + u.set_scheme(scheme); + return u; + } + + capy::task + next() + { + corosio::tcp_socket peer{ ioc_ }; + auto [ec] = co_await acceptor_.accept(peer); + BOOST_TEST(!ec); + co_return std::move(peer); + } + }; + + static capy::task<> + ping(capy::any_stream s) + { + auto [wec, wn] = co_await capy::write( + s, make_buffer("ping", 4)); + BOOST_TEST(!wec); + + char buf[4]; + auto [rec, rn] = co_await capy::read( + s, make_buffer(buf)); + BOOST_TEST(!rec); + BOOST_TEST_EQ(std::string_view(buf, 4), "pong"); + } + + static capy::task<> + pong(capy::any_stream s) + { + char buf[4]; + auto [rec, rn] = co_await capy::read( + s, make_buffer(buf)); + BOOST_TEST(!rec); + BOOST_TEST_EQ(std::string_view(buf, 4), "ping"); + + auto [wec, wn] = co_await capy::write( + s, make_buffer("pong", 4)); + BOOST_TEST(!wec); + } + public: void testOriginKeySeparation() @@ -286,16 +384,316 @@ class connection_pool_test char buf[1] = {}; - auto [rec, n1] = co_await pc.read_some(capy::make_buffer(buf)); + auto [rec, n1] = co_await pc.read_some(make_buffer(buf)); BOOST_TEST(rec == capy::error::timeout); BOOST_TEST_EQ(n1, 0); - auto [wec, n2] = co_await pc.write_some(capy::make_buffer(buf)); + auto [wec, n2] = co_await pc.write_some(make_buffer(buf)); BOOST_TEST(wec == capy::error::timeout); BOOST_TEST_EQ(n2, 0); }()); } + void + testTcpConnectionReuse() + { + corosio::io_context ioc; + loopback_server server{ ioc }; + + auto server_task = [&]() -> capy::task<> + { + auto s = co_await server.next(); + co_await pong(&s); + co_await pong(&s); + }; + + auto client_task = [&]() -> capy::task<> + { + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context{}, + client::config{}); + + for(auto i : { 0, 1 }) + { + auto [aec, pc] = co_await pool->acquire(server.url("http")); + BOOST_TEST(!aec); + co_await ping(&pc); + pool->release(std::move(pc)); + } + }; + + capy::run_async(ioc.get_executor())(server_task()); + capy::run_async(ioc.get_executor())(client_task()); + ioc.run(); + } + + void + testTcpConnectionRefused() + { + corosio::io_context ioc; + + // Reserve an ephemeral port, then drop the listener. + urls::url url; + { + loopback_server server{ ioc }; + url = server.url("http"); + } + + auto client_task = [&]() -> capy::task<> + { + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context(), + client::config{}); + + auto [aec, pc] = co_await pool->acquire(url); + BOOST_TEST(aec); + BOOST_TEST(!static_cast(pc)); + pool->release(std::move(pc)); + }; + + capy::run_async(ioc.get_executor())(client_task()); + ioc.run(); + } + + void + testTlsConnectionReuse() + { + corosio::io_context ioc; + loopback_server server{ ioc }; + + corosio::tls_context tls_ctx; + BOOST_TEST(!tls_ctx.use_certificate( + server_cert, corosio::tls_file_format::pem)); + BOOST_TEST(!tls_ctx.use_private_key( + server_key, corosio::tls_file_format::pem)); + BOOST_TEST(!tls_ctx.set_verify_mode( + corosio::tls_verify_mode::none)); + + auto server_task = [&]() -> capy::task<> + { + corosio::openssl_stream s{ co_await server.next(), tls_ctx }; + auto [hec] = co_await s.handshake( + corosio::openssl_stream::server); + BOOST_TEST(!hec); + + co_await pong(&s); + co_await pong(&s); + + // TODO: tls shutdown + }; + + auto client_task = [&]() -> capy::task<> + { + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context{}, + client::config{}); + + for(auto i : { 0, 1 }) + { + auto [aec, pc] = co_await pool->acquire(server.url("https")); + BOOST_TEST(!aec); + co_await ping(&pc); + pool->release(std::move(pc)); + } + }; + + capy::run_async(ioc.get_executor())(server_task()); + capy::run_async(ioc.get_executor())(client_task()); + ioc.run(); + } + + void + testTlsHandshakeFailure() + { + corosio::io_context ioc; + loopback_server server{ ioc }; + + auto server_task = [&]() -> capy::task<> + { + auto peer = co_await server.next(); + peer.close(); // the client handshake should fail + }; + + auto client_task = [&]() -> capy::task<> + { + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context(), + client::config{}); + + auto [ec, pc] = co_await pool->acquire(server.url("https")); + BOOST_TEST(ec); + BOOST_TEST(!static_cast(pc)); + }; + + capy::run_async(ioc.get_executor())(server_task()); + capy::run_async(ioc.get_executor())(client_task()); + ioc.run(); + } + + void + testSocks5Proxy() + { + corosio::io_context ioc; + loopback_server server{ ioc }; + + auto server_task = [&]() -> capy::task<> + { + auto s = co_await server.next(); + + // socks5 handshake + { + // greeting: VER, NMETHODS, METHODS... + std::uint8_t greeting[3]; + auto [gec, gn] = + co_await capy::read(s, make_buffer(greeting)); + BOOST_TEST(!gec); + BOOST_TEST_EQ(greeting[0], 0x05); // SOCKS5 + BOOST_TEST_EQ(greeting[1], 0x01); // one method + BOOST_TEST_EQ(greeting[2], 0x00); // no authentication + + // reply: VER, METHOD (no authentication) + std::uint8_t method[2] = { 0x05, 0x00 }; + auto [mec, mn] = co_await s.write_some(make_buffer(method)); + BOOST_TEST(!mec); + + // request head: VER, CMD, RSV, ATYP, domain length + std::uint8_t head[5]; + auto [hec, hn] = co_await capy::read(s, make_buffer(head)); + BOOST_TEST(!hec); + BOOST_TEST_EQ(head[0], 0x05); // SOCKS5 + BOOST_TEST_EQ(head[1], 0x01); // CONNECT + BOOST_TEST_EQ(head[3], 0x03); // domain name + + // request tail: domain name + port + char tail[256 + 2]; + auto [tec, tn] = co_await capy::read( + s, make_buffer(tail, head[4] + 2u)); + BOOST_TEST(!tec); + BOOST_TEST_EQ(std::string_view(tail, head[4]), "example.com"); + BOOST_TEST_EQ(tail[head[4]], 0x00); // port hi + BOOST_TEST_EQ(tail[head[4] + 1], 0x50); // 80 + + // reply success: VER, REP, RSV, ATYP, BND.ADDR, BND.PORT + std::uint8_t reply[10] = { + 0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0 }; + auto [rec, rn] = co_await s.write_some(make_buffer(reply)); + BOOST_TEST(!rec); + } + + co_await pong(&s); + co_await pong(&s); + }; + + auto client_task = [&]() -> capy::task<> + { + client::config cfg{}; + cfg.proxy = server.url("socks5h"); + + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context{}, + std::move(cfg)); + + for(auto i : { 0, 1 }) + { + auto [aec, pc] = co_await pool->acquire("http://example.com"); + BOOST_TEST(!aec); + co_await ping(&pc); + pool->release(std::move(pc)); + } + }; + + capy::run_async(ioc.get_executor())(server_task()); + capy::run_async(ioc.get_executor())(client_task()); + ioc.run(); + } + + void + testHttpProxy() + { + corosio::io_context ioc; + loopback_server server{ ioc }; + + auto server_task = [&]() -> capy::task<> + { + auto s = co_await server.next(); + + // http proxy handshake + { + // CONNECT request: read up to the end of the headers + std::string req; + auto [rec, rn] = co_await capy::read_until( + s, capy::dynamic_buffer(req), "\r\n\r\n"); + BOOST_TEST(!rec); + BOOST_TEST(req.starts_with( + "CONNECT example.com:80 HTTP/1.1\r\n")); + + // reply: tunnel established + std::string_view resp = + "HTTP/1.1 200 Connection established\r\n\r\n"; + auto [wec, wn] = co_await capy::write(s, make_buffer(resp)); + BOOST_TEST(!wec); + } + + co_await pong(&s); + co_await pong(&s); + }; + + auto client_task = [&]() -> capy::task<> + { + client::config cfg{}; + cfg.proxy = server.url("http"); + + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context{}, + std::move(cfg)); + + for(auto i : { 0, 1 }) + { + auto [aec, pc] = co_await pool->acquire("http://example.com"); + BOOST_TEST(!aec); + co_await ping(&pc); + pool->release(std::move(pc)); + } + }; + + capy::run_async(ioc.get_executor())(server_task()); + capy::run_async(ioc.get_executor())(client_task()); + ioc.run(); + } + + void + testUnsupportedProxyScheme() + { + corosio::io_context ioc; + loopback_server server{ ioc }; + + for(auto const scheme : { "ftp", "https" }) + { + capy::run_async(ioc.get_executor())([&]() -> capy::task<> + { + client::config cfg; + cfg.proxy = server.url(scheme); + auto pool = std::make_shared( + co_await capy::this_coro::executor, + corosio::tls_context(), + cfg); + + auto [ec, pc] = + co_await pool->acquire("http://example.com"); + BOOST_TEST_EQ(ec, error::unsupported_proxy_scheme); + BOOST_TEST(!pc); + }()); + ioc.run(); + ioc.restart(); + } + } + void run() { @@ -307,6 +705,13 @@ class connection_pool_test testConnectionOutlivesPool(); testConnectTimeout(); testIoTimeout(); + testTcpConnectionReuse(); + testTcpConnectionRefused(); + testTlsConnectionReuse(); + testTlsHandshakeFailure(); + testSocks5Proxy(); + testHttpProxy(); + testUnsupportedProxyScheme(); } };