Boost.beast http :: read () возвращает «плохую версию» - PullRequest
2 голосов
/ 19 февраля 2020

На основе двух официальных примеров /example/http/client/sync и /example/http/client/sync-ssl1 Я пытаюсь создать клиентский класс HTTP, способный обрабатывать как HTTP, так и HTTPS. Поскольку я довольно новичок в boost.beast, я обратился к , чтобы выяснить, каков наилучший / намеченный способ сделать это. Оказалось, что использование подхода polymorphi c с абстрактным классом транспорта должно работать хорошо:

My polymorphic transport class design

Я реализовал client и client_transport класс, основанный на двух примерах. Создание запросов воспроизведения текста (HTTP) работает хорошо. Однако, когда я устанавливаю свой encryption режим на encryption::tls, функция http::read() возвращает ошибку bad version. Я могу успешно запустить официальный пример sync-ssl на своем компьютере, который показывает мне, что такие зависимости, как OpenSSL, здесь не должны играть никакой роли, но вместо этого я просто что-то упустил. К сожалению, я не добился большого прогресса в выяснении этого, поэтому я был бы признателен за любую помощь.

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;
    };
}

1 Ответ

1 голос
/ 22 февраля 2020

Проблема в том, что когда вы отправляете запрос и получаете ответ, вы делаете это прямо в потоке. Вам потребуется реализовать запись и чтение как переопределения polymorphi c, чтобы запрос / ответ http мог быть выполнен для правильного типа потока.

Исправленный код здесь: https://github.com/test-scenarios/beast-issue-1849

...