PrevUpHomeNext

c++ botan tls + boost asio beast (https client)


cpp: c++ networking https silice

Created on Oct 10, 2024

Updated On Oct 13, 2024 - Add asio::as_tuple, update throwing c++ std::exception.

Home: cpp Silice

cpp botan tls + boost asio beast

Reading this article requires reading c++ asio beast http networking

Hunt (track) it Step By Step

Start

Botan http tls (https) client commuication add a handshake to the boost::asio communication after connection.

tls handshake: tls_stream.async_handshake(...)

tls_stream.async_handshake(...) starts an asynchronous ssl (tls) handshake.

tls_stream.async_handshake(	// async handshake
	Botan::TLS::Connection_Side::Client,
	[self = this->shared_from_this()] (std::error_code ec)	// handler
	{
	}
);

How to get tls_stream

tls_stream is an object created from Botan::TLS::Stream.

Botan::TLS::Stream<beast::tcp_stream> tls_stream{tls_context, io_context};

Botan::TLS::Stream is a compatible tls stream to boost::asio.

Stream-Layer-Type: beast::tcp_stream - the next layer type.

io_context is an object of asio::io_context.

How to get tls_context

tls_context is an object created from Botan::TLS::Context.

Botan::TLS::Context tls_context_object{credentials_manager, rng, session_manager, policy, server_info);
std::shared_ptr<Botan::TLS::Context> tls_context
	{
		std::make_shared<Botan::TLS::Context>(
			credentials_manager,
			rng,
			session_manager,
			policy,
			server_info
		)
	};

Botan::TLS::Context is a helper class to initialize and configure Botan::TLS::Stream.

Arguments:

c++ Smart Pointer

[Tip] Tip

Quote

U* shall be implicitly convertible to T*

Constructor: https://cplusplus.com/reference/memory/shared_ptr/shared_ptr

operator=: https://cplusplus.com/reference/memory/shared_ptr/operator=

Constructor

std::shared_ptr<T> ptr = std::make_shared<U>(...);

operator=

std::shared_ptr<T> ptr;
ptr  = std::make_shared<U>(...);

In this article, U is the derived class, T is the base class, so U * is convertibale to T * .

RNG

std::shared_ptr<Botan::RandomNumberGenerator> rng
	{
		std::make_shared<Botan::AutoSeeded_RNG>()
	};

Botan::RandomNumberGenerator is an interface to a crypto random number generator. It is an abstract class.

Some implementors of Botan::RandomNumberGenerator:

credentials_manager

std::shared_ptr<Botan::Credentials_Manager> credentials_manager
	{
		std::make_shared<my_tls::credentials_manager>()
	};

Botan::Credentials_Manger - Interface for a credentials manager.

my_tls::credentials_manager - my_tls::credentials_manager is a user-defined class, which must be derived from Botan::Credentials_Manager and specify how to get the system certificate store. Otherwise the tls handshake will report certificate error.

session_manager

std::shared_ptr<Botan::TLS::Session_Manager> session_manager
	{
		std::make_shared<Botan::TLS::Session_Manager_In_Memory>(rng)
	};

Botan::TLS::Session_Manager is an interface to systems which can save session parameters for supporting session resumption. - it is an abstract class.

Other session manager:

rng

The std::shared_ptr smart pointer of rng described before.

policy

std::shared_ptr<Botan::TLS::Policy> policy
	{
		std::make_shared<Botan::TLS::Policy>()
	};

Botan::TLS::Policy is the TLS policy base class. Inherit it and overload as desired to suit local policy concerns.

server_info

Botan::TLS::Server_Information server_info;

Botan::TLS::Server_Information represents information known about a TLS server.

Conclusion

tls_stream

All of the above parameters are for creating tls_stream, the object of Botan::TLS::Stream<beast::tcp_stream>. After getting object tls_stream, we can let encrypted boost::asio/boost::beast networking work.

Botan::TLS and asio/beast c++ example (silice)

c++ example

#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <botan/tls.h>
#include <botan/asio_stream.h>
#include <botan/certstor_system.h>
#include <botan/auto_rng.h>
#include <iostream>

using std::string_literals::operator""s;

namespace beast = boost::beast;
namespace asio = boost::asio;
namespace http = beast::http;
using asio::ip::tcp;

namespace iotls
{

class credentials_manager:
	virtual public Botan::Credentials_Manager
{
protected:
	using ret_t = std::vector<Botan::Certificate_Store *>;
protected:
	Botan::System_Certificate_Store __store;
public:
	ret_t trusted_certificate_authorities(const std::string & type, const std::string & context) override
	{
		return ret_t{&__store};
	}
public:
	virtual ~credentials_manager() {}
};

class https_client:
	virtual public std::enable_shared_from_this<
		iotls::https_client
	>
{
protected:
	asio::io_context & __io_context;
	const std::string __host;
	const std::string __port;
protected:
	tcp::resolver __resolver;
	http::request<http::string_body> __request;
	http::response<http::string_body> __response;
	beast::flat_buffer __buffer;
protected:
// Parameters for __tls_context
	std::shared_ptr<Botan::RandomNumberGenerator> __rng;
	std::shared_ptr<Botan::Credentials_Manager> __cred_man;
	std::shared_ptr<Botan::TLS::Session_Manager> __sess_man;
	std::shared_ptr<Botan::TLS::Policy> __policy;
	Botan::TLS::Server_Information __server_info;
// __tls_context
	std::shared_ptr<Botan::TLS::Context> __tls_context;
// __tls_stream
	Botan::TLS::Stream<beast::tcp_stream> __tls_stream;
public:
	https_client(
		asio::io_context & io_context__,
		const std::string_view host__,
		const std::string_view port__
	):
		__io_context{io_context__},
		__host{host__},
		__port{port__},
		__resolver{__io_context},

	// Construct parameters for __tls_context
		__rng{std::make_shared<Botan::AutoSeeded_RNG>()},
		__cred_man{std::make_shared<iotls::credentials_manager>()},
		__sess_man{std::make_shared<Botan::TLS::Session_Manager_In_Memory>(__rng)},
		__policy{std::make_shared<Botan::TLS::Policy>()},
		__server_info{},

	// __tls_context
		__tls_context{
			std::make_shared<Botan::TLS::Context>(
				__cred_man,
				__rng,
				__sess_man,
				__policy,
				__server_info
			)
		},

	// __tls_stream
		__tls_stream{__tls_context, __io_context}

	{
				std::clog << "https session is created.\n";
	}
public:
	virtual ~https_client()
	{
		__tls_stream.async_shutdown(
			[] (std::error_code ec)
			{
				std::clog << "tls async shutdown.\n";
				if (ec)
					std::clog << "0\n";
				else
					std::clog << "1\n";
			}
		);
		std::clog << "https session is ended.\n";
	}
public:
	void run()
	{
		this->start();
	}
protected:
	void start()
	{
		this->resolve();
	}
protected:
	void resolve()
	{
		__resolver.async_resolve(
			__host,
			__port,
			[self=this->shared_from_this()]
			(
				std::error_code ec,
				tcp::resolver::results_type results
			)
			{
				if (ec)
					throw std::system_error{ec, "async resolve error."};
				std::clog << "async resolve OK.\n";
				self->connect(std::move(results));
			}
		);
	}
	void connect(tcp::resolver::results_type && results)
	{
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		__tls_stream.next_layer().async_connect(
			results,
			[self=this->shared_from_this()]
			(
				std::error_code ec,
				tcp::endpoint ep
			)
			{
				if (ec)
					throw std::system_error{ec, "async connect error."};
				std::clog << "async connect OK.\n";
				self->handshake();
			}
		);
	}
	void handshake()
	{
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		__tls_stream.async_handshake(
			Botan::TLS::Connection_Side::Client,
			[self = this->shared_from_this()]
			(
				std::error_code ec
			)
			{
				if (ec)
					throw std::system_error{ec, "tls async handshake error."};
				std::clog << "tls async handshake OK.\n";
				self->write();
			}
		);
	}
protected:
	void write()
	{
		__request.target("/");
		__request.set(http::field::host, __host);
		__request.method(http::verb::get);
		__request.version(11);
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		http::async_write(
			__tls_stream,
			__request,
			[self = this->shared_from_this()]
			(
				std::error_code ec,
				unsigned long bytes
			)
			{
				if (ec)
					throw std::system_error{ec, "async write error."};
				std::clog << "async write OK." << std::endl;
				self->read();
			}
		);
	}
	void read()
	{
		http::async_read(
			__tls_stream,
			__buffer,
			__response,
			[self = this->shared_from_this()]
			(
				std::error_code ec,
				unsigned long bytes
			)
			{
				if (ec)
					throw std::system_error{ec, "async read error."};
				std::cout << "Got:\n\n";
				std::cout << self->__response << "\n\n";
			}
		);
	}
};	// class https_client

}	// namespace iotls

int main(int argc, char * argv[])
try
{
	if (argc != 3)
		throw std::runtime_error{""s + argv[0] + " <remote host> <remote port>"};

	asio::io_context io_context;
	std::make_shared<iotls::https_client>(io_context, argv[1], argv[2])->run();

	io_context.run();
}
catch (const std::exception & e)
{
	std::cerr << "ERROR:\n" << e.what() << std::endl;
}

Botan::TLS and asio/beast c++ example, coroutines (silice)

c++ example

Coroutines (co_await ...) can not be used in a destructor.

#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <botan/certstor_system.h>
#include <botan/tls.h>
#include <botan/auto_rng.h>
#include <iostream>
#include <botan/asio_stream.h>

namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
using asio::ip::tcp;
using std::string_literals::operator""s;

namespace my::https
{

class credentials_manager:
	virtual public Botan::Credentials_Manager
// Requires to load certs from system, otherwise the handshaking reports cert error.
{
protected:
	Botan::System_Certificate_Store __store;
public:
	std::vector<Botan::Certificate_Store *>
	trusted_certificate_authorities(const std::string & type, const std::string & context) override
	{
		return {&__store};
	}
};

class client:
	virtual public std::enable_shared_from_this<my::https::client>
{
protected:
	const std::string __host;
	const std::string __port;
	tcp::resolver __resolver;
	http::request<http::string_body> __request;
	http::response<http::string_body> __response;
	beast::flat_buffer __buffer;
protected:
	// params for __tls_context
	std::shared_ptr<Botan::RandomNumberGenerator> __rng;
	std::shared_ptr<Botan::Credentials_Manager> __cred_man;
	std::shared_ptr<Botan::TLS::Session_Manager> __sess_man;
	std::shared_ptr<Botan::TLS::Policy> __policy;
	Botan::TLS::Server_Information __server_info;

	// __tls_context
	std::shared_ptr<Botan::TLS::Context> __tls_context;

	// __tls_stream
	Botan::TLS::Stream<beast::tcp_stream> __tls_stream;
public:
	virtual ~client()
	{
		std::clog << "https session is closed.\n";
	}
public:
	client(
		asio::any_io_executor executor__,
		const std::string_view host__,
		const std::string_view port__
	):
		__host{host__},
		__port{port__},
		__resolver{executor__},

	// params for __tls_context
		__rng{std::make_shared<Botan::AutoSeeded_RNG>()},
		__cred_man{std::make_shared<my::https::credentials_manager>()},
		__sess_man{std::make_shared<Botan::TLS::Session_Manager_In_Memory>(__rng)},
		__policy{std::make_shared<Botan::TLS::Policy>()},
		__server_info{},

	// __tls_context
		__tls_context{
			std::make_shared<Botan::TLS::Context>(
				__cred_man,
				__rng,
				__sess_man,
				__policy,
				__server_info
			)
		},

	// __tls_stream
		__tls_stream{__tls_context, executor__}

	{
		std::clog << "https session is created.\n";
	}
public:
	asio::awaitable<void> run()
	{
		co_await this->start();
	}
protected:
	asio::awaitable<void> start()
	{
		co_await this->resolve();
	}
protected:
	asio::awaitable<void> resolve()
	{
		auto [ec, results] = co_await __resolver.async_resolve(
			__host,
			__port,
			asio::as_tuple(asio::use_awaitable)
		);
		if (ec)
			throw std::system_error{ec, "async resolve error!"};
		std::clog << "async resolve OK!" << std::endl;
		co_await this->connect(std::move(results));
	}
	asio::awaitable<void> connect(tcp::resolver::results_type && results__)
	{
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));	// Only wait 3 seconds.
		auto [ec, ep] = co_await __tls_stream.next_layer().async_connect(
			results__,
			asio::as_tuple(asio::use_awaitable)
		);
		if (ec)
			throw std::system_error{ec, "async connect error!"};
		std::clog << "async connect OK!" << std::endl;
		co_await this->handshake();
	}
	asio::awaitable<void> handshake()
	{
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		auto [ec] = co_await __tls_stream.async_handshake(
			Botan::TLS::Connection_Side::Client,
			asio::as_tuple(asio::use_awaitable)
		);
		if (ec)
			throw std::system_error{ec, "async handshake error!"};
		std::clog << "async handshake OK!\n";
		co_await this->write();
	}
protected:
	asio::awaitable<void> write()
	{
		__request.method(http::verb::get);
		__request.target("/");
		__request.set(http::field::host, __host);
		__request.version(11);
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		auto [ec, bytes] = co_await http::async_write(
			__tls_stream,
			__request,
			asio::as_tuple(asio::use_awaitable)
		);
		if (ec)
			throw std::system_error{ec, "async write request error."};
		std::clog << "async write request OK." << std::endl;
		co_await this->read();
	}
	asio::awaitable<void> read()
	{
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		auto [ec, bytes] = co_await http::async_read(
			__tls_stream,
			__buffer,
			__response,
			asio::as_tuple(asio::use_awaitable)
		);
		if (ec)
			throw std::system_error{ec, "async read error!"};
		std::cout << __response.body().data() << std::endl;
		co_await this->close();
	}
protected:
	asio::awaitable<void> close()
	{
		__tls_stream.next_layer().expires_after(std::chrono::seconds(3));
		auto [ec] = co_await __tls_stream.async_shutdown(asio::as_tuple(asio::use_awaitable));
		if (ec)
			std::clog << "0\n";
		else
			std::clog << "1\n";
	}
};	// class client

}	// namespace my::https

int main(int argc, char * argv[])
try
{
	if (argc != 3)
		throw std::runtime_error{""s + argv[0] + " <remote host> <remote port>"};
	auto io_context = asio::io_context{};
	asio::co_spawn(
		io_context,
		[host=argv[1], port=argv[2]]
			-> asio::awaitable<void>
		{
			try
			{
				auto executor = co_await asio::this_coro::executor;
				co_await std::make_shared<my::https::client>(executor, host, port)->run();
			}
			catch (const std::exception & e)
			{
				std::cerr << "Run ERROR:\n" << e.what() << std::endl;
			}
		},
		asio::detached
	);
	io_context.run();
}
catch (const std::exception & e)
{
	std::cerr << "ERROR:\n" << e.what() << std::endl;
}

Home: cpp Silice

See Also

c++ botan

boost::asio

boost::beast


PrevUpHomeNext

E

U