Beast Logo

PrevUpHomeNext

Example: Detect SSL

In this example we will build a simple function to detect the presence of the SSL handshake given an input buffer sequence. Then we build on the example by adding a synchronous stream algorithm. Finally, we implemement an asynchronous detection function using a composed operation. This SSL detector may be used to allow a server to accept both SSL/TLS and unencrypted connections at the same port.

Here is the declaration for a function to detect the SSL client handshake. The input to the function is simply a buffer sequence, no stream. This allows the detection algorithm to be used elsewhere.

#include <beast.hpp>
#include <boost/logic/tribool.hpp>

/** Return `true` if a buffer contains a TLS/SSL client handshake.

    This function returns `true` if the beginning of the buffer
    indicates that a TLS handshake is being negotiated, and that
    there are at least four octets in the buffer.

    If the content of the buffer cannot possibly be a TLS handshake
    request, the function returns `false`. Otherwise, if additional
    octets are required, `boost::indeterminate` is returned.

    @param buffer The input buffer to inspect. This type must meet
    the requirements of @b ConstBufferSequence.

    @return `boost::tribool` indicating whether the buffer contains
    a TLS client handshake, does not contain a handshake, or needs
    additional octets.

    @see

    http://www.ietf.org/rfc/rfc2246.txt
    7.4. Handshake protocol
*/
template<class ConstBufferSequence>
boost::tribool
is_ssl_handshake(ConstBufferSequence const& buffers);

The implementation checks the buffer for the presence of the SSL Handshake message octet sequence and returns an apporopriate value:

template<
    class ConstBufferSequence>
boost::tribool
is_ssl_handshake(
    ConstBufferSequence const& buffers)
{
    // Make sure buffers meets the requirements
    static_assert(is_const_buffer_sequence<ConstBufferSequence>::value,
        "ConstBufferSequence requirements not met");

    // We need at least one byte to really do anything
    if(boost::asio::buffer_size(buffers) < 1)
        return boost::indeterminate;

    // Extract the first byte, which holds the
    // "message" type for the Handshake protocol.
    unsigned char v;
    boost::asio::buffer_copy(boost::asio::buffer(&v, 1), buffers);

    // Check that the message type is "SSL Handshake" (rfc2246)
    if(v != 0x16)
    {
        // This is definitely not a handshake
        return false;
    }

    // At least four bytes are needed for the handshake
    // so make sure that we get them before returning `true`
    if(boost::asio::buffer_size(buffers) < 4)
        return boost::indeterminate;

    // This can only be a TLS/SSL handshake
    return true;
}

Now we define a stream operation. We start with the simple, synchronous version which takes the stream and buffer as input:

/** Detect a TLS/SSL handshake on a stream.

    This function reads from a stream to determine if a TLS/SSL
    handshake is being received. The function call will block
    until one of the following conditions is true:

    @li The disposition of the handshake is determined

    @li An error occurs

    Octets read from the stream will be stored in the passed dynamic
    buffer, which may be used to perform the TLS handshake if the
    detector returns true, or otherwise consumed by the caller based
    on the expected protocol.

    @param stream The stream to read from. This type must meet the
    requirements of @b SyncReadStream.

    @param buffer The dynamic buffer to use. This type must meet the
    requirements of @b DynamicBuffer.

    @param ec Set to the error if any occurred.

    @return `boost::tribool` indicating whether the buffer contains
    a TLS client handshake, does not contain a handshake, or needs
    additional octets. If an error occurs, the return value is
    undefined.
*/
template<
    class SyncReadStream,
    class DynamicBuffer>
boost::tribool
detect_ssl(
    SyncReadStream& stream,
    DynamicBuffer& buffer,
    error_code& ec)
{
    // Make sure arguments meet the requirements
    static_assert(is_sync_read_stream<SyncReadStream>::value,
        "SyncReadStream requirements not met");
    static_assert(is_dynamic_buffer<DynamicBuffer>::value,
        "DynamicBuffer requirements not met");

    // Loop until an error occurs or we get a definitive answer
    for(;;)
    {
        // There could already be data in the buffer
        // so we do this first, before reading from the stream.
        auto const result = is_ssl_handshake(buffer.data());

        // If we got an answer, return it
        if(! boost::indeterminate(result))
        {
            ec = {}; // indicate no error
            return result;
        }

        // The algorithm should never need more than 4 bytes
        BOOST_ASSERT(buffer.size() < 4);

        // Create up to 4 bytes of space in the buffer's output area.
        auto const mutable_buffer = buffer.prepare(4 - buffer.size());

        // Try to fill our buffer by reading from the stream
        std::size_t const bytes_transferred = stream.read_some(mutable_buffer, ec);

        // Check for an error
        if(ec)
            break;

        // Commit what we read into the buffer's input area.
        buffer.commit(bytes_transferred);
    }

    // error
    return false;
}

The synchronous algorithm is the model for building the asynchronous operation which has more boilerplate. First, we declare the asynchronous initiating function:

/** Detect a TLS/SSL handshake asynchronously on a stream.

    This function is used to asynchronously determine if a TLS/SSL
    handshake is being received.
    The function call always returns immediately. The asynchronous
    operation will continue until one of the following conditions
    is true:

    @li The disposition of the handshake is determined

    @li An error occurs

    This operation is implemented in terms of zero or more calls to
    the next layer's `async_read_some` function, and is known as a
    <em>composed operation</em>. The program must ensure that the
    stream performs no other operations until this operation completes.

    Octets read from the stream will be stored in the passed dynamic
    buffer, which may be used to perform the TLS handshake if the
    detector returns true, or otherwise consumed by the caller based
    on the expected protocol.

    @param stream The stream to read from. This type must meet the
    requirements of @b AsyncReadStream.

    @param buffer The dynamic buffer to use. This type must meet the
    requirements of @b DynamicBuffer.

    @param handler The handler to be called when the request
    completes. Copies will be made of the handler as required.
    The equivalent function signature of the handler must be:
    @code
    void handler(
        error_code const& error,    // Set to the error, if any
        boost::tribool result       // The result of the detector
    );
    @endcode
    Regardless of whether the asynchronous operation completes
    immediately or not, the handler will not be invoked from within
    this function. Invocation of the handler will be performed in a
    manner equivalent to using `boost::asio::io_service::post`.
*/
template<
    class AsyncReadStream,
    class DynamicBuffer,
    class CompletionToken>
async_return_type< 1
    CompletionToken,
    void(error_code, boost::tribool)> 2
async_detect_ssl(
    AsyncReadStream& stream,
    DynamicBuffer& buffer,
    CompletionToken&& token);

1

The async_return_type customizes the return value based on the completion token

2

This is the signature for the completion handler

The implementation of the initiating function is straightforward and contains mostly boilerplate. It is to construct the return type customization helper to obtain the actual handler, and then create the composed operation and launch it. The actual code for interacting with the stream is in the composed operation, which is written as a separate class.

// This is the composed operation.
template<
    class AsyncReadStream,
    class DynamicBuffer,
    class Handler>
class detect_ssl_op;

// Here is the implementation of the asynchronous initation function
template<
    class AsyncReadStream,
    class DynamicBuffer,
    class CompletionToken>
async_return_type<
    CompletionToken,
    void(error_code, boost::tribool)>
async_detect_ssl(
    AsyncReadStream& stream,
    DynamicBuffer& buffer,
    CompletionToken&& token)
{
    // Make sure arguments meet the requirements
    static_assert(is_async_read_stream<AsyncReadStream>::value,
        "SyncReadStream requirements not met");
    static_assert(is_dynamic_buffer<DynamicBuffer>::value,
        "DynamicBuffer requirements not met");

    // This helper manages some of the handler's lifetime and
    // uses the result and handler specializations associated with
    // the completion token to help customize the return value.
    //
    beast::async_completion<
        CompletionToken, void(beast::error_code, boost::tribool)> init{token};

    // Create the composed operation and launch it. This is a constructor
    // call followed by invocation of operator(). We use handler_type
    // to convert the completion token into the correct handler type,
    // allowing user defined specializations of the async result template
    // to take effect.
    //
    detect_ssl_op<AsyncReadStream, DynamicBuffer, handler_type<
        CompletionToken, void(error_code, boost::tribool)>>{
            stream, buffer, init.completion_handler}(
                beast::error_code{}, 0);

    // This hook lets the caller see a return value when appropriate.
    // For example this might return std::future<error_code, boost::tribool> if
    // CompletionToken is boost::asio::use_future.
    //
    // If a coroutine is used for the token, the return value from
    // this function will be the `boost::tribool` representing the result.
    //
    return init.result.get();
}

Now we will declare our composed operation. There is a considerable amount of necessary boilerplate to get this right, but the result is worth the effort.

// Read from a stream to invoke is_tls_handshake asynchronously
//
template<
    class AsyncReadStream,
    class DynamicBuffer,
    class Handler>
class detect_ssl_op
{
    // This composed operation has trivial state,
    // so it is just kept inside the class and can
    // be cheaply copied as needed by the implementation.

    // Indicates what step in the operation's state
    // machine to perform next, starting from zero.
    int step_ = 0;

    AsyncReadStream& stream_;
    DynamicBuffer& buffer_;
    Handler handler_;
    boost::tribool result_ = false;

public:
    // Boost.Asio requires that handlers are CopyConstructible.
    // The state for this operation is cheap to copy.
    detect_ssl_op(detect_ssl_op const&) = default;

    // The constructor just keeps references the callers varaibles.
    //
    template<class DeducedHandler>
    detect_ssl_op(AsyncReadStream& stream,
            DynamicBuffer& buffer, DeducedHandler&& handler)
        : stream_(stream)
        , buffer_(buffer)
        , handler_(std::forward<DeducedHandler>(handler))
    {
    }

    // Determines if the next asynchronous operation represents a
    // continuation of the asynchronous flow of control associated
    // with the final handler. If we are past step two, it means
    // we have performed an asynchronous operation therefore any
    // subsequent operation would represent a continuation.
    // Otherwise, we propagate the handler's associated value of
    // is_continuation. Getting this right means the implementation
    // may schedule the invokation of the invoked functions more
    // efficiently.
    //
    friend bool asio_handler_is_continuation(detect_ssl_op* op)
    {
        // This next call is structured to permit argument
        // dependent lookup to take effect.
        using boost::asio::asio_handler_is_continuation;

        // Always use std::addressof to pass the pointer to the handler,
        // otherwise an unwanted overload of operator& may be called instead.
        return op->step_ > 2 ||
            asio_handler_is_continuation(std::addressof(op->handler_));
    }

    // Handler hook forwarding. These free functions invoke the hooks
    // associated with the final completion handler. In effect, they
    // make the Asio implementation treat our composed operation the
    // same way it would treat the final completion handler for the
    // purpose of memory allocation and invocation.
    //
    // Our implementation just passes through the call to the hook
    // associated with the final handler.

    friend void* asio_handler_allocate(std::size_t size, detect_ssl_op* op)
    {
        using boost::asio::asio_handler_allocate;
        return asio_handler_allocate(size, std::addressof(op->handler_));
    }

    friend void asio_handler_deallocate(void* p, std::size_t size, detect_ssl_op* op)
    {
        using boost::asio::asio_handler_deallocate;
        return asio_handler_deallocate(p, size, std::addressof(op->handler_));
    }

    template<class Function>
    friend void asio_handler_invoke(Function&& f, detect_ssl_op* op)
    {
        using boost::asio::asio_handler_invoke;
        return asio_handler_invoke(f, std::addressof(op->handler_));
    }

    // Our main entry point. This will get called as our
    // intermediate operations complete. Definition below.
    //
    void operator()(beast::error_code ec, std::size_t bytes_transferred);
};

The boilerplate is all done, and now we need to implement the function call operator that turns this composed operation a completion handler with the signature void(error_code, std::size_t) which is exactly the signature needed when performing asynchronous reads. This function is a transformation of the synchronous version of detect_ssl above, but with the inversion of flow that characterizes code written in the callback style:

// detect_ssl_op is callable with the signature
// void(error_code, bytes_transferred),
// allowing `*this` to be used as a ReadHandler
//
template<
    class AsyncStream,
    class DynamicBuffer,
    class Handler>
void
detect_ssl_op<AsyncStream, DynamicBuffer, Handler>::
operator()(beast::error_code ec, std::size_t bytes_transferred)
{
    // Execute the state machine
    switch(step_)
    {
    // Initial state
    case 0:
        // See if we can detect the handshake
        result_ = is_ssl_handshake(buffer_.data());

        // If there's a result, call the handler
        if(! boost::indeterminate(result_))
        {
            // We need to invoke the handler, but the guarantee
            // is that the handler will not be called before the
            // call to async_detect_ssl returns, so we must post
            // the operation to the io_service. The helper function
            // `bind_handler` lets us bind arguments in a safe way
            // that preserves the type customization hooks of the
            // original handler.
            step_ = 1;
            return stream_.get_io_service().post(
                bind_handler(std::move(*this), ec, 0));
        }

        // The algorithm should never need more than 4 bytes
        BOOST_ASSERT(buffer_.size() < 4);

        step_ = 2;

    do_read:
        // We need more bytes, but no more than four total.
        return stream_.async_read_some(buffer_.prepare(4 - buffer_.size()), std::move(*this));

    case 1:
        // Call the handler
        break;

    case 2:
        // Set this so that asio_handler_is_continuation knows that
        // the next asynchronous operation represents a continuation
        // of the initial asynchronous operation.
        step_ = 3;
        BOOST_FALLTHROUGH;

    case 3:
        if(ec)
        {
            // Deliver the error to the handler
            result_ = false;

            // We don't need bind_handler here because we were invoked
            // as a result of an intermediate asynchronous operation.
            break;
        }

        // Commit the bytes that we read
        buffer_.commit(bytes_transferred);

        // See if we can detect the handshake
        result_ = is_ssl_handshake(buffer_.data());

        // If it is detected, call the handler
        if(! boost::indeterminate(result_))
        {
            // We don't need bind_handler here because we were invoked
            // as a result of an intermediate asynchronous operation.
            break;
        }

        // Read some more
        goto do_read;
    }

    // Invoke the final handler.
    handler_(ec, result_);
}

This SSL detector is used by the server framework in the example directory.


PrevUpHomeNext