如何创建一个Boost SSL iostream?

24

我正在为使用boost tcp::iostream(作为HTTP服务器)进行输入和输出的代码添加HTTPS支持。

我已经找到了一些示例(并且有一个可以工作的玩具HTTPS服务器),它们使用boost::asio::read/boost::asio::write进行SSL输入/输出,但是没有使用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);
    }
}

因为我要给已经使用iostreams通信的代码添加HTTPS支持,而且我想尽可能减少改动的代码量。 - gavinandresen
最好向我们展示那些“不起作用”的代码,这样会更有帮助。 - joshperry
1
问题不在于让代码工作 - 而是将其清晰地集成到现有代码中,而无需完全重写它。 "真正的" 代码是:http://github.com/gavinandresen/bitcoin-git/blob/master/rpc.cpp#L1280 - gavinandresen
这段代码的简洁性极好。 - chmike
很遗憾,这个方法不可行。请参考http://www.boost.org/doc/libs/1_58_0/libs/iostreams/doc/index.html。如果`read`返回的字符比请求的要少,那么就会认为`eof`已经到达了。这通常是在`read_some`中发生的情况。因此,我们无法将套接字插入设备。这是boost::iostream::stream类的一个限制。 - chmike
显示剩余2条评论
3个回答

15

@Guy的建议(使用boost::asio::streambuf)应该可以解决问题,而且实现起来可能是最简单的。这种方法的主要缺点是,你写入 iostream 的所有内容都会在内存中缓存,直到最后调用boost::asio::write()时才会一次性将整个缓冲区的内容转储到 ssl 流中。(需要注意的是,在许多情况下,这种缓冲实际上是可取的,在你的情况下可能没有任何区别,因为你已经说过它是一个低流量的应用程序。)

如果这只是一个“一次性”的应用程序,我可能会使用@Guy的方法来实现。

话虽如此,有很多好的理由让你更愿意使用 iostream 调用直接写入你的ssl_stream。如果你发现这是情况,那么你就需要构建自己的包装类,扩展std::streambuf,重写overflow()sync()(根据你的需求可能还有其他函数)。

幸运的是,boost::iostreams提供了一种相对简单的方法来完成此操作,而无需直接处理std类。您只需构建自己的类,实现适当的Device协议。在这种情况下,那就是Sink,而boost::iostreams::sink类则提供了一个方便的方式来完成大部分工作。一旦您有了一个新的Sink类,它封装了向底层ssl_stream写入的过程,您所要做的就是创建一个以您的新设备类型为模板的boost::iostreams::stream,然后开始使用。

它将看起来像以下内容(此示例改编自here,还请参见this related stackoverflow post):

//---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框架中。现在,您应该能够像对待任何其他std::ostream(如stdout)一样对待上面示例中的,并且写入它的内容将在幕后写入ssl_stream。iostream具有内置缓冲区,因此将在内部进行某种程度的缓冲--但这是一件好事--它会缓冲直到积累了一些合理数量的数据,然后将其转储到ssl流,并返回缓冲区。最终的std::flush应该强制将其缓冲区清空到ssl_stream中。
如果您需要更多控制内部缓冲区(或任何其他高级功能),请查看boost::iostreams中提供的其他酷功能。具体而言,您可以从stream_buffer开始。祝你好运!

2
我认为你想做的是使用流缓冲区(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教程/示例。一旦您这样做了,您可能会想要更改您的代码以异步工作,而不是上面显示的同步示例。

如果我无法让ssl-enabled-iostream正常工作,我可能最终会这样做。关于异步:不需要,服务器已经在单独的线程中运行,每分钟甚至不必处理数十个连接,因此同步运行更好。 - gavinandresen
你也可以尝试使用basic_socket_iostream,但我认为它不支持SSL(它支持tcp、ip::tcp::iostream)。不过你可能可以调整你的SSL流以使其与它兼容。 - Guy Sirton
我曾经尝试使用现有的basic_socket_iostream系统,通过为basic_socket<ssl>(它包装现有的ssl::contextssl::stream<ip::tcp::socket>)提供特化来实现-- https://dev59.com/MrHma4cB1Zd3GeqPJUxN#57341216。由于在`basic_socket_streambuf<>::connect_to_endpoints()`中存在一个抽象性破坏,所以没有完全成功,但看起来非常整洁和有前途。 - Tanz87

1

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接