c++sslwebsocketboostboost-beast

Uninitialized (SSL routines) in an adapted Boost Beast async-ssl websocket example


I'm trying to adapt boost beast's websocket/server/async-ssl/websocket_server_async_ssl.cpp to display dynamically refreshing html tables which are to be viewed by index.html. My code for the server main.cpp compiles and runs on 0.0.0.0:8080 but when the browser views the html file, the server gives an error message accept: uninitialized (SSL routines). I'm using boost v1.81.0.

main.cpp

//------------------------------------------------------------------------------
//
// Example: WebSocket server, asynchronous with SSL/TLS encryption
//
//------------------------------------------------------------------------------

#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <sstream>
#include <chrono>
#include "utils.hpp"

// Generate the HTML table content with random values
std::string generate_html_table()
{
    std::stringstream ss;
    // Generate a random number of rows and columns
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 10);
    int rows = dis(gen);
    int cols = dis(gen);

    // Generate a random HTML table with random values
    ss << "<table>\n";
    for (int i = 0; i < rows; i++) {
        ss << "<tr>\n";
        for (int j = 0; j < cols; j++) {
            std::uniform_int_distribution<> dis(1, 100);
            int value = dis(gen);
            ss << "<td style=\"background-color:rgb(" << value << "," << value << "," << value << ")\">" << value << "</td>\n";
        }
        ss << "</tr>\n";
    }
    ss << "</table>\n";

    return ss.str();
}

namespace beast = boost::beast;         // from <boost/beast.hpp>
namespace http = beast::http;           // from <boost/beast/http.hpp>
namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp>
namespace net = boost::asio;            // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp>
namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp>

//------------------------------------------------------------------------------

// Report a failure
void fail(beast::error_code ec, char const* what)
{
    std::cerr << what << ": " << ec.message() << "\n";
}

// Echoes back all received WebSocket messages
class session : public std::enable_shared_from_this<session>
{
    websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws_;
    beast::flat_buffer buffer_;

public:
    // Take ownership of the socket
    explicit session(tcp::socket&& socket, ssl::context& ctx)
            : ws_(std::move(socket), ctx)
    {
    }

// Get on the correct executor
    void run()
    {
        // We need to be executing within a strand to perform async operations
        // on the I/O objects in this session. Although not strictly necessary
        // for single-threaded contexts, this example code is written to be
        // thread-safe by default.
        net::dispatch(ws_.get_executor(),
                      beast::bind_front_handler(
                              &session::on_run,
                              shared_from_this()));
    }

    // Start the asynchronous operation
    void on_run()
    {
        // Set suggested timeout settings for the websocket
        ws_.set_option(
                websocket::stream_base::timeout::suggested(
                        beast::role_type::server));

        // Set a decorator to change the Server of the handshake
        ws_.set_option(websocket::stream_base::decorator(
                [](websocket::response_type& res)
                {
                    res.set(http::field::server,
                            std::string(BOOST_BEAST_VERSION_STRING) +
                            " websocket-server-async");
                }));

        // Accept the websocket handshake
        ws_.async_accept(
                beast::bind_front_handler(
                        &session::on_accept,
                        shared_from_this()));
    }

    void on_accept(beast::error_code ec)
    {
        if(ec)
            return fail(ec, "accept");

        // Send the colorful HTML table
        send_html_table();
    }

    void send_html_table()
    {
        std::string html;
        for (int i = 0; i < 10; i++) {
            html += generate_html_table(); // Generate the HTML table content
        }

        // Create the WebSocket message with the HTML content
        ws_.text(true);
        ws_.async_write(
                net::buffer(html),
                [self = shared_from_this()](beast::error_code ec, std::size_t bytes_transferred) {
                    if (ec)
                        return fail(ec, "write");

                    // Clear the buffer
                    self->buffer_.consume(self->buffer_.size());

                    // Schedule sending the next HTML table after 1 second
                    self->schedule_next_send();
                });
    }

    void schedule_next_send()
    {
        // Wait for 1 second before sending the next HTML table
        std::this_thread::sleep_for(std::chrono::seconds(1));

        // Check if the socket is still open
        if (ws_.is_open())
        {
            // Send the next HTML table
            send_html_table();
        }
    }
};

//------------------------------------------------------------------------------

// Accepts incoming connections and launches the sessions
class listener : public std::enable_shared_from_this<listener>
{
    net::io_context& ioc_;
    tcp::acceptor acceptor_;
    ssl::context& ctx_;

public:
    listener(
            net::io_context& ioc,
            tcp::endpoint endpoint,
            ssl::context& ctx)
            : ioc_(ioc)
            , acceptor_(ioc)
            , ctx_(ctx)
    {
        beast::error_code ec;

        // Open the acceptor
        acceptor_.open(endpoint.protocol(), ec);
        if(ec)
        {
            fail(ec, "open");
            return;
        }

        // Allow address reuse
        acceptor_.set_option(net::socket_base::reuse_address(true), ec);
        if(ec)
        {
            fail(ec, "set_option");
            return;
        }

        // Bind to the server address
        acceptor_.bind(endpoint, ec);
        if(ec)
        {
            fail(ec, "bind");
            return;
        }

        // Start listening for connections
        acceptor_.listen(
                net::socket_base::max_listen_connections, ec);
        if(ec)
        {
            fail(ec, "listen");
            return;
        }
    }

    // Start accepting incoming connections
    void run()
    {
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(
                net::make_strand(ioc_),
                beast::bind_front_handler(
                        &listener::on_accept,
                        shared_from_this()));
    }

    void on_accept(beast::error_code ec, tcp::socket socket)
    {
        if(ec)
        {
            fail(ec, "accept");
        }
        else
        {
            std::make_shared<session>(std::move(socket), ctx_)->run();
        }

        // Accept another connection
        do_accept();
    }
};

//------------------------------------------------------------------------------

int main(int argc, char* argv[])
{
    auto const address = net::ip::make_address("0.0.0.0");
    auto const port = static_cast<unsigned short>(8080);
    auto const threads = std::max<int>(1, 1);

    // The io_context is required for all I/O
    net::io_context ioc{threads};

    // The SSL context is required, and holds certificates
    ssl::context ctx{ssl::context::tlsv12};

    // Load certificates
    auto path = path_to_project().string();
    ctx.use_certificate_chain_file(path + "/certificates/server.crt");
    ctx.use_private_key_file(path + "/certificates/server.key", ssl::context::pem);
    ctx.use_tmp_dh_file(path + "/certificates/dh2048.pem");

    // Verify the certificate
    ctx.load_verify_file(path + "/certificates/ca.crt");
    ctx.set_verify_mode(ssl::verify_peer);

    // Create and launch a listening port
    std::make_shared<listener>(ioc, tcp::endpoint{address, port}, ctx)->run();

    // Run the I/O service on the requested number of threads
    std::vector<std::thread> v;
    v.reserve(threads - 1);
    for(auto i = threads - 1; i > 0; --i)
        v.emplace_back(
                [&ioc]
                {
                    ioc.run();
                });
    ioc.run();

    return EXIT_SUCCESS;
}


index.html

<!DOCTYPE html>
<html>
<head>
    <title>Dynamic Table</title>
    <style>
        table {
            border-collapse: collapse;
        }
        td {
            border: 1px solid black;
            padding: 5px;
        }
    </style>
    <script>
        var socket = new WebSocket("ws://localhost:8080"); 

        socket.onmessage = function(event) {
            var tableContainer = document.getElementById("table-container");
            tableContainer.innerHTML = event.data;
        };

        function refreshTable() {
            socket.send("refresh"); // Send a message to the server to request a table refresh
        }

        // Refresh the table every 1 second
        setInterval(refreshTable, 1000);
    </script>
</head>
<body>
<h1>Dynamic Table</h1>
<div id="table-container"></div>
</body>
</html>

I suspect that the bug is in the on_accept() method of the listener class. Somehow the SSL is not initilized to this point.

    void on_accept(beast::error_code ec, tcp::socket socket)
    {
        if(ec)
        {
            fail(ec, "accept");
        }
        else
        {
            std::make_shared<session>(std::move(socket), ctx_)->run();
        }

        // Accept another connection
        do_accept();
    }

The error is caught in the line fail(ec, "accept").

How can I fix this?


Solution

  • As evidenced from this line

    res.set(http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-server-async");
    

    your code was in actual practice (partly) based on the non-SSL version of the example program. This leads to more than one thing missing:

    Your own intentional changes also have some problems:

    Buried under all this is the problem where you fail to set a password callback for the server key. This was inside load_server_certificate originally in the SSL example. We can't really tell what options are fine for your certificates, because we don't have the files.

    I do think that set_verify_mode(ssl::verify_peer) is typically related to the client-side in SSL exchange.

    Fixed Demo

    Ignoring the missing bits, here's the code with the mentioned problems fixed, and working:

    Live On Coliru

    // #include "utils.hpp"
    #include <filesystem>
    std::filesystem::path path_to_project() { return "."; }
    
    #include <algorithm>
    #include <boost/asio/dispatch.hpp>
    #include <boost/asio/strand.hpp>
    #include <boost/beast/core.hpp>
    #include <boost/beast/ssl.hpp>
    #include <boost/beast/websocket.hpp>
    #include <chrono>
    #include <iostream>
    #include <memory>
    #include <random>
    #include <sstream>
    #include <string>
    #include <thread>
    #include <vector>
    
    inline void load_server_certificate(boost::asio::ssl::context& ctx) {
        /*
            The certificate was generated from bash on Ubuntu (OpenSSL 1.1.1f) using:
    
            openssl dhparam -out dh.pem 2048
            openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "/C=US/ST=CA/L=Los Angeles/O=Beast/CN=www.example.com"
        */
    
        std::string const cert =
            "-----BEGIN CERTIFICATE-----\n"
            "MIIDlTCCAn2gAwIBAgIUOLxr3q7Wd/pto1+2MsW4fdRheCIwDQYJKoZIhvcNAQEL\n"
            "BQAwWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtMb3MgQW5n\n"
            "ZWxlczEOMAwGA1UECgwFQmVhc3QxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAe\n"
            "Fw0yMTA3MDYwMTQ5MjVaFw00ODExMjEwMTQ5MjVaMFoxCzAJBgNVBAYTAlVTMQsw\n"
            "CQYDVQQIDAJDQTEUMBIGA1UEBwwLTG9zIEFuZ2VsZXMxDjAMBgNVBAoMBUJlYXN0\n"
            "MRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB\n"
            "DwAwggEKAoIBAQCz0GwgnxSBhygxBdhTHGx5LDLIJSuIDJ6nMwZFvAjdhLnB/vOT\n"
            "Lppr5MKxqQHEpYdyDYGD1noBoz4TiIRj5JapChMgx58NLq5QyXkHV/ONT7yi8x05\n"
            "P41c2F9pBEnUwUxIUG1Cb6AN0cZWF/wSMOZ0w3DoBhnl1sdQfQiS25MTK6x4tATm\n"
            "Wm9SJc2lsjWptbyIN6hFXLYPXTwnYzCLvv1EK6Ft7tMPc/FcJpd/wYHgl8shDmY7\n"
            "rV+AiGTxUU35V0AzpJlmvct5aJV/5vSRRLwT9qLZSddE9zy/0rovC5GML6S7BUC4\n"
            "lIzJ8yxzOzSStBPxvdrOobSSNlRZIlE7gnyNAgMBAAGjUzBRMB0GA1UdDgQWBBR+\n"
            "dYtY9zmFSw9GYpEXC1iJKHC0/jAfBgNVHSMEGDAWgBR+dYtY9zmFSw9GYpEXC1iJ\n"
            "KHC0/jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBzKrsiYywl\n"
            "RKeB2LbddgSf7ahiQMXCZpAjZeJikIoEmx+AmjQk1bam+M7WfpRAMnCKooU+Utp5\n"
            "TwtijjnJydkZHFR6UH6oCWm8RsUVxruao/B0UFRlD8q+ZxGd4fGTdLg/ztmA+9oC\n"
            "EmrcQNdz/KIxJj/fRB3j9GM4lkdaIju47V998Z619E/6pt7GWcAySm1faPB0X4fL\n"
            "FJ6iYR2r/kJLoppPqL0EE49uwyYQ1dKhXS2hk+IIfA9mBn8eAFb/0435A2fXutds\n"
            "qhvwIOmAObCzcoKkz3sChbk4ToUTqbC0TmFAXI5Upz1wnADzjpbJrpegCA3pmvhT\n"
            "7356drqnCGY9\n"
            "-----END CERTIFICATE-----\n";
    
        std::string const key =
            "-----BEGIN PRIVATE KEY-----\n"
            "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCz0GwgnxSBhygx\n"
            "BdhTHGx5LDLIJSuIDJ6nMwZFvAjdhLnB/vOTLppr5MKxqQHEpYdyDYGD1noBoz4T\n"
            "iIRj5JapChMgx58NLq5QyXkHV/ONT7yi8x05P41c2F9pBEnUwUxIUG1Cb6AN0cZW\n"
            "F/wSMOZ0w3DoBhnl1sdQfQiS25MTK6x4tATmWm9SJc2lsjWptbyIN6hFXLYPXTwn\n"
            "YzCLvv1EK6Ft7tMPc/FcJpd/wYHgl8shDmY7rV+AiGTxUU35V0AzpJlmvct5aJV/\n"
            "5vSRRLwT9qLZSddE9zy/0rovC5GML6S7BUC4lIzJ8yxzOzSStBPxvdrOobSSNlRZ\n"
            "IlE7gnyNAgMBAAECggEAY0RorQmldGx9D7M+XYOPjsWLs1px0cXFwGA20kCgVEp1\n"
            "kleBeHt93JqJsTKwOzN2tswl9/ZrnIPWPUpcbBlB40ggjzQk5k4jBY50Nk2jsxuV\n"
            "9A9qzrP7AoqhAYTQjZe42SMtbkPZhEeOyvCqxBAi6csLhcv4eB4+In0kQo7dfvLs\n"
            "Xu/3WhSsuAWqdD9EGnhD3n+hVTtgiasRe9318/3R9DzP+IokoQGOtXm+1dsfP0mV\n"
            "8XGzQHBpUtJNn0yi6SC4kGEQuKkX33zORlSnZgT5VBLofNgra0THd7x3atOx1lbr\n"
            "V0QizvCdBa6j6FwhOQwW8UwgOCnUbWXl/Xn4OaofMQKBgQDdRXSMyys7qUMe4SYM\n"
            "Mdawj+rjv0Hg98/xORuXKEISh2snJGKEwV7L0vCn468n+sM19z62Axz+lvOUH8Qr\n"
            "hLkBNqJvtIP+b0ljRjem78K4a4qIqUlpejpRLw6a/+44L76pMJXrYg3zdBfwzfwu\n"
            "b9NXdwHzWoNuj4v36teGP6xOUwKBgQDQCT52XX96NseNC6HeK5BgWYYjjxmhksHi\n"
            "stjzPJKySWXZqJpHfXI8qpOd0Sd1FHB+q1s3hand9c+Rxs762OXlqA9Q4i+4qEYZ\n"
            "qhyRkTsl+2BhgzxmoqGd5gsVT7KV8XqtuHWLmetNEi+7+mGSFf2iNFnonKlvT1JX\n"
            "4OQZC7ntnwKBgH/ORFmmaFxXkfteFLnqd5UYK5ZMvGKTALrWP4d5q2BEc7HyJC2F\n"
            "+5lDR9nRezRedS7QlppPBgpPanXeO1LfoHSA+CYJYEwwP3Vl83Mq/Y/EHgp9rXeN\n"
            "L+4AfjEtLo2pljjnZVDGHETIg6OFdunjkXDtvmSvnUbZBwG11bMnSAEdAoGBAKFw\n"
            "qwJb6FNFM3JnNoQctnuuvYPWxwM1yjRMqkOIHCczAlD4oFEeLoqZrNhpuP8Ij4wd\n"
            "GjpqBbpzyVLNP043B6FC3C/edz4Lh+resjDczVPaUZ8aosLbLiREoxE0udfWf2dU\n"
            "oBNnrMwwcs6jrRga7Kr1iVgUSwBQRAxiP2CYUv7tAoGBAKdPdekPNP/rCnHkKIkj\n"
            "o13pr+LJ8t+15vVzZNHwPHUWiYXFhG8Ivx7rqLQSPGcuPhNss3bg1RJiZAUvF6fd\n"
            "e6QS4EZM9dhhlO2FmPQCJMrRVDXaV+9TcJZXCbclQnzzBus9pwZZyw4Anxo0vmir\n"
            "nOMOU6XI4lO9Xge/QDEN4Y2R\n"
            "-----END PRIVATE KEY-----\n";
    
        std::string const dh =
            "-----BEGIN DH PARAMETERS-----\n"
            "MIIBCAKCAQEArzQc5mpm0Fs8yahDeySj31JZlwEphUdZ9StM2D8+Fo7TMduGtSi+\n"
            "/HRWVwHcTFAgrxVdm+dl474mOUqqaz4MpzIb6+6OVfWHbQJmXPepZKyu4LgUPvY/\n"
            "4q3/iDMjIS0fLOu/bLuObwU5ccZmDgfhmz1GanRlTQOiYRty3FiOATWZBRh6uv4u\n"
            "tff4A9Bm3V9tLx9S6djq31w31Gl7OQhryodW28kc16t9TvO1BzcV3HjRPwpe701X\n"
            "oEEZdnZWANkkpR/m/pfgdmGPU66S2sXMHgsliViQWpDCYeehrvFRHEdR9NV+XJfC\n"
            "QMUk26jPTIVTLfXmmwU0u8vUkpR7LQKkwwIBAg==\n"
            "-----END DH PARAMETERS-----\n";
        
        ctx.set_password_callback(
            [](std::size_t,
                boost::asio::ssl::context_base::password_purpose)
            {
                return "test";
            });
    
        ctx.set_options(
            boost::asio::ssl::context::default_workarounds |
            boost::asio::ssl::context::no_sslv2 |
            boost::asio::ssl::context::single_dh_use);
    
        ctx.use_certificate_chain(
            boost::asio::buffer(cert.data(), cert.size()));
    
        ctx.use_private_key(
            boost::asio::buffer(key.data(), key.size()),
            boost::asio::ssl::context::file_format::pem);
    
        ctx.use_tmp_dh(
            boost::asio::buffer(dh.data(), dh.size()));
    }
    
    // Generate the HTML table content with random values
    std::string generate_html_table() {
        std::stringstream ss;
        // Generate a random number of rows and columns
        std::random_device              rd;
        std::mt19937                    gen(rd());
        std::uniform_int_distribution<> dis(1, 10);
        int                             rows = dis(gen);
        int                             cols = dis(gen);
        dis = std::uniform_int_distribution<>(1, 100);
    
        // Generate a random HTML table with random values
        ss << "<table>\n";
        for (int i = 0; i < rows; i++) {
            ss << "<tr>\n";
            for (int j = 0; j < cols; j++) {
                int value = dis(gen);
                ss << "<td style=\"background-color:rgb(" << value << "," << value << "," << value << ")\">"
                   << value << "</td>\n";
            }
            ss << "</tr>\n";
        }
        ss << "</table>\n";
    
        return ss.str();
    }
    
    namespace beast     = boost::beast;         // from <boost/beast.hpp>
    namespace http      = beast::http;          // from <boost/beast/http.hpp>
    namespace websocket = beast::websocket;     // from <boost/beast/websocket.hpp>
    namespace net       = boost::asio;          // from <boost/asio.hpp>
    namespace ssl       = boost::asio::ssl;     // from <boost/asio/ssl.hpp>
    using tcp           = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
    
    //------------------------------------------------------------------------------
    
    // Report a failure
    void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; }
    
    // Echoes back all received WebSocket messages
    class session : public std::enable_shared_from_this<session> {
        websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws_;
        beast::flat_buffer                                      buffer_;
    
      public:
        // Take ownership of the socket
        session(tcp::socket&& socket, ssl::context& ctx) : ws_(std::move(socket), ctx) {}
    
        // Get on the correct executor
        void run() {
            // We need to be executing within a strand to perform async operations
            // on the I/O objects in this session. Although not strictly necessary
            // for single-threaded contexts, this example code is written to be
            // thread-safe by default.
            net::dispatch(ws_.get_executor(), beast::bind_front_handler(&session::on_run, shared_from_this()));
        }
    
        // Start the asynchronous operation
        void on_run() {
            // Set the timeout.
            beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30));
    
            // Perform the SSL handshake
            ws_.next_layer().async_handshake(
                ssl::stream_base::server, beast::bind_front_handler(&session::on_handshake, shared_from_this()));
        }
    
        void on_handshake(beast::error_code ec) {
            if (ec)
                return fail(ec, "handshake");
    
            // Turn off the timeout on the tcp_stream, because
            // the websocket stream has its own timeout system.
            beast::get_lowest_layer(ws_).expires_never();
    
            // Set suggested timeout settings for the websocket
            ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
    
            // Set a decorator to change the Server of the handshake
            ws_.set_option(websocket::stream_base::decorator([](websocket::response_type& res) {
                res.set(http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " stackoverflow");
            }));
    
            // Accept the websocket handshake
            ws_.async_accept(beast::bind_front_handler(&session::on_accept, shared_from_this()));
        }
    
        void on_accept(beast::error_code ec) {
            if (ec)
                return fail(ec, "accept");
    
            // Send the colorful HTML table
            send_html_table();
        }
    
        std::string html;
    
        void send_html_table() {
            html.clear();
            for (int i = 0; i < 10; i++) {
                html += generate_html_table(); // Generate the HTML table content
            }
    
            // Create the WebSocket message with the HTML content
            ws_.text(true);
            ws_.async_write(net::buffer(html),
                            [self = shared_from_this()](beast::error_code ec, std::size_t /*bytes_transferred*/) {
                                if (ec)
                                    return fail(ec, "write");
    
                                // Clear the buffer
                                self->buffer_.consume(self->buffer_.size());
    
                                // Schedule sending the next HTML table after 1 second
                                self->schedule_next_send();
                            });
        }
    
        void schedule_next_send() {
            // Wait for 1 second before sending the next HTML table
            std::this_thread::sleep_for(std::chrono::seconds(1));
    
            // Check if the socket is still open
            if (ws_.is_open()) {
                // Send the next HTML table
                send_html_table();
            }
        }
    };
    
    //------------------------------------------------------------------------------
    
    // Accepts incoming connections and launches the sessions
    class listener : public std::enable_shared_from_this<listener> {
        net::io_context& ioc_;
        ssl::context&    ctx_;
        tcp::acceptor    acceptor_;
    
      public:
        listener(net::io_context& ioc, tcp::endpoint endpoint, ssl::context& ctx)
            : ioc_(ioc)
            , ctx_(ctx)
            , acceptor_(net::make_strand(ioc)) {
            beast::error_code ec;
    
            // Open the acceptor
            acceptor_.open(endpoint.protocol(), ec);
            if (ec) {
                fail(ec, "open");
                return;
            }
    
            // Allow address reuse
            acceptor_.set_option(net::socket_base::reuse_address(true), ec);
            if (ec) {
                fail(ec, "set_option");
                return;
            }
    
            // Bind to the server address
            acceptor_.bind(endpoint, ec);
            if (ec) {
                fail(ec, "bind");
                return;
            }
    
            // Start listening for connections
            acceptor_.listen(net::socket_base::max_listen_connections, ec);
            if (ec) {
                fail(ec, "listen");
                return;
            }
        }
    
        // Start accepting incoming connections
        void run() { do_accept(); }
    
      private:
        void do_accept() {
            // The new connection gets its own strand
            acceptor_.async_accept(net::make_strand(ioc_),
                                   beast::bind_front_handler(&listener::on_accept, shared_from_this()));
        }
    
        void on_accept(beast::error_code ec, tcp::socket socket) {
            if (ec) {
                fail(ec, "accept");
            } else {
                // Create the session and run it
                std::make_shared<session>(std::move(socket), ctx_)->run();
            }
    
            // Accept another connection
            do_accept();
        }
    };
    
    //------------------------------------------------------------------------------
    
    int main() {
        auto const     address = net::ip::make_address("0.0.0.0");
        uint16_t const port    = 8080;
        auto const     threads = 1;
    
        // The io_context is required for all I/O
        net::io_context ioc{threads};
    
        // The SSL context is required, and holds certificates
        ssl::context ctx{ssl::context::tlsv12};
    
        // Load certificates
        auto path = path_to_project(); // TODO
    
        load_server_certificate(ctx); // instead for now
    
        // Create and launch a listening port
        std::make_shared<listener>(ioc, tcp::endpoint{address, port}, ctx)->run();
    
        // Run the I/O service on the requested number of threads
        std::vector<std::thread> v;
        v.reserve(threads - 1);
        for (auto i = threads - 1; i > 0; --i)
            v.emplace_back([&ioc] { ioc.run(); });
        ioc.run();
    }
    

    Compilation is too heavy for Coliru (in fact compiling with -fsanitize=undefined,address took over 8min on my computer...). So here's a live local demo:

    enter image description here