aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/api_ref/tls.rst21
-rw-r--r--src/lib/tls/asio/asio_context.h4
-rw-r--r--src/lib/tls/asio/asio_stream.h63
-rw-r--r--src/tests/test_tls_stream_integration.cpp722
4 files changed, 775 insertions, 35 deletions
diff --git a/doc/api_ref/tls.rst b/doc/api_ref/tls.rst
index 74eebb0a5..fdffeda26 100644
--- a/doc/api_ref/tls.rst
+++ b/doc/api_ref/tls.rst
@@ -497,6 +497,8 @@ The full code for a TLS client using BSD sockets is in `src/cli/tls_client.cpp`
}
}
+.. _tls_server:
+
TLS Servers
----------------------------------------
@@ -1641,7 +1643,7 @@ Server Code Example
TLS Stream
----------------------------------------
-:cpp:class:`TLS::Stream` offers a Boost.Asio compatible wrapper around :cpp:class:`TLS::Client`.
+:cpp:class:`TLS::Stream` offers a Boost.Asio compatible wrapper around :cpp:class:`TLS::Client` and :cpp:class:`TLS::Server`.
It can be used as an alternative to Boost.Asio's `ssl::stream <https://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/reference/ssl__stream.html>`_ with minor adjustments to the using code.
It offers the following interface:
@@ -1654,7 +1656,7 @@ It offers the following interface:
explicit Stream(Context& context, Args&& ... args)
Construct a new TLS stream.
- The *context* parameter will be used to set up the underlying *native handle*, i.e. the :ref:`TLS::Client <tls_client>`, when :cpp:func:`handshake` is called.
+ The *context* parameter will be used to initialize the underlying *native handle*, i.e. the :ref:`TLS::Client <tls_client>` or :ref:`TLS::Server <tls_server>`, when :cpp:func:`handshake` is called.
Using code must ensure the context is kept alive for the lifetime of the stream.
The further *args* will be forwarded to the *next layer*'s constructor.
@@ -1668,7 +1670,6 @@ It offers the following interface:
.. cpp:function:: void handshake(Connection_Side side, boost::system::error_code& ec)
Set up the *native handle* and perform the TLS handshake.
- As only the client side of the stream is currently implemented, *side* should be ``Connection_Side::CLIENT``.
.. cpp:function:: void handshake(Connection_Side side)
@@ -1690,6 +1691,12 @@ It offers the following interface:
Overload of :cpp:func:`shutdown` that throws an exception if an error occurs.
+ .. cpp:function:: template <typename ShutdownHandler> \
+ void async_shutdown(ShutdownHandler&& handler)
+
+ Asynchronous variant of :cpp:func:`shutdown`.
+ The function returns immediately and calls the *handler* callback function after performing asynchronous I/O to complete the TLS shutdown.
+
.. cpp:function:: template <typename MutableBufferSequence> \
std::size_t read_some(const MutableBufferSequence& buffers, boost::system::error_code& ec)
@@ -1734,7 +1741,7 @@ It offers the following interface:
.. cpp:class:: TLS::Context
- A helper class to initialize and configure the Stream's underlying *native handle* (see :cpp:class:`TLS::Client`).
+ A helper class to initialize and configure the Stream's underlying *native handle* (see :cpp:class:`TLS::Client` and :cpp:class:`TLS::Server`).
.. cpp:function:: Context(Credentials_Manager& credentialsManager, \
RandomNumberGenerator& randomNumberGenerator, \
@@ -1750,8 +1757,10 @@ It offers the following interface:
will cause the stream to override the default implementation of the
:cpp:func:`tls_verify_cert_chain` callback.
-Stream Code Example
-^^^^^^^^^^^^^^^^^^^^
+TLS Stream Client Code Example
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The code below illustrates how to build a simple HTTPS client based on the TLS Stream and Boost.Beast. When run, it fetches the content of `https://botan.randombit.net/news.html` and prints it to stdout.
.. code-block:: cpp
diff --git a/src/lib/tls/asio/asio_context.h b/src/lib/tls/asio/asio_context.h
index e5e99e83a..e225fde6a 100644
--- a/src/lib/tls/asio/asio_context.h
+++ b/src/lib/tls/asio/asio_context.h
@@ -1,7 +1,7 @@
/*
* TLS Context
- * (C) 2018-2019 Jack Lloyd
- * 2018-2019 Hannes Rantzsch, Tim Oesterreich, Rene Meusel
+ * (C) 2018-2020 Jack Lloyd
+ * 2018-2020 Hannes Rantzsch, Tim Oesterreich, Rene Meusel
*
* Botan is released under the Simplified BSD License (see license.txt)
*/
diff --git a/src/lib/tls/asio/asio_stream.h b/src/lib/tls/asio/asio_stream.h
index 6dfd130f7..cb49f259b 100644
--- a/src/lib/tls/asio/asio_stream.h
+++ b/src/lib/tls/asio/asio_stream.h
@@ -23,6 +23,7 @@
#include <botan/tls_channel.h>
#include <botan/tls_client.h>
#include <botan/tls_magic.h>
+#include <botan/tls_server.h>
// We need to define BOOST_ASIO_DISABLE_SERIAL_PORT before any asio imports. Otherwise asio will include <termios.h>,
// which interferes with Botan's amalgamation by defining macros like 'B0' and 'FF1'.
@@ -40,8 +41,6 @@ namespace TLS {
/**
* @brief boost::asio compatible SSL/TLS stream
*
- * Currently only the TLS::Client specialization is implemented.
- *
* @tparam StreamLayer type of the next layer, usually a network socket
* @tparam ChannelT type of the native_handle, defaults to Botan::TLS::Channel, only needed for testing purposes
*/
@@ -200,7 +199,7 @@ class Stream
* The function call will block until handshaking is complete or an error occurs.
*
* @param side The type of handshaking to be performed, i.e. as a client or as a server.
- * @throws boost::system::system_error if error occured, or if the chosen Connection_Side is not available
+ * @throws boost::system::system_error if error occured
*/
void handshake(Connection_Side side)
{
@@ -221,8 +220,11 @@ class Stream
{
setup_native_handle(side, ec);
- // send client hello, which was written to the send buffer on client instantiation
- send_pending_encrypted_data(ec);
+ if(side == CLIENT)
+ {
+ // send client hello, which was written to the send buffer on client instantiation
+ send_pending_encrypted_data(ec);
+ }
while(!native_handle()->is_active() && !ec)
{
@@ -244,11 +246,10 @@ class Stream
* @param side The type of handshaking to be performed, i.e. as a client or as a server.
* @param handler The handler to be called when the handshake operation completes.
* The equivalent function signature of the handler must be: void(boost::system::error_code)
- * @throws NotImplemented if Connection_Side is not CLIENT
*/
template <typename HandshakeHandler>
auto async_handshake(Connection_Side side, HandshakeHandler&& handler) ->
- BOOST_ASIO_INITFN_RESULT_TYPE(HandshakeHandler, void(boost::system::error_code))
+ BOOST_ASIO_INITFN_RESULT_TYPE(HandshakeHandler, void(boost::system::error_code))
{
BOOST_ASIO_HANDSHAKE_HANDLER_CHECK(HandshakeHandler, handler) type_check;
@@ -492,8 +493,8 @@ class Stream
*/
template <typename ConstBufferSequence, typename WriteHandler>
auto async_write_some(const ConstBufferSequence& buffers, WriteHandler&& handler) ->
- BOOST_ASIO_INITFN_RESULT_TYPE(WriteHandler,
- void(boost::system::error_code, std::size_t))
+ BOOST_ASIO_INITFN_RESULT_TYPE(WriteHandler,
+ void(boost::system::error_code, std::size_t))
{
BOOST_ASIO_WRITE_HANDLER_CHECK(WriteHandler, handler) type_check;
@@ -529,8 +530,8 @@ class Stream
*/
template <typename MutableBufferSequence, typename ReadHandler>
auto async_read_some(const MutableBufferSequence& buffers, ReadHandler&& handler) ->
- BOOST_ASIO_INITFN_RESULT_TYPE(ReadHandler,
- void(boost::system::error_code, std::size_t))
+ BOOST_ASIO_INITFN_RESULT_TYPE(ReadHandler,
+ void(boost::system::error_code, std::size_t))
{
BOOST_ASIO_READ_HANDLER_CHECK(ReadHandler, handler) type_check;
@@ -667,28 +668,36 @@ class Stream
* Botan::TLS::Server.
*
* @param side The desired connection side (client or server)
- * @param ec Set to NotImplemented when side is SERVER - currently only CLIENT is implemented
+ * @param ec Set to indicate what error occurred, if any.
*/
template<class T = ChannelT>
typename std::enable_if<std::is_same<Channel, T>::value>::type
setup_native_handle(Connection_Side side, boost::system::error_code& ec)
{
- if(side == CLIENT)
- {
- m_native_handle = std::unique_ptr<Client>(
- new Client(m_core,
- m_context.m_session_manager,
- m_context.m_credentials_manager,
- m_context.m_policy,
- m_context.m_rng,
- m_context.m_server_info));
- }
- else
+ try_with_error_code([&]
{
- // TODO: First steps in order to support the server side of this stream would be to instantiate a
- // Botan::TLS::Server instance as the stream's native_handle and implement the handshake appropriately.
- ec = Botan::ErrorType::NotImplemented;
- }
+ if(side == CLIENT)
+ {
+ m_native_handle = std::unique_ptr<Client>(
+ new Client(m_core,
+ m_context.m_session_manager,
+ m_context.m_credentials_manager,
+ m_context.m_policy,
+ m_context.m_rng,
+ m_context.m_server_info,
+ Protocol_Version::latest_tls_version()));
+ }
+ else
+ {
+ m_native_handle = std::unique_ptr<Server>(
+ new Server(m_core,
+ m_context.m_session_manager,
+ m_context.m_credentials_manager,
+ m_context.m_policy,
+ m_context.m_rng,
+ false /* no DTLS */));
+ }
+ }, ec);
}
/** @brief Synchronously write encrypted data from the send buffer to the next layer.
diff --git a/src/tests/test_tls_stream_integration.cpp b/src/tests/test_tls_stream_integration.cpp
new file mode 100644
index 000000000..c1ade79d9
--- /dev/null
+++ b/src/tests/test_tls_stream_integration.cpp
@@ -0,0 +1,722 @@
+/*
+* TLS ASIO Stream Client-Server Interaction Test
+* (C) 2018-2020 Jack Lloyd
+* 2018-2020 Hannes Rantzsch, Rene Meusel
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include "tests.h"
+
+#if defined(BOTAN_HAS_TLS) && defined(BOTAN_HAS_TLS_ASIO_STREAM)
+
+// first version to be compatible with Networking TS (N4656) and boost::beast
+#include <boost/version.hpp>
+#if BOOST_VERSION >= 106600
+
+#include <functional>
+
+#include <botan/asio_stream.h>
+#include <botan/auto_rng.h>
+
+#include <boost/asio.hpp>
+
+#include "../cli/tls_helpers.h" // for Basic_Credentials_Manager
+
+namespace {
+
+namespace net = boost::asio;
+
+using tcp = net::ip::tcp;
+using error_code = boost::system::error_code;
+using ssl_stream = Botan::TLS::Stream<net::ip::tcp::socket>;
+using namespace std::placeholders;
+using Result = Botan_Tests::Test::Result;
+
+static const auto k_timeout = std::chrono::seconds(3);
+static const auto k_endpoints = std::vector<tcp::endpoint> {tcp::endpoint{net::ip::make_address("127.0.0.1"), 8082}};
+
+enum { max_msg_length = 512 };
+
+static std::string server_cert() { return Botan_Tests::Test::data_dir() + "/x509/certstor/cert1.crt"; }
+static std::string server_key() { return Botan_Tests::Test::data_dir() + "/x509/certstor/key01.pem"; }
+
+class Timeout_Exception : public std::runtime_error
+ {
+ using std::runtime_error::runtime_error;
+ };
+
+struct Side
+ {
+ Side()
+ : m_credentials_manager(true, ""),
+ m_ctx(m_credentials_manager, m_rng, m_session_mgr, m_policy, Botan::TLS::Server_Information()) {}
+
+ Side(const std::string& server_cert, const std::string& server_key)
+ : m_credentials_manager(m_rng, server_cert, server_key),
+ m_ctx(m_credentials_manager, m_rng, m_session_mgr, m_policy, Botan::TLS::Server_Information()) {}
+
+ virtual ~Side() {}
+
+ net::mutable_buffer buffer() { return net::buffer(m_data, max_msg_length); }
+ net::mutable_buffer buffer(size_t size) { return net::buffer(m_data, size); }
+
+ std::string message() const { return std::string(m_data); }
+
+ // This is a CompletionCondition for net::async_read().
+ // Our toy protocol always expects a single \0-terminated string.
+ std::size_t received_zero_byte(const boost::system::error_code& error,
+ std::size_t bytes_transferred)
+ {
+ if(error)
+ {
+ return 0;
+ }
+
+ if(bytes_transferred > 0 && m_data[bytes_transferred - 1] == '\0')
+ {
+ return 0;
+ }
+
+ return max_msg_length - bytes_transferred;
+ };
+
+ protected:
+ Botan::AutoSeeded_RNG m_rng;
+ Basic_Credentials_Manager m_credentials_manager;
+ Botan::TLS::Session_Manager_Noop m_session_mgr;
+ Botan::TLS::Policy m_policy;
+ Botan::TLS::Context m_ctx;
+ std::unique_ptr<ssl_stream> m_stream;
+
+ char m_data[max_msg_length];
+ };
+
+struct Result_Wrapper
+ {
+ Result_Wrapper(net::io_context& ioc, const std::string& name) : m_timer(ioc), m_result(name) {}
+
+ Result& result() { return m_result; }
+
+ void set_timer(const std::string& msg)
+ {
+ m_timer.expires_after(k_timeout);
+ m_timer.async_wait([this, msg](const error_code &ec)
+ {
+ if(ec != net::error::operation_aborted) // timer cancelled
+ {
+ m_result.test_failure(m_result.who() + ": timeout in " + msg);
+ throw Timeout_Exception(m_result.who());
+ }
+ });
+ }
+
+ void stop_timer()
+ {
+ m_timer.cancel();
+ }
+
+ void expect_success(const std::string& msg, const error_code& ec)
+ {
+ error_code success;
+ expect_ec(msg, success, ec);
+ }
+
+ void expect_ec(const std::string& msg, const error_code& expected, const error_code& ec)
+ {
+ if(ec != expected)
+ { m_result.test_failure(msg, "Unexpected error code: " + ec.message()); }
+ else
+ { m_result.test_success(msg); }
+ }
+
+ void confirm(const std::string& msg, bool condition)
+ {
+ m_result.confirm(msg, condition);
+ }
+
+ void test_failure(const std::string& msg)
+ {
+ m_result.test_failure(msg);
+ }
+
+ private:
+ net::system_timer m_timer;
+ Result m_result;
+ };
+
+class Server : public Side, public std::enable_shared_from_this<Server>
+ {
+ public:
+ Server(net::io_context& ioc)
+ : Side(server_cert(), server_key()),
+ m_acceptor(ioc),
+ m_result(ioc, "Server"),
+ m_short_read_expected(false) {}
+
+ // Control messages
+ // The messages below can be used by the test clients in order to configure the server's behavior during a test
+ // case.
+ //
+ // Tell the server that the next read should result in a StreamTruncated error
+ std::string expect_short_read_message = "SHORT_READ";
+ // Prepare the server for the test case "Shutdown No Response"
+ std::string prepare_shutdown_no_response_message = "SHUTDOWN_NOW";
+
+ void listen()
+ {
+ error_code ec;
+ const auto endpoint = k_endpoints.back();
+
+ m_acceptor.open(endpoint.protocol(), ec);
+ m_acceptor.set_option(net::socket_base::reuse_address(true), ec);
+ m_acceptor.bind(endpoint, ec);
+ m_acceptor.listen(net::socket_base::max_listen_connections, ec);
+
+ m_result.expect_success("listen", ec);
+
+ m_result.set_timer("accept");
+ m_acceptor.async_accept(std::bind(&Server::start_session, shared_from_this(), _1, _2));
+ }
+
+ void expect_short_read()
+ {
+ m_short_read_expected = true;
+ }
+
+ Result result() { return m_result.result(); }
+
+ private:
+ void start_session(const error_code& ec, tcp::socket socket)
+ {
+ // Note: If this fails with 'Operation canceled', it likely means the timer expired and the port is taken.
+ m_result.expect_success("accept", ec);
+
+ // Note: If this was a real server, we should create a new session (with its own stream) for each accepted
+ // connection. In this test we only have one connection.
+ m_stream = std::unique_ptr<ssl_stream>(new ssl_stream(std::move(socket), m_ctx));
+
+ m_result.set_timer("handshake");
+ m_stream->async_handshake(Botan::TLS::Connection_Side::SERVER,
+ std::bind(&Server::handle_handshake, shared_from_this(), _1));
+ }
+
+ void shutdown()
+ {
+ error_code shutdown_ec;
+ m_stream->shutdown(shutdown_ec);
+ m_result.expect_success("shutdown", shutdown_ec);
+ handle_write(error_code{});
+ }
+
+
+ void handle_handshake(const error_code& ec)
+ {
+ m_result.expect_success("handshake", ec);
+ handle_write(error_code{});
+ }
+
+ void handle_write(const error_code& ec)
+ {
+ m_result.expect_success("send_response", ec);
+ m_result.set_timer("read_message");
+ net::async_read(*m_stream, buffer(),
+ std::bind(&Server::received_zero_byte, shared_from_this(), _1, _2),
+ std::bind(&Server::handle_read, shared_from_this(), _1, _2));
+ }
+
+ void handle_read(const error_code& ec, size_t bytes_transferred=0)
+ {
+ if(m_short_read_expected)
+ {
+ m_result.expect_ec("received stream truncated error", Botan::TLS::StreamTruncated, ec);
+ quit();
+ return;
+ }
+
+ if(ec)
+ {
+ if(m_stream->shutdown_received())
+ {
+ m_result.expect_ec("received EOF after close_notify", net::error::eof, ec);
+ m_result.set_timer("shutdown");
+ m_stream->async_shutdown(std::bind(&Server::handle_shutdown, shared_from_this(), _1));
+ }
+ else
+ {
+ m_result.test_failure("Unexpected error code: " + ec.message());
+ quit();
+ }
+ return;
+ }
+
+ m_result.expect_success("read_message", ec);
+
+ if(message() == prepare_shutdown_no_response_message)
+ {
+ m_short_read_expected = true;
+ shutdown();
+ return;
+ }
+
+ if(message() == expect_short_read_message)
+ {
+ m_short_read_expected = true;
+ }
+
+ m_result.set_timer("send_response");
+ net::async_write(*m_stream, buffer(bytes_transferred),
+ std::bind(&Server::handle_write, shared_from_this(), _1));
+ }
+
+ void handle_shutdown(const error_code& ec)
+ {
+ m_result.expect_success("shutdown", ec);
+ quit();
+ }
+
+ void quit()
+ {
+ m_result.stop_timer();
+ }
+
+ private:
+ tcp::acceptor m_acceptor;
+ Result_Wrapper m_result;
+ bool m_short_read_expected;
+ };
+
+class Client : public Side
+ {
+ static void accept_all(
+ const std::vector<Botan::X509_Certificate>&,
+ const std::vector<std::shared_ptr<const Botan::OCSP::Response>>&,
+ const std::vector<Botan::Certificate_Store*>&, Botan::Usage_Type,
+ const std::string&, const Botan::TLS::Policy&) {}
+
+ public:
+ Client(net::io_context& ioc)
+ : Side()
+ {
+ m_ctx.set_verify_callback(accept_all);
+ m_stream = std::unique_ptr<ssl_stream>(new ssl_stream(ioc, m_ctx));
+ }
+
+ ssl_stream& stream() {return *m_stream; }
+
+ void close_socket()
+ {
+ // Shutdown on TCP level before closing the socket for portable behavior. Otherwise the peer will see a
+ // connection_reset error rather than EOF on Windows.
+ // See the remark in
+ // https://www.boost.org/doc/libs/1_68_0/doc/html/boost_asio/reference/basic_stream_socket/close/overload1.html
+ m_stream->lowest_layer().shutdown(tcp::socket::shutdown_both);
+ m_stream->lowest_layer().close();
+ }
+
+ };
+
+#include <boost/asio/yield.hpp>
+
+class TestBase
+ {
+ public:
+ TestBase(net::io_context& ioc, std::shared_ptr<Server> server, const std::string& name)
+ : m_client(ioc),
+ m_server(server),
+ m_result(ioc, name) {}
+
+ virtual void finishAsynchronousWork() {}
+
+ Result result() { return m_result.result(); }
+
+ protected:
+ Client m_client;
+ std::shared_ptr<Server> m_server;
+ Result_Wrapper m_result;
+ };
+
+class Synchronous_Test : public TestBase
+ {
+ public:
+ using TestBase::TestBase;
+
+ void finishAsynchronousWork() override
+ {
+ m_client_thread.join();
+ }
+
+ void run(const error_code&)
+ {
+ m_client_thread = std::thread(std::bind(&Synchronous_Test::run_synchronous_client, this));
+ }
+
+ virtual void run_synchronous_client() = 0;
+
+ private:
+ std::thread m_client_thread;
+ };
+
+/* In this test case both parties perform the handshake, exchange a message, and do a full shutdown.
+ *
+ * The client expects the server to echo the same message it sent. The client then initiates the shutdown. The server is
+ * expected to receive a close_notify and complete its shutdown with an error_code Success, the client is expected to
+ * receive a close_notify and complete its shutdown with an error_code EOF.
+ */
+class Test_Conversation : public TestBase, public net::coroutine, public std::enable_shared_from_this<Test_Conversation>
+ {
+ public:
+ Test_Conversation(net::io_context& ioc, std::shared_ptr<Server> server)
+ : TestBase(ioc, server, "Test Conversation") {}
+
+ void run(const error_code& ec)
+ {
+ static auto test_case = &Test_Conversation::run;
+ const std::string message("Time is an illusion. Lunchtime doubly so.");
+
+ reenter(*this)
+ {
+ m_result.set_timer("connect");
+ yield net::async_connect(m_client.stream().lowest_layer(), k_endpoints,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("connect", ec);
+
+ m_result.set_timer("handshake");
+ yield m_client.stream().async_handshake(Botan::TLS::Connection_Side::CLIENT,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("handshake", ec);
+
+ m_result.set_timer("send_message");
+ yield net::async_write(m_client.stream(),
+ net::buffer(message.c_str(), message.size() + 1), // including \0 termination
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("send_message", ec);
+
+ m_result.set_timer("receive_response");
+ yield net::async_read(m_client.stream(),
+ m_client.buffer(),
+ std::bind(&Client::received_zero_byte, &m_client, _1, _2),
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("receive_response", ec);
+ m_result.confirm("correct message", m_client.message() == message);
+
+ m_result.set_timer("shutdown");
+ yield m_client.stream().async_shutdown(std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("shutdown", ec);
+
+ m_result.set_timer("await close_notify");
+ yield net::async_read(m_client.stream(), m_client.buffer(),
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.confirm("received close_notify", m_client.stream().shutdown_received());
+ m_result.expect_ec("closed with EOF", net::error::eof, ec);
+
+ m_result.stop_timer();
+ }
+ }
+ };
+
+class Test_Conversation_Sync : public Synchronous_Test
+ {
+ public:
+ Test_Conversation_Sync(net::io_context& ioc, std::shared_ptr<Server> server)
+ : Synchronous_Test(ioc, server, "Test Conversation Sync") {}
+
+ void run_synchronous_client() override
+ {
+ const std::string message("Time is an illusion. Lunchtime doubly so.");
+ error_code ec;
+
+ net::connect(m_client.stream().lowest_layer(), k_endpoints, ec);
+ m_result.expect_success("connect", ec);
+
+ m_client.stream().handshake(Botan::TLS::Connection_Side::CLIENT, ec);
+ m_result.expect_success("handshake", ec);
+
+ net::write(m_client.stream(),
+ net::buffer(message.c_str(), message.size() + 1), // including \0 termination
+ ec);
+ m_result.expect_success("send_message", ec);
+
+ net::read(m_client.stream(),
+ m_client.buffer(),
+ std::bind(&Client::received_zero_byte, &m_client, _1, _2),
+ ec);
+ m_result.expect_success("receive_response", ec);
+ m_result.confirm("correct message", m_client.message() == message);
+
+ m_client.stream().shutdown(ec);
+ m_result.expect_success("shutdown", ec);
+
+ net::read(m_client.stream(), m_client.buffer(), ec);
+ m_result.confirm("received close_notify", m_client.stream().shutdown_received());
+ m_result.expect_ec("closed with EOF", net::error::eof, ec);
+ }
+ };
+
+/* In this test case the client shuts down the SSL connection, but does not wait for the server's response before
+ * closing the socket. Accordingly, it will not receive the server's close_notify alert. Instead, the async_read
+ * operation will be aborted. The server should be able to successfully shutdown nonetheless.
+ */
+class Test_Eager_Close : public TestBase, public net::coroutine, public std::enable_shared_from_this<Test_Eager_Close>
+ {
+ public:
+ Test_Eager_Close(net::io_context& ioc, std::shared_ptr<Server> server)
+ : TestBase(ioc, server, "Test Eager Close") {}
+
+ void run(const error_code& ec)
+ {
+ static auto test_case = &Test_Eager_Close::run;
+ reenter(*this)
+ {
+ m_result.set_timer("connect");
+ yield net::async_connect(m_client.stream().lowest_layer(), k_endpoints,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("connect", ec);
+
+ m_result.set_timer("handshake");
+ yield m_client.stream().async_handshake(Botan::TLS::Connection_Side::CLIENT,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("handshake", ec);
+
+ m_result.set_timer("shutdown");
+ yield m_client.stream().async_shutdown(std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("shutdown", ec);
+ m_result.stop_timer();
+
+ m_client.close_socket();
+ m_result.confirm("did not receive close_notify", !m_client.stream().shutdown_received());
+ }
+ }
+ };
+
+class Test_Eager_Close_Sync : public Synchronous_Test
+ {
+ public:
+ Test_Eager_Close_Sync(net::io_context& ioc, std::shared_ptr<Server> server)
+ : Synchronous_Test(ioc, server, "Test Eager Close Sync") {}
+
+ void run_synchronous_client()
+ {
+ error_code ec;
+
+ net::connect(m_client.stream().lowest_layer(), k_endpoints, ec);
+ m_result.expect_success("connect", ec);
+
+ m_client.stream().handshake(Botan::TLS::Connection_Side::CLIENT, ec);
+ m_result.expect_success("handshake", ec);
+
+ m_client.stream().shutdown(ec);
+ m_result.expect_success("shutdown", ec);
+
+ m_client.close_socket();
+ m_result.confirm("did not receive close_notify", !m_client.stream().shutdown_received());
+ }
+ };
+
+/* In this test case the client closes the socket without properly shutting down the connection.
+ * The server should see a StreamTruncated error.
+ */
+class Test_Close_Without_Shutdown
+ : public TestBase,
+ public net::coroutine,
+ public std::enable_shared_from_this<Test_Close_Without_Shutdown>
+ {
+ public:
+ Test_Close_Without_Shutdown(net::io_context& ioc, std::shared_ptr<Server> server)
+ : TestBase(ioc, server, "Test Close Without Shutdown") {}
+
+ void run(const error_code& ec)
+ {
+ static auto test_case = &Test_Close_Without_Shutdown::run;
+ reenter(*this)
+ {
+ m_result.set_timer("connect");
+ yield net::async_connect(m_client.stream().lowest_layer(), k_endpoints,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("connect", ec);
+
+ m_result.set_timer("handshake");
+ yield m_client.stream().async_handshake(Botan::TLS::Connection_Side::CLIENT,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("handshake", ec);
+
+ // send the control message to configure the server to expect a short-read
+ m_result.set_timer("send expect_short_read message");
+ yield net::async_write(m_client.stream(),
+ net::buffer(m_server->expect_short_read_message.c_str(),
+ m_server->expect_short_read_message.size() + 1), // including \0 termination
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("send expect_short_read message", ec);
+
+ // read the confirmation of the control message above
+ m_result.set_timer("receive_response");
+ yield net::async_read(m_client.stream(),
+ m_client.buffer(),
+ std::bind(&Client::received_zero_byte, &m_client, _1, _2),
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("receive_response", ec);
+
+ m_result.stop_timer();
+
+ m_client.close_socket();
+ m_result.confirm("did not receive close_notify", !m_client.stream().shutdown_received());
+ }
+ }
+ };
+
+class Test_Close_Without_Shutdown_Sync : public Synchronous_Test
+ {
+ public:
+ Test_Close_Without_Shutdown_Sync(net::io_context& ioc, std::shared_ptr<Server> server)
+ : Synchronous_Test(ioc, server, "Test Close Without Shutdown Sync") {}
+
+ void run_synchronous_client()
+ {
+ error_code ec;
+ net::connect(m_client.stream().lowest_layer(), k_endpoints, ec);
+ m_result.expect_success("connect", ec);
+
+ m_client.stream().handshake(Botan::TLS::Connection_Side::CLIENT, ec);
+ m_result.expect_success("handshake", ec);
+
+ m_server->expect_short_read();
+
+ m_client.close_socket();
+ m_result.confirm("did not receive close_notify", !m_client.stream().shutdown_received());
+ }
+ };
+
+/* In this test case the server shuts down the connection but the client doesn't send the corresponding close_notify
+ * response. Instead, it closes the socket immediately.
+ * The server should see a short-read error.
+ */
+class Test_No_Shutdown_Response : public TestBase, public net::coroutine,
+ public std::enable_shared_from_this<Test_No_Shutdown_Response>
+ {
+ public:
+ Test_No_Shutdown_Response(net::io_context& ioc, std::shared_ptr<Server> server)
+ : TestBase(ioc, server, "Test No Shutdown Response") {}
+
+ void run(const error_code& ec)
+ {
+ static auto test_case = &Test_No_Shutdown_Response::run;
+ reenter(*this)
+ {
+ m_result.set_timer("connect");
+ yield net::async_connect(m_client.stream().lowest_layer(), k_endpoints,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("connect", ec);
+
+ m_result.set_timer("handshake");
+ yield m_client.stream().async_handshake(Botan::TLS::Connection_Side::CLIENT,
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("handshake", ec);
+
+ // send a control message to make the server shut down
+ m_result.set_timer("send shutdown message");
+ yield net::async_write(m_client.stream(),
+ net::buffer(m_server->prepare_shutdown_no_response_message.c_str(),
+ m_server->prepare_shutdown_no_response_message.size() + 1), // including \0 termination
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_success("send shutdown message", ec);
+
+ // read the server's close-notify message
+ m_result.set_timer("read close_notify");
+ yield net::async_read(m_client.stream(), m_client.buffer(),
+ std::bind(test_case, shared_from_this(), _1));
+ m_result.expect_ec("read gives EOF", net::error::eof, ec);
+ m_result.confirm("received close_notify", m_client.stream().shutdown_received());
+
+ m_result.stop_timer();
+
+ // close the socket rather than shutting down
+ m_client.close_socket();
+ }
+ }
+ };
+
+class Test_No_Shutdown_Response_Sync : public Synchronous_Test
+ {
+ public:
+ Test_No_Shutdown_Response_Sync(net::io_context& ioc, std::shared_ptr<Server> server)
+ : Synchronous_Test(ioc, server, "Test No Shutdown Response Sync") {}
+
+ void run_synchronous_client()
+ {
+ error_code ec;
+ net::connect(m_client.stream().lowest_layer(), k_endpoints, ec);
+ m_result.expect_success("connect", ec);
+
+ m_client.stream().handshake(Botan::TLS::Connection_Side::CLIENT, ec);
+ m_result.expect_success("handshake", ec);
+
+ net::write(m_client.stream(),
+ net::buffer(m_server->prepare_shutdown_no_response_message.c_str(),
+ m_server->prepare_shutdown_no_response_message.size() + 1), // including \0 termination
+ ec);
+ m_result.expect_success("send expect_short_read message", ec);
+
+ net::read(m_client.stream(), m_client.buffer(), ec);
+ m_result.expect_ec("read gives EOF", net::error::eof, ec);
+ m_result.confirm("received close_notify", m_client.stream().shutdown_received());
+
+ // close the socket rather than shutting down
+ m_client.close_socket();
+ }
+ };
+
+#include <boost/asio/unyield.hpp>
+
+template<typename TestT>
+void run_test_case(std::vector<Result>& results)
+ {
+ net::io_context ioc;
+
+ auto s = std::make_shared<Server>(ioc);
+ s->listen();
+
+ auto t = std::make_shared<TestT>(ioc, s);
+ t->run(error_code{});
+
+ try
+ {
+ ioc.run();
+ }
+ catch(Timeout_Exception&) { /* the test result will already contain a failure */ }
+
+ t->finishAsynchronousWork();
+
+ results.push_back(s->result());
+ results.push_back(t->result());
+ }
+
+} // namespace
+
+namespace Botan_Tests {
+
+class Tls_Stream_Integration_Tests final : public Test
+ {
+ public:
+ std::vector<Test::Result> run() override
+ {
+ std::vector<Test::Result> results;
+
+ run_test_case<Test_Conversation>(results);
+ run_test_case<Test_Eager_Close>(results);
+ run_test_case<Test_Close_Without_Shutdown>(results);
+ run_test_case<Test_No_Shutdown_Response>(results);
+ run_test_case<Test_Conversation_Sync>(results);
+ run_test_case<Test_Eager_Close_Sync>(results);
+ run_test_case<Test_Close_Without_Shutdown_Sync>(results);
+ run_test_case<Test_No_Shutdown_Response_Sync>(results);
+
+ return results;
+ }
+ };
+
+BOTAN_REGISTER_TEST("tls_stream_integration", Tls_Stream_Integration_Tests);
+
+} // namespace Botan_Tests
+
+#endif // BOOST_VERSION
+#endif // BOTAN_HAS_TLS && BOTAN_HAS_BOOST_ASIO