Based on the two official examples /example/http/client/sync
and /example/http/client/sync-ssl1
I am trying to build an HTTP client class that is able to handle both HTTP and HTTPS. As I am fairly new to boost.beast
I reached out to figure out what the best/intended way of doing this is. It turned out that using a polymorphic approach with an abstract transport class should work well:
I've implemented my client
and the client_transport
class based on the two examples.
Making play text requests (HTTP) works well. However, when I set my encryption
mode to encryption::tls
the http::read()
function returns a bad version
error.
I can successfully run the official sync-ssl
example on my machine which indicates to me that dependencies such as OpenSSL shouldn't play a role here but that instead I simply overlooked something. Unfortunately, I didn't make much progress in figuring this out so I'd appreciate any kind of help.
main.cpp
:
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/error.hpp>
#include <cstdlib>
#include <iostream>
#include <string>
#include "http/client.hpp"
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
namespace ssl = net::ssl; // from <boost/asio/ssl.hpp>
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
// Performs an HTTP GET and prints the response
int main(int argc, char** argv)
{
// Check command line arguments.
if(argc != 4 && argc != 5)
{
std::cerr <<
"Usage: http-client-sync-ssl <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
"Example:\n" <<
" http-client-sync-ssl www.example.com 443 /\n" <<
" http-client-sync-ssl 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;
try {
// IO context
boost::asio::io_context io_ctx;
// Build request
elx::http::client::request req = {
.host = host,
.port = static_cast<uint16_t>(std::stoi(port)),
.target = target,
.encryption = elx::http::client::request::encryption::tls
};
// Perform synchronous GET request
elx::http::client client(io_ctx);
auto response = client.synchronous_get(req);
// Show the response body
std::cout << "response = " << response.body_string() << std::endl;
}
catch(std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
client.hpp
:
#pragma once
#include <boost/beast/http.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include "authorization.hpp"
#include "client_transport.hpp"
namespace http = boost::beast::http;
namespace elx::http
{
class client
{
public:
struct request
{
enum class encryption {
none,
tls
};
std::string host;
uint16_t port;
std::string target;
uint8_t http_version = 11;
authorization::auth auth;
encryption encryption = encryption::none;
};
struct response
{
boost::beast::http::response<boost::beast::http::dynamic_body> raw;
std::string body_string() const
{
return boost::beast::buffers_to_string(raw.body().data());
}
};
// Construction
explicit client(boost::asio::io_context& ioc);
client(const client& other) = delete;
client(client&& other) = delete;
virtual ~client() = default;
[[nodiscard]] static enum request::encryption determine_encryption(const std::string& host);
[[nodiscard]] response synchronous_get(const request& req);
private:
boost::asio::io_context& m_io_ctx;
boost::asio::ip::tcp::resolver m_resolver;
std::unique_ptr<client_transport> m_transport;
};
}
client.cpp
:
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/strand.hpp>
#include "client.hpp"
#include "client_transport.hpp"
using namespace elx::http;
client::client(boost::asio::io_context& ioc) :
m_io_ctx(ioc),
m_resolver(boost::asio::make_strand(ioc))
{
}
client::response client::synchronous_get(const request& req)
{
namespace http = boost::beast::http;
// Setup SSL context (in case we need that)
boost::asio::ssl::context ssl_ctx(boost::asio::ssl::context::tlsv12_client);
// Setup encryption
switch (req.encryption) {
case request::encryption::none:
m_transport = std::make_unique<client_transport_plain>(m_io_ctx);
break;
tls
case request::encryption::tls:
ssl_ctx.set_default_verify_paths();
#warning "ToDo: We definitely want to verify the peer"
ssl_ctx.set_verify_mode(boost::asio::ssl::verify_none);
m_transport = std::make_unique<client_transport_tls>(m_io_ctx, ssl_ctx);
m_transport->set_hostname(req.host);
break;
}
// Sanity check
if (not m_transport)
return { };
// Look up the domain name
auto const results = m_resolver.resolve(req.host, std::to_string(req.port));
// Make the connection on the IP address we get from a lookup
m_transport->connect(results->endpoint());
// Perform handshake
m_transport->handshake();
// Set up an HTTP GET request message
http::request<boost::beast::http::string_body> beast_req;
beast_req.set(http::field::host, req.host);
beast_req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
if (not req.auth.is_empty())
beast_req.set(http::field::authorization, req.auth.to_string());
beast_req.method(http::verb::get);
beast_req.version(req.http_version);
beast_req.target(req.target);
// Send the HTTP beast_request to the remote host
http::write(m_transport->stream(), beast_req);
// This buffer is used for reading and must be persisted
boost::beast::flat_buffer buffer;
// Declare a container to hold the response
response res;
// Receive the HTTP response
boost::beast::error_code ec;
http::read(m_transport->stream(), buffer, res.raw, ec);
if (ec) {
throw boost::beast::system_error{ec};
}
// Gracefully close the socket
m_transport->stream().socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
// not_connected happens sometimes
// so don't bother reporting it.
//
if(ec && ec != boost::beast::errc::not_connected)
throw boost::beast::system_error{ec};
// If we get here then the connection is closed gracefully
return res;
}
client_transport.hpp
:
#pragma once
#include <boost/asio/ip/resolver_base.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/ssl.hpp>
namespace elx::http
{
class client_transport
{
public:
[[nodiscard]] virtual boost::beast::tcp_stream& stream() = 0;
virtual boost::beast::error_code set_hostname(const std::string& hostname) { return { }; }
virtual void handshake() { }
void connect(const boost::beast::tcp_stream::endpoint_type& endpoint)
{
stream().connect(endpoint);
}
};
class client_transport_plain :
public client_transport
{
public:
client_transport_plain(boost::asio::io_context& io_ctx) :
m_stream(io_ctx)
{
}
[[nodiscard]] boost::beast::tcp_stream& stream() override
{
return m_stream;
}
private:
boost::beast::tcp_stream m_stream;
};
class client_transport_tls :
public client_transport
{
public:
client_transport_tls(boost::asio::io_context& io_ctx, boost::asio::ssl::context& ssl_ctx) :
m_io_ctx(io_ctx),
m_ssl_ctx(ssl_ctx),
m_stream(io_ctx, ssl_ctx)
{
}
[[nodiscard]] boost::beast::tcp_stream& stream() override
{
return m_stream.next_layer();
}
boost::beast::error_code set_hostname(const std::string& hostname) override
{
// Set SNI Hostname (many hosts need this to handshake successfully)
if (not SSL_set_tlsext_host_name(m_stream.native_handle(), hostname.c_str())) {
return boost::beast::error_code{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
}
return { };
}
void handshake() override
{
m_stream.handshake(boost::asio::ssl::stream_base::client);
}
private:
boost::asio::io_context& m_io_ctx;
boost::asio::ssl::context& m_ssl_ctx;
boost::beast::ssl_stream<boost::beast::tcp_stream> m_stream;
};
}
The problem is that when you send the request and receive the response, you are doing so directly on the stream. You will need to implement write and read as polymorphic overrides so that the http request/response can be performed on the correct stream type.
Corrected code here: https://github.com/test-scenarios/beast-issue-1849