Как создать boost ssl iostream?

Я добавляю поддержку HTTPS в код, который выполняет ввод и вывод с помощью boost tcp::iostream (действуя как HTTP-сервер).

Я нашел примеры (и у меня есть работающий игрушечный HTTPS-сервер), которые выполняют ввод/вывод SSL с использованием boost::asio::read/boost::asio::write, но ни один из них не использует iostreams и операторы ‹‹ >>. Как превратить ssl::stream в iostream?

Рабочий код:

#include <boost/asio.hpp> 
#include <boost/asio/ssl.hpp> 
#include <boost/foreach.hpp>
#include <iostream> 
#include <sstream>
#include <string>

using namespace std;
using namespace boost;
using boost::asio::ip::tcp;

typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_stream;

string HTTPReply(int nStatus, const string& strMsg)
{
    string strStatus;
    if (nStatus == 200) strStatus = "OK";
    else if (nStatus == 400) strStatus = "Bad Request";
    else if (nStatus == 404) strStatus = "Not Found";
    else if (nStatus == 500) strStatus = "Internal Server Error";
    ostringstream s;
    s << "HTTP/1.1 " << nStatus << " " << strStatus << "\r\n"
      << "Connection: close\r\n"
      << "Content-Length: " << strMsg.size() << "\r\n"
      << "Content-Type: application/json\r\n"
      << "Date: Sat, 09 Jul 2009 12:04:08 GMT\r\n"
      << "Server: json-rpc/1.0\r\n"
      << "\r\n"
      << strMsg;
    return s.str();
}

int main() 
{ 
    // Bind to loopback 127.0.0.1 so the socket can only be accessed locally                                            
    boost::asio::io_service io_service;
    tcp::endpoint endpoint(boost::asio::ip::address_v4::loopback(), 1111);
    tcp::acceptor acceptor(io_service, endpoint);

    boost::asio::ssl::context context(io_service, boost::asio::ssl::context::sslv23);
    context.set_options(
        boost::asio::ssl::context::default_workarounds
        | boost::asio::ssl::context::no_sslv2);
    context.use_certificate_chain_file("server.cert");
    context.use_private_key_file("server.pem", boost::asio::ssl::context::pem);

    for(;;)
    {
        // Accept connection                                                                                            
        ssl_stream stream(io_service, context);
        tcp::endpoint peer_endpoint;
        acceptor.accept(stream.lowest_layer(), peer_endpoint);
        boost::system::error_code ec;
        stream.handshake(boost::asio::ssl::stream_base::server, ec);

        if (!ec) {
            boost::asio::write(stream, boost::asio::buffer(HTTPReply(200, "Okely-Dokely\n")));
            // I really want to write:
            // iostream_object << HTTPReply(200, "Okely-Dokely\n") << std::flush;
        }
    }
}

Кажется, что ssl::stream_service был бы ответом, но это тупик.

Использование boost::iostreams (как предложено в принятом ответе) - правильный подход; вот рабочий код, который у меня получился:

#include <boost/asio.hpp> 
#include <boost/asio/ssl.hpp> 
#include <boost/iostreams/concepts.hpp>
#include <boost/iostreams/stream.hpp>
#include <sstream>
#include <string>
#include <iostream>

using namespace boost::asio;

typedef ssl::stream<ip::tcp::socket> ssl_stream;


//
// IOStream device that speaks SSL but can also speak non-SSL
//
class ssl_iostream_device : public boost::iostreams::device<boost::iostreams::bidirectional> {
public:
    ssl_iostream_device(ssl_stream &_stream, bool _use_ssl ) : stream(_stream)
    {
        use_ssl = _use_ssl;
        need_handshake = _use_ssl;
    }

    void handshake(ssl::stream_base::handshake_type role)
    {
        if (!need_handshake) return;
        need_handshake = false;
        stream.handshake(role);
    }
    std::streamsize read(char* s, std::streamsize n)
    {
        handshake(ssl::stream_base::server); // HTTPS servers read first
        if (use_ssl) return stream.read_some(boost::asio::buffer(s, n));
        return stream.next_layer().read_some(boost::asio::buffer(s, n));
    }
    std::streamsize write(const char* s, std::streamsize n)
    {
        handshake(ssl::stream_base::client); // HTTPS clients write first
        if (use_ssl) return boost::asio::write(stream, boost::asio::buffer(s, n));
        return boost::asio::write(stream.next_layer(), boost::asio::buffer(s, n));
    }

private:
    bool need_handshake;
    bool use_ssl;
    ssl_stream& stream;
};

std::string HTTPReply(int nStatus, const std::string& strMsg)
{
    std::string strStatus;
    if (nStatus == 200) strStatus = "OK";
    else if (nStatus == 400) strStatus = "Bad Request";
    else if (nStatus == 404) strStatus = "Not Found";
    else if (nStatus == 500) strStatus = "Internal Server Error";
    std::ostringstream s;
    s << "HTTP/1.1 " << nStatus << " " << strStatus << "\r\n"
      << "Connection: close\r\n"
      << "Content-Length: " << strMsg.size() << "\r\n"
      << "Content-Type: application/json\r\n"
      << "Date: Sat, 09 Jul 2009 12:04:08 GMT\r\n"
      << "Server: json-rpc/1.0\r\n"
      << "\r\n"
      << strMsg;
    return s.str();
}


void handle_request(std::iostream& s)
{
    s << HTTPReply(200, "Okely-Dokely\n") << std::flush;
}

int main(int argc, char* argv[])
{ 
    bool use_ssl = (argc <= 1);

    // Bind to loopback 127.0.0.1 so the socket can only be accessed locally                                            
    io_service io_service;
    ip::tcp::endpoint endpoint(ip::address_v4::loopback(), 1111);
    ip::tcp::acceptor acceptor(io_service, endpoint);

    ssl::context context(io_service, ssl::context::sslv23);
    context.set_options(
        ssl::context::default_workarounds
        | ssl::context::no_sslv2);
    context.use_certificate_chain_file("server.cert");
    context.use_private_key_file("server.pem", ssl::context::pem);

    for(;;)
    {
        ip::tcp::endpoint peer_endpoint;
        ssl_stream _ssl_stream(io_service, context);
        ssl_iostream_device d(_ssl_stream, use_ssl);
        boost::iostreams::stream<ssl_iostream_device> ssl_iostream(d);

        // Accept connection                                                                                            
        acceptor.accept(_ssl_stream.lowest_layer(), peer_endpoint);
        std::string method;
        std::string path;
        ssl_iostream >> method >> path;

        handle_request(ssl_iostream);
    }
}

person gavinandresen    schedule 08.09.2010    source источник
comment
почему вы хотите использовать iostream, если методы синхронного чтения и записи уже работают?   -  person Sam Miller    schedule 08.09.2010
comment
Потому что я добавляю поддержку HTTPS в код, который уже говорит по HTTP с использованием iostreams, и я хочу свести к минимуму объем кода, который я изменяю.   -  person gavinandresen    schedule 08.09.2010
comment
Возможно, вам было бы полезнее показать нам код, который НЕ работает.   -  person joshperry    schedule 11.09.2010
comment
Проблема не в том, чтобы заставить код работать, а в том, чтобы полностью интегрировать его в существующий код, не переписывая его полностью. «Настоящий» код: github.com/gavinandresen/bitcoin -git/blob/мастер/rpc.cpp#L1280   -  person gavinandresen    schedule 12.09.2010
comment
Простота этого кода потрясающая   -  person chmike    schedule 23.07.2015
comment
К сожалению, это не работает. См. boost.org/doc/libs/1_58_0/libs. /iostreams/doc/index.html. Если read возвращает меньше символов, чем запрошено, предполагается, что eof достигнуто. Обычно это происходит с read_some. Таким образом, мы не можем подключить розетку как устройство. Это ограничение класса boost::iostream::stream.   -  person chmike    schedule 23.07.2015
comment
ssl::stream в Boost.Asio действительно должен был называться ssl::socket. Он аналогичен ip::tcp::socket в поддержке функций-членов read_some()/write_some().   -  person Tanz87    schedule 29.07.2019


Ответы (3)


Предложение @Guy (с использованием boost::asio::streambuf) должно работать, и его, вероятно, проще всего реализовать. Главный недостаток такого подхода заключается в том, что все, что вы пишете в iostream, будет буферизоваться в памяти до конца, когда вызов boost::asio::write() сразу выгрузит все содержимое буфера в поток ssl. (Я должен отметить, что во многих случаях такая буферизация может быть желательна, и в вашем случае, вероятно, это не имеет никакого значения, поскольку вы сказали, что это приложение с небольшим объемом).

Если это всего лишь «разовый», я бы, вероятно, реализовал его, используя подход @Guy.

При этом есть ряд веских причин, по которым вы могли бы предпочесть решение, позволяющее использовать вызовы iostream для записи непосредственно в файл ssl_stream. Если вы обнаружите, что это так, вам нужно будет создать свой собственный класс-оболочку, который расширяет std::streambuf, переопределяет overflow() и sync() (и, возможно, другие, в зависимости от ваших потребностей).

К счастью, boost::iostreams предоставляет относительно простой способ чтобы сделать это без необходимости возиться со стандартными классами напрямую. Вы просто создаете свой собственный класс, реализующий соответствующий Device контракт. В данном случае это Sink, и класс boost::iostreams::sink предоставляется как удобный способ пройти большую часть пути. Когда у вас есть новый класс Sink, который инкапсулирует процесс записи в ваш базовый ssl_stream, все, что вам нужно сделать, это создать boost::iostreams::stream, шаблон которого соответствует вашему новому типу устройства, и вперед.

Это будет выглядеть примерно так (этот пример адаптирован из здесь, см. также эту связанную публикацию о stackoverflow) :

//---this should be considered to be "pseudo-code", 
//---it has not been tested, and probably won't even compile
//---

#include <boost/iostreams/concepts.hpp>
// other includes omitted for brevity ...

typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_stream;

class ssl_iostream_sink : public sink {
public:
    ssl_iostream_sink( ssl_stream *theStream )
    {
        stream = theStream;
    }

    std::streamsize write(const char* s, std::streamsize n)
    {
        // Write up to n characters to the underlying 
        // data sink into the buffer s, returning the 
        // number of characters written

        boost::asio::write(*stream, boost::asio::buffer(s, n));
    }
private:
    ssl_stream *stream;
};

Теперь ваш цикл приема может измениться и выглядеть примерно так:

for(;;)
{
    // Accept connection                                                                                            
    ssl_stream stream(io_service, context);
    tcp::endpoint peer_endpoint;
    acceptor.accept(stream.lowest_layer(), peer_endpoint);
    boost::system::error_code ec;
    stream.handshake(boost::asio::ssl::stream_base::server, ec);


    if (!ec) {

        // wrap the ssl stream with iostream
        ssl_iostream_sink my_sink(&stream);
        boost::iostream::stream<ssl_iostream_sink> iostream_object(my_sink);

        // Now it works the way you want...
        iostream_object << HTTPReply(200, "Okely-Dokely\n") << std::flush;
    }
}

Этот подход подключает поток ssl к инфраструктуре iostream. Так что теперь вы должны быть в состоянии сделать что-нибудь с iostream_object в приведенном выше примере, что вы обычно делаете с любым другим std::ostream (например, stdout). И то, что вы на него напишете, будет записано в ssl_stream за кулисами. Iostreams имеет встроенную буферизацию, поэтому некоторая степень буферизации будет иметь место внутри — но это хорошо — он будет буферизовать до тех пор, пока не накопит разумный объем данных, затем он выгрузит их в поток ssl и вернуться к буферизации. Последний std::flush, должен принудительно очистить буфер в ssl_stream.

Если вам нужно больше контроля над внутренней буферизацией (или любыми другими дополнительными функциями), взгляните на другие интересные функции, доступные в boost::iostreams. В частности, вы можете начать с просмотра stream_buffer.

Удачи!

person Lee    schedule 12.09.2010

Я думаю, что вы хотите использовать буферы потока (asio::streambuf)

Затем вы можете сделать что-то вроде (следует непроверенный код, написанный на лету):

boost::asio::streambuf msg;
std::ostream msg_stream(&msg);
msg_stream << "hello world";
msg_stream.flush();
boost::asio::write(stream, msg);

Точно так же ваша сторона чтения/получения может считывать в буфер потока в сочетании с std::istream, чтобы вы могли обрабатывать свой ввод, используя различные функции/операторы потока.

справочник по Asio для streambuf

Еще одно замечание: я думаю, вам следует ознакомиться с учебниками/примерами asio. Как только вы это сделаете, вы, вероятно, захотите изменить свой код, чтобы он работал асинхронно, а не в синхронном примере, который вы показали выше.

person Guy Sirton    schedule 11.09.2010
comment
Я могу в конечном итоге сделать это, если не смогу заставить работать iostream с поддержкой ssl. RE асинхронный: нет, сервер уже работает в отдельном потоке и ему не нужно обрабатывать даже десятки подключений в минуту, поэтому синхронная работа лучше. - person gavinandresen; 12.09.2010
comment
Вы также можете попробовать basic_socket_iostream, но я не думаю, что он работает с SSL (он работает с tcp, ip::tcp::iostream). Однако вы можете адаптировать свой поток ssl для работы с ним. - person Guy Sirton; 12.09.2010
comment
Я попытался использовать существующую систему basic_socket_iostream, предоставив специализацию для basic_socket<ssl> (которая охватывает существующие ssl::context и ssl::stream<ip::tcp::socket>) -- stackoverflow.com/ а/57341216/1433768. Полностью не получилось из-за существующего разрыва абстракции в basic_socket_streambuf<>::connect_to_endpoints(), но выглядит очень аккуратно и многообещающе. - person Tanz87; 03.08.2019

ssl::stream можно обернуть с помощью boost::iostreams/bidirectional, чтобы имитировать поведение, аналогичное tcp::iostream. сброса вывода перед дальнейшим чтением кажется, что нельзя избежать.

#include <regex>
#include <string>
#include <iostream>
#include <boost/iostreams/stream.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

namespace bios = boost::iostreams;
namespace asio = boost::asio;
namespace ssl = boost::asio::ssl;

using std::string;
using boost::asio::ip::tcp;
using boost::system::system_error;
using boost::system::error_code;

int parse_url(const std::string &s,
    std::string& proto, std::string& host, std::string& path)
{
    std::smatch m;
    bool found = regex_search(s, m, std::regex("^(http[s]?)://([^/]*)(.*)$"));
    if (m.size() != 4)
        return -1;
    proto = m[1].str();
    host = m[2].str();
    path = m[3].str();
    return 0;
}

void get_page(std::iostream& s, const string& host, const string& path)
{ 
    s << "GET " <<  path << " HTTP/1.0\r\n"
        << "Host: " << host << "\r\n"
        << "Accept: */*\r\n"
        << "Connection: close\r\n\r\n" << std::flush;

    std::cout << s.rdbuf() << std::endl;;
}

typedef ssl::stream<tcp::socket> ssl_socket;
class ssl_wrapper : public bios::device<bios::bidirectional>
{
    ssl_socket& sock;
public:
    typedef char char_type;

    ssl_wrapper(ssl_socket& sock) : sock(sock) {}

    std::streamsize read(char_type* s, std::streamsize n) {
        error_code ec;          
        auto rc = asio::read(sock, asio::buffer(s,n), ec);
        return rc;
    }
    std::streamsize write(const char_type* s, std::streamsize n) {
        return asio::write(sock, asio::buffer(s,n));
    }
};

int main(int argc, char* argv[])
{
    std::string proto, host, path;
    if (argc!= 2 || parse_url(argv[1], proto, host, path)!=0)
        return EXIT_FAILURE;
    try {
        if (proto != "https") {
            tcp::iostream s(host, proto);
            s.expires_from_now(boost::posix_time::seconds(60));
            get_page(s, host, path);
        } else {
            asio::io_service ios;

            tcp::resolver resolver(ios);
            tcp::resolver::query query(host, "https");
            tcp::resolver::iterator endpoint_iterator = 
               resolver.resolve(query);

            ssl::context ctx(ssl::context::sslv23);
            ctx.set_default_verify_paths();
            ssl_socket socket(ios, ctx);

            asio::connect(socket.lowest_layer(), endpoint_iterator);

            socket.set_verify_mode(ssl::verify_none);
            socket.set_verify_callback(ssl::rfc2818_verification(host));
            socket.handshake(ssl_socket::client);

            bios::stream<ssl_wrapper> ss(socket);
            get_page(ss, host, path);
        }
    } catch (const std::exception& e) {
        std::cout << "Exception: " << e.what() << "\n";
    }
}
person wkliang    schedule 06.10.2015