Why is there no asio::ssl::iostream? (and how to implement it)
Asked Answered
E

2

3

I'am currently exploring the Asio library and have working code for regular TCP connections. I used asio::ip::tcp::iostream objects since stuff I want to transmit already can serialize to/deserialize from iostreams, so this was really handy and worked well for me.

I then tried to switch to SSL connections and that's when everything turned crazy. There is apparently no built-in support to get the same iostream interface that all other protocols support for a secured connection. From a design perspective this is really perplexing to me. Is there any reason why this is the case?

I am aware of the discussion in How to create a boost ssl iostream? which concludes with a wrapper class to provide iostream functionality using boost. Apart from that, according to a comment, the implementation is flawed, this also does not give the same interface as for the other protocols (a basic_socket_iostream) which also allows to e.g., set expiration times and close the connection. (I am also using asio in the non-boost version and want to avoid adding boost as an additional dependency if possible).

So, I guess my questions are:

  1. What exactly would I need to implement to get a basic_socket_iostream for an SSL connection? I assume it would be a derivation of asio::basic_streambuf or asio::basic_socket_streambuf but I somehow can't figure out how they work and need to be tweaked.. there's just a bunch of weird pointer movement and buffer allocations and documentation to me is quite unclear on what happens when exactly to achieve what...
  2. Why is this not already present in the first place? It seems very unreasonable to have this one protocol behave entirely different from any other and thus require major refactoring for changing a tcp::iostream based project to support secured connections
Exorable answered 22/1, 2019 at 15:6 Comment(2)
A stream is not a socket, so it's not like "this one protocol" behaves "very differently from any other". ssl::stream<socket> defines a stream over a socket, not a "socket adaptor"Hudgens
Well, the problem I have is that the ssl::stream<socket> really does neither: I doesn't give a socket but it also doesn't give me a stream interface that would be compatible to those available from the other protocols and, yes, in that sense it behaves very differently from the others (for no apparent reason). I think I can see where your argument is coming from (ssl being built on another protocol/socket and not doing connection itself), but the same could be said about any other higher-level protocol (e.g. tcp) and good design would provide an agnostic interface for communication.Exorable
H
1

> Well, the problem I have is that the ssl::stream really does neither: I doesn't give a socket but it also doesn't give me a stream interface that would be compatible to those available from the other protocols and, yes, in that sense it behaves very differently from the others (for no apparent reason)

I don't think the stream behaves any differently from the other protocols (see https://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/overview/core/streams.html):

Streams, Short Reads and Short Writes

Many I/O objects in Boost.Asio are stream-oriented. This means that:

There are no message boundaries. The data being transferred is a continuous sequence of bytes.
Read or write operations may transfer fewer bytes than requested. This is referred to as a short read or short write.
Objects that provide stream-oriented I/O model one or more of the following type requirements:

  • SyncReadStream, where synchronous read operations are performed using a member function called read_some().
  • AsyncReadStream, where asynchronous read operations are performed using a member function called async_read_some().
  • SyncWriteStream, where synchronous write operations are performed using a member function called write_some().
  • AsyncWriteStream, where synchronous write operations are performed using a member function called async_write_some().

Examples of stream-oriented I/O objects include ip::tcp::socket, ssl::stream<>, posix::stream_descriptor, windows::stream_handle, etc.

Perhaps the confusion is that you're comparing to the iostream interface, which is simply not the same concept (it comes from the standard library).

To the question how you could make a iostream compatible stream wrapper for the ssl stream, I cannot devise an answer without consulting the documentations more and using a compiler, which I don't have on hand at the moment.

Hudgens answered 25/1, 2019 at 2:37 Comment(2)
Alright, thanks for pointing all that out in detail! For me it really comes down to the iostream interface, as also indicated by the title of my question: Why isn't there one for ssl? There is one for TCP/UDP.. Further, it is unreasonably complicated to implement it myself because the whole class structure is incompatible due to a lack of a common base. This just seems weird given the reference you linked above.. if ip::tcp::socket and ssl::stream<> model the same type requirements, why not model them as such in software?Exorable
That way, all the existing classes that provide the iostream interface (basic_socket_streambuf, basic_socket_iostream, ...) for tcp and udp could be reused easily. But somehow this is not the case and I would like to know if there is a reason for that..? I know solved this by implementing a std::streambuf` derivation around the ssl::stream<> to be used in a regular iostream following the implementation of basic_socket_streambuf. It works now but it seems a waste of time given that all the stuff is basically already there but just can't work together properlyExorable
A
0

I think there is room for improvement in the library here. If you read the ip::tcp::iostream class (i.e. basic_socket_iostream<ip::tcp>), you'll see that it has two base classes:

  • private detail::socket_iostream_base<ip::tcp>
  • public std::basic_iostream<char>

The former contains a basic_socket_streambuf<ip::tcp> (a derived class of std::streambuf and basic_socket<ip::tcp>), whose address is passed to the latter at construction-time.

For the most part, basic_socket_streambuf<ip::tcp> performs the actual socket operations via its basic_socket<ip::tcp> base class. However, there is the connect_to_endpoints() member function that jumps the abstraction and calls several low-level functions from the detail::socket_ops namespace directly on socket().native_handle(). (This seems to have been introduced in Git commit b60e92b13e.) Those functions will only work on TCP sockets, even though the class is a template for any protocol.

Until I discovered this issue, my plan to integrate SSL support as a iostream/streambuf was to provide a ssl protocol class and a basic_socket<ssl> template specialization to wrap the existing ssl::context and ssl::stream<ip::tcp::socket> classes. Something like this (won't compile):

#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/basic_socket.hpp>
#include <boost/asio/ssl.hpp>

namespace boost {
namespace asio {

namespace ip {

class ssl
    : public tcp  // for reuse (I'm lazy!)
{
  public:
    typedef basic_socket_iostream<ssl> iostream;
    // more things as needed ...
};

}  // namespace ip

template <>
class basic_socket<ip::ssl>
{
    class SslContext
    {
        ssl::context ctx;

      public:
        SslContext() : ctx(ssl::context::sslv23_client)
        {
            ctx.set_options(ssl::context::default_workarounds);
            ctx.set_default_verify_paths();
        }

        ssl::context & context() { return ctx; }
    } sslContext;

    ssl::stream<ip::tcp::socket> sslSocket;

  public:
    explicit basic_socket(const executor & ex)
            : sslSocket(ex, sslContext.context())
    {}

    executor get_executor() noexcept
    {
        return sslSocket.lowest_layer().get_executor();
    }

    void connect(const ip::tcp::endpoint & endpoint_)
    {
        sslSocket.next_layer().connect(endpoint_);
        sslSocket.lowest_layer().set_option(ip::tcp::no_delay(true));
        sslSocket.set_verify_mode(ssl::verify_peer);
        sslSocket.set_verify_callback(
            ssl::rfc2818_verification("TODO: pass the domain here through the stream/streambuf somehow"));
        sslSocket.handshake(ssl::stream<ip::tcp::socket>::client);
    }

    void close()
    {
        sslSocket.shutdown();
        sslSocket.next_layer().close();
    }
};

}  // namespace asio
}  // namespace boost

But due to the design issue with basic_socket_streambuf<>::connect_to_endpoints() I'll have to specialize basic_socket_streambuf<ip::ssl> as well, to avoid the detail::socket_ops routines. (I should also avoid injecting the ssl protocol class into the boost::asio::ip namespace, but that's a side concern.)

Haven't spent much time on this, but it seems doable. Fixing basic_socket_streambuf<>::connect_to_endpoints() first should help greatly.

Albanian answered 3/8, 2019 at 18:45 Comment(1)
Hey! Thanks for replying to this rather old thread of mine. I agree with you that there is room for improvment in asio. Unfortunately, I have no in-depth understanding of how everything works within, so I wasn't delving into it deeper previously. My solution at the time was to wrap asios tcp and ssl solutions into my own common interface and have custom std::streambuf and std::iostream derivation working with that. If your continuing with the above, I'd be glad to help, but not sure what I did back then would have much value. But if you'd like some help, PM me maybe so we can coordinate :)Exorable

© 2022 - 2024 — McMap. All rights reserved.