diff --git a/doc/qbk/02_examples/_examples.qbk b/doc/qbk/02_examples/_examples.qbk index 81b26ca5..c5889a8b 100644 --- a/doc/qbk/02_examples/_examples.qbk +++ b/doc/qbk/02_examples/_examples.qbk @@ -43,7 +43,7 @@ used to evaluate robustness. All asynchronous clients support timeouts. ][ [WebSocket, C++20 coroutine] [[path_link example/http/client/awaitable/http_client_awaitable.cpp http_client_awaitable.cpp]] - [] + [[path_link example/http/client/awaitable-ssl/http_client_awaitable_ssl.cpp http_client_awaitable_ssl.cpp]] ][ [HTTP crawl (asynchronous)] [[path_link example/http/client/crawl/http_crawl.cpp http_crawl.cpp]] diff --git a/example/http/client/CMakeLists.txt b/example/http/client/CMakeLists.txt index 82e57e9b..5b08610e 100644 --- a/example/http/client/CMakeLists.txt +++ b/example/http/client/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory (body) if (OPENSSL_FOUND) add_subdirectory (async-ssl) add_subdirectory (async-ssl-system-executor) + add_subdirectory (awaitable-ssl) add_subdirectory (coro-ssl) add_subdirectory (sync-ssl) endif() diff --git a/example/http/client/Jamfile b/example/http/client/Jamfile index d45dbe6f..cfcced01 100644 --- a/example/http/client/Jamfile +++ b/example/http/client/Jamfile @@ -15,6 +15,7 @@ build-project awaitable ; # SSL build-project async-ssl ; +build-project awaitable-ssl ; build-project coro-ssl ; build-project sync-ssl ; diff --git a/example/http/client/awaitable-ssl/CMakeLists.txt b/example/http/client/awaitable-ssl/CMakeLists.txt new file mode 100644 index 00000000..62146ff2 --- /dev/null +++ b/example/http/client/awaitable-ssl/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +GroupSources(include/boost/beast beast) +GroupSources(example/http/client/awaitable "/") + +add_executable (http-client-awaitable-ssl + ${BOOST_BEAST_FILES} + Jamfile + http_client_awaitable_ssl.cpp + ) + +target_link_libraries(http-client-awaitable-ssl + OpenSSL::SSL OpenSSL::Crypto + lib-asio + lib-asio-ssl + lib-beast) + +set_property(TARGET http-client-awaitable PROPERTY FOLDER "example-http-client") diff --git a/example/http/client/awaitable-ssl/Jamfile b/example/http/client/awaitable-ssl/Jamfile new file mode 100644 index 00000000..7a3b8266 --- /dev/null +++ b/example/http/client/awaitable-ssl/Jamfile @@ -0,0 +1,22 @@ +# +# Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +import ac ; + +project + : requirements + [ ac.check-library /boost/beast//lib-asio-ssl : /boost/beast//lib-asio-ssl/static : no ] + ; + +exe http-client-awaitable-ssl : + http_client_awaitable_ssl.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/http/client/awaitable-ssl/http_client_awaitable_ssl.cpp b/example/http/client/awaitable-ssl/http_client_awaitable_ssl.cpp new file mode 100644 index 00000000..8ace8b93 --- /dev/null +++ b/example/http/client/awaitable-ssl/http_client_awaitable_ssl.cpp @@ -0,0 +1,191 @@ +// +// Copyright (c) 2022 Klemens D. Morgenstern (klemens dot morgenstern at gmx dot net) +// +// 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/boostorg/beast +// + +//------------------------------------------------------------------------------ +// +// Example: HTTP client, coroutine +// +//------------------------------------------------------------------------------ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +#include +#include +#include +#include +#include "example/common/root_certificates.hpp" + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +namespace ssl = boost::asio::ssl; // from +using tcp = boost::asio::ip::tcp; // from + +//------------------------------------------------------------------------------ + +// Performs an HTTP GET and prints the response +net::awaitable +do_session( + std::string host, + std::string port, + std::string target, + int version, + ssl::context& ctx) +{ + // These objects perform our I/O + // They use an executor with a default completion token of use_awaitable + // This makes our code easy, but will use exceptions as the default error handling, + // i.e. if the connection drops, we might see an exception. + // See async_shutdown for error handling with an error_code. + auto resolver = net::use_awaitable.as_default_on(tcp::resolver(co_await net::this_coro::executor)); + using executor_with_default = net::use_awaitable_t<>::executor_with_default; + using tcp_stream = typename beast::tcp_stream::rebind_executor::other; + + // We construct the ssl stream from the already rebound tcp_stream. + beast::ssl_stream stream{ + net::use_awaitable.as_default_on(beast::tcp_stream(co_await net::this_coro::executor)), + ctx}; + + // Set SNI Hostname (many hosts need this to handshake successfully) + if(! SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) + throw boost::system::system_error(static_cast(::ERR_get_error()), net::error::get_ssl_category()); + + // Look up the domain name + auto const results = co_await resolver.async_resolve(host, port); + + // Set the timeout. + beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + co_await beast::get_lowest_layer(stream).async_connect(results); + + // Set the timeout. + beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); + + // Perform the SSL handshake + co_await stream.async_handshake(ssl::stream_base::client); + + // Set up an HTTP GET request message + http::request req{http::verb::get, target, version}; + req.set(http::field::host, host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // Set the timeout. + beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); + + // Send the HTTP request to the remote host + co_await http::async_write(stream, req); + + // This buffer is used for reading and must be persisted + beast::flat_buffer b; + + // Declare a container to hold the response + http::response res; + + // Receive the HTTP response + co_await http::async_read(stream, b, res); + + // Write the message to standard out + std::cout << res << std::endl; + + // Set the timeout. + beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(30)); + + // Gracefully close the stream - do not threat every error as an exception! + auto [ec] = co_await stream.async_shutdown(net::as_tuple(net::use_awaitable)); + if (ec == net::error::eof) + { + // Rationale: + // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error + ec = {}; + } + if (ec) + throw boost::system::system_error(ec, "shutdown"); + + // If we get here then the connection is closed gracefully +} + +//------------------------------------------------------------------------------ + +int main(int argc, char** argv) +{ + // Check command line arguments. + if(argc != 4 && argc != 5) + { + std::cerr << + "Usage: http-client-awaitable []\n" << + "Example:\n" << + " http-client-awaitable www.example.com 443 /\n" << + " http-client-awaitable www.example.com 443 / 1.0\n"; + return EXIT_FAILURE; + } + auto const host = argv[1]; + auto const port = argv[2]; + auto const target = argv[3]; + int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11; + + // The io_context is required for all I/O + net::io_context ioc; + + // The SSL context is required, and holds certificates + ssl::context ctx{ssl::context::tlsv12_client}; + + // This holds the root certificate used for verification + load_root_certificates(ctx); + + // Verify the remote server's certificate + ctx.set_verify_mode(ssl::verify_peer); + + + // Launch the asynchronous operation + net::co_spawn( + ioc, + do_session(host, port, target, version, ctx), + // If the awaitable exists with an exception, it gets delivered here as `e`. + // This can happen for regular errors, such as connection drops. + [](std::exception_ptr e) + { + if (!e) + return ; + try + { + std::rethrow_exception(e); + } + catch(std::exception & ex) + { + std::cerr << "Error: " << ex.what() << "\n"; + } + }); + + // Run the I/O service. The call will return when + // the get operation is complete. + ioc.run(); + + return EXIT_SUCCESS; +} + +#else + +int main(int, char * []) +{ + std::printf("awaitables require C++20\n"); + return 1; +} + +#endif diff --git a/example/http/client/awaitable/http_client_awaitable.cpp b/example/http/client/awaitable/http_client_awaitable.cpp index 93c0fa22..85675716 100644 --- a/example/http/client/awaitable/http_client_awaitable.cpp +++ b/example/http/client/awaitable/http_client_awaitable.cpp @@ -37,13 +37,6 @@ using tcp = boost::asio::ip::tcp; // from //------------------------------------------------------------------------------ -// Report a failure -void -fail(beast::error_code ec, char const* what) -{ - std::cerr << what << ": " << ec.message() << "\n"; -} - // Performs an HTTP GET and prints the response net::awaitable do_session( @@ -53,7 +46,9 @@ do_session( int version) { // These objects perform our I/O - + // They use an executor with a default completion token of use_awaitable + // This makes our code easy, but will use exceptions as the default error handling, + // i.e. if the connection drops, we might see an exception. auto resolver = net::use_awaitable.as_default_on(tcp::resolver(co_await net::this_coro::executor)); auto stream = net::use_awaitable.as_default_on(beast::tcp_stream(co_await net::this_coro::executor)); @@ -125,20 +120,23 @@ int main(int argc, char** argv) net::io_context ioc; // Launch the asynchronous operation - net::co_spawn(ioc, - do_session(host, port, target, version), - [](std::exception_ptr e) - { - if (e) - try - { - std::rethrow_exception(e); - } - catch(std::exception & e) - { - std::cerr << "Error: " << e.what() << "\n"; - } - }); + net::co_spawn( + ioc, + do_session(host, port, target, version), + // If the awaitable exists with an exception, it gets delivered here as `e`. + // This can happen for regular errors, such as connection drops. + [](std::exception_ptr e) + { + if (e) + try + { + std::rethrow_exception(e); + } + catch(std::exception & ex) + { + std::cerr << "Error: " << ex.what() << "\n"; + } + }); // Run the I/O service. The call will return when // the get operation is complete.