Using SSL sockets and non-SSL sockets simultaneously in Boost.Asio?

10,155

Solution 1

There's a couple of ways you can do this. In the past, I've done something like

if ( sslEnabled )
    boost::asio::async_write( secureSocket_ );
} else {
    boost::asio::async_write( secureSocket_.lowest_layer() );
}

Which can get messy pretty quickly with a lot of if/else statements. You could also create an abstract class (pseudo code - oversimplified)

class Socket
{
    public:
       virtual void connect( ... );
       virtual void accept( ... );
       virtual void async_write( ... );
       virtual void async_read( ... );
    private:
        boost::asio::ip::tcp::socket socket_;
};

Then create a derived class SecureSocket to operate on a secureSocket_ instead of socket_. I don't think it would be duplicating a lot of code, and it's probably cleaner than if/else whenever you need to async_read or async_write.

Solution 2

I'm rather late in answering this question, but I hope this will help others. Sam's answer contains the germ of an idea, but doesn't quit go far enough in my opinion.

The idea came about from the observation that asio wraps an SSL socket in a stream. All this solution does is that it wraps the non-SSL socket similarly.

The desired result of having a uniform external interface between SSL and non-SSL sockets is done with three classes. One, the base, effectively defines the interface:

class Socket {
public:
    virtual boost::asio::ip::tcp::socket &getSocketForAsio() = 0;

    static Socket* create(boost::asio::io_service& iIoService, boost::asio::ssl::context *ipSslContext) {
        // Obviously this has to be in a separate source file since it makes reference to subclasses
        if (ipSslContext == nullptr) {
            return new NonSslSocket(iIoService);
        }
       return new SslSocket(iIoService, *ipSslContext);
    }

    size_t _read(void *ipData, size_t iLength) {
        return boost::asio::read(getSocketForAsio(), boost::asio::buffer(ipData, iLength));
    }
    size_t _write(const void *ipData, size_t iLength) {
        return boost::asio::write(getSocketForAsio(), boost::asio::buffer(ipData, iLength));
    }
};

Two sub-classes wrap SSL and non-SSL sockets.

typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> SslSocket_t;
class SslSocket: public Socket, private SslSocket_t {
public:
    SslSocket(boost::asio::io_service& iIoService, boost::asio::ssl::context &iSslContext) :
        SslSocket_t(iIoService, iSslContext) {
    }

private:
    boost::asio::ip::tcp::socket &getSocketForAsio() {
        return next_layer();
    }
};

and

class NonSslSocket: public Socket, private Socket_t {
public:
    NonSslSocket(boost::asio::io_service& iIoService) :
            Socket_t(iIoService) {
    }

private:
    boost::asio::ip::tcp::socket &getSocketForAsio() {
        return next_layer();
    }
};

Every time you call an asio function use getSocketForAsio(), rather than pass a reference to the Socket object. For example:

boost::asio::async_read(pSocket->getSocketForAsio(),
            boost::asio::buffer(&buffer, sizeof(buffer)),
            boost::bind(&Connection::handleRead,
                    shared_from_this(),
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred));

Notice that the Socket is stored as pointer. I cannot think how else the polymorphism can be hidden.

The penalty (which I don't think great) is the extra level of indirection used to obtain non-SSL sockets.

Solution 3

The problem of course is that tcp::socket and the ssl "socket" don't share the any common ancestor. But most functions for using the socket once it's open share the exact same syntax. The cleanest solution is thus with templates.

template <typename SocketType>
void doStuffWithOpenSocket(SocketType socket) {
   boost::asio::write(socket, ...);
   boost::asio::read(socket, ...);
   boost::asio::read_until(socket, ...);
   // etc...
}

This function will work work with normal tcp::sockets and also secure SSL sockets:

boost::asio::ip::tcp::socket socket_;
// socket_ opened normally ...
doStuffWithOpenSocket<boost::asio::ip::tcp::socket>(socket_); // works!

boost::asio::ssl::stream<boost::asio::ip::tcp::socket> secureSocket_;
// secureSocket_ opened normally (including handshake) ...
doStuffWithOpenSocket(secureSocket_); // also works, with (different) implicit instantiation!
// shutdown the ssl socket when done ...

Solution 4

It would compile with something like this:

typedef boost::asio::buffered_stream<boost::asio::ip::tcp::socket> Socket_t;

Share:
10,155

Related videos on Youtube

DSB
Author by

DSB

Updated on May 16, 2022

Comments

  • DSB
    DSB over 1 year

    I'm in the process of converting a library to Boost.Asio (which has worked very well so far), but I've hit something of a stumbling block with regards to a design decision.

    Boost.Asio provides support for SSL, but a boost::asio::ssl::stream<boost::asio::ip::tcp::socket> type must be used for the socket. My library has the option of connecting to SSL servers or connecting normally, so I've made a class with two sockets like this:

    class client : public boost::enable_shared_from_this<client>
    {
    public:
        client(boost::asio::io_service & io_service, boost::asio::ssl::context & context) : socket_(io_service), secureSocket_(io_service, context) {}
    private:
        boost::asio::ip::tcp::socket socket_;
        boost::asio::ssl::stream<boost::asio::ip::tcp::socket> secureSocket_;
    };
    

    And within there are a bunch of handlers that reference socket_. (For example, I have socket_.is_open() in several places, which would need to become secureSocket_.lowest_layer().is_open() for the other socket.)

    Can anyone suggest the best way to go about this? I'd rather not create a separate class just for this purpose, because that would mean duplicating a lot of code.

    Edit: I rephrased my original question because I misunderstood the purpose of an OpenSSL function.

  • DSB
    DSB almost 13 years
    The first approach is what I ended up doing, but I do like the abstract class idea. I'll take a look at it - thanks.
  • yhager
    yhager over 11 years
    seems like the typedef for Socket_t is missing. Also, can you explain why this works, i.e. what exactly does next_layer() do for each type of socket?
  • Nicole
    Nicole almost 10 years
    @yhager, The way I made this work is to copy the way that an SSL socket derives from a non-SSL socket. Asio creates an SSL socket by wrapping it in a container that provides some additional functionality and that intercepts some activities by adding an SSL flavour to them. Code using such an SSL socket will need to use next_layer() to gain access to the underlying socket for some activities common to both SSL and non-SSL sockets. All my suggestion does is to wrap the socket in such a way that it can be treated as it were an SSL socket, but without implementing any SSL-related activities.
  • Vinnie Falco
    Vinnie Falco about 9 years
    None of this will work correctly. First because you cannot do raw read and write on the underlying ip::tcp::socket of an ssl::stream. And second, it will not work with stackless or stackful coroutines because you do not support customization of the return value via the async_result mechanism.
  • Alexandru Irimiea
    Alexandru Irimiea about 7 years
    I can confirm what Vinnie says. When instantiating the SslSocket with this mechanism, boost::asio::async_write will try to write to the underlying socket which results in nasty errors like "SSL routines:SSL3_GET_RECORD:wrong version number" on server (tested with a Node.JS server).
  • Nicole
    Nicole about 7 years
    You're both probably right. I was still experimenting with this technique at the time. I had yet to discover its limitations.