diff options
| -rw-r--r-- | TODO | 1 | ||||
| -rw-r--r-- | http.cpp | 278 | ||||
| -rw-r--r-- | http.h | 11 | ||||
| -rw-r--r-- | https.cpp | 818 | ||||
| -rw-r--r-- | https.h | 27 | ||||
| -rw-r--r-- | response.h | 2 | ||||
| -rw-r--r-- | server.cpp | 335 | ||||
| -rw-r--r-- | server.h | 3 | ||||
| -rw-r--r-- | websocket.cpp | 302 | ||||
| -rw-r--r-- | websocket.h | 314 | 
10 files changed, 975 insertions, 1116 deletions
@@ -5,7 +5,6 @@ test:  - Redirect  Big file bug  - dynamic plugin interface (file buffer, ...) -CRTP: http+https  FastCGI from command line  stats.png @@ -30,281 +30,3 @@ namespace net = boost::asio;            // from <boost/asio.hpp>  using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp>  namespace websocket = beast::websocket; -namespace { - -//------------------------------------------------------------------------------ - -// Report a failure -void fail(beast::error_code ec, char const* what) -{ - std::cerr << what << ": " << ec.message() << "\n"; -} - -// Handles an HTTP server connection -class session : public std::enable_shared_from_this<session> -{ - boost::asio::io_context& ioc_; - beast::tcp_stream stream_; - beast::flat_buffer buffer_; - Server& m_server; - std::optional<http::request_parser<http::string_body>> parser_; - request_type req_; - std::shared_ptr<response_type> res_; // std::shared_ptr<void> ? - - void handle_request(::Server& server, request_type&& req) - { -  stream_.expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client -  auto sp = std::make_shared<response_type>(response::generate_response(req, server)); - -  res_ = sp; - -  // Write the response -  http::async_write( -      stream_, -      *sp, -      beast::bind_front_handler( -          &session::on_write, -          shared_from_this(), -          sp->need_eof())); - } - - void handle_websocket() - { -  beast::get_lowest_layer(stream_).expires_never(); -  make_websocket_session(ioc_, std::move(stream_), response::get_websocket_address(req_, m_server), parser_->release()); - } - -public: -    // Take ownership of the stream -    session( -        boost::asio::io_context& ioc, -        tcp::socket&& socket, -        Server& server): -         ioc_(ioc), -         stream_(std::move(socket)), -         m_server(server) -    { -    } - -    // Start the asynchronous operation -    void run() -    { -        // We need to be executing within a strand to perform async operations -        // on the I/O objects in this session. -        net::dispatch(stream_.get_executor(), -                      beast::bind_front_handler( -                          &session::do_read, -                          shared_from_this())); -    } - -    void do_read() -    { -        // Make the request empty before reading, -        // otherwise the operation behavior is undefined. -        req_ = {}; -        -        // this is the way to reset the parser. it's necessary. -        // https://github.com/boostorg/beast/issues/927 -        parser_.emplace(); -        parser_->body_limit(1000000000); // 1GB limit - -        // Set the timeout. -        stream_.expires_after(std::chrono::seconds(30)); - -        // Read a request -        http::async_read(stream_, buffer_, *parser_, -            beast::bind_front_handler( -                &session::on_read, -                shared_from_this())); -    } - -    void -    on_read( -        beast::error_code ec, -        std::size_t bytes_transferred -        ) -    { -        boost::ignore_unused(bytes_transferred); - -        // This means they closed the connection -        if (ec == http::error::end_of_stream) -            return do_close(); - -        if (ec == http::error::partial_message) -         return; // ignore - -        if (ec) -            return fail(ec, "read"); - -        req_ = parser_->get(); -         -        if (websocket::is_upgrade(req_)) -        { -         handle_websocket(); -         return; -        } -         -        // Send the response -        handle_request(m_server, std::move(req_)); -    } - -    void -    on_write( -        bool close, -        beast::error_code ec, -        std::size_t bytes_transferred -        ) -    { -        boost::ignore_unused(bytes_transferred); - -        if(ec) -            return fail(ec, "write"); - -        if(close) -        { -            // This means we should close the connection, usually because -            // the response indicated the "Connection: close" semantic. -            return do_close(); -        } - -        // We're done with the response so delete it -        res_ = nullptr; - -        // Read another request -        do_read(); -    } - -    void -    do_close() -    { -        // Send a TCP shutdown -        beast::error_code ec; -        stream_.socket().shutdown(tcp::socket::shutdown_send, ec); -        // At this point the connection is closed gracefully -    } -}; - -//------------------------------------------------------------------------------ - -// Accepts incoming connections and launches the sessions -class listener : public std::enable_shared_from_this<listener> -{ -    net::io_context& ioc_; -    tcp::acceptor acceptor_; -    Server& m_server; - -public: -    listener( -        net::io_context& ioc, -        tcp::endpoint endpoint, -        Server& server) -        : ioc_(ioc) -        , acceptor_(net::make_strand(ioc)) -        , m_server(server) -    { -        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>( -                ioc_, -                std::move(socket), -                m_server)->run(); -        } - -        // Accept another connection -        do_accept(); -    } -}; - -} // anonymous namespace - -//------------------------------------------------------------------------------ - -namespace HTTP { - - Server::Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics) -  : ::Server(config, ioc, socket, plugins, statistics) - { - } - - Server::~Server() - { - } - - int Server::start() - { -  auto const address = net::ip::make_address(m_socket.address); -  auto const port = static_cast<unsigned short>(std::atoi(m_socket.port.data())); - -  // Create and launch a listening port -  std::make_shared<listener>( -   m_ioc, -   tcp::endpoint{address, port}, -   *this)->run(); - -  return EXIT_SUCCESS; - } - -} // namespace HTTP @@ -6,14 +6,3 @@  #include "config.h"  #include "server.h" -namespace HTTP { - -class Server: public ::Server -{ -public: - Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics); - virtual ~Server(); - int start() override; -}; - -} // namespace HTTP @@ -6,8 +6,6 @@  #include "response.h"  #include "websocket.h" -#include "libreichwein/file.h" -  #include <openssl/ssl.h>  #include <openssl/crypto.h> @@ -46,526 +44,420 @@ using namespace Reichwein;  namespace { -//------------------------------------------------------------------------------ -  // Handles an HTTP server connection -class session : public std::enable_shared_from_this<session> +template<class Derived> +class session  { +private: + Derived& derived() + { +  return static_cast<Derived&>(*this); + } +   boost::asio::io_context& ioc_; - beast::ssl_stream<beast::tcp_stream> stream_;   beast::flat_buffer buffer_; - Server& m_server; + Server& server_;   std::optional<http::request_parser<http::string_body>> parser_; // need to reset parser every time, no other mechanism currently   request_type req_; - std::shared_ptr<response_type> res_; // std::shared_ptr<void> + std::shared_ptr<response_type> res_;   void handle_request()   { -  beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client -  auto sp = std::make_shared<response_type>(response::generate_response(req_, m_server)); +  beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client +  auto sp = std::make_shared<response_type>(response::generate_response(req_, server_));    res_ = sp;    // Write the response    http::async_write( -      stream_, +      derived().stream(),        *sp,        beast::bind_front_handler(            &session::on_write, -          shared_from_this(), +          derived().shared_from_this(),            sp->need_eof()));   }   void handle_websocket()   { -  beast::get_lowest_layer(stream_).expires_never(); -  make_websocket_session(ioc_, std::move(stream_), response::get_websocket_address(req_, m_server), parser_->release()); +  beast::get_lowest_layer(derived().stream()).expires_never(); +  make_websocket_session(ioc_, std::move(derived().stream()), response::get_websocket_address(req_, server_), parser_->release());   }  public: -    // Take ownership of the socket -    explicit -    session( -        boost::asio::io_context& ioc, -        tcp::socket&& socket, -        ssl::context& ctx, -        Server& server): -         ioc_(ioc), -         stream_(std::move(socket), ctx), -         m_server(server) -    { -    } - -    // Start the asynchronous operation -    void -    run() -    { -        // We need to be executing within a strand to perform async operations -        // on the I/O objects in this session. -        net::dispatch( -            stream_.get_executor(), -            beast::bind_front_handler( -                &session::on_run, -                shared_from_this())); -    } - -    void -    on_run() -    { -        // Set the timeout. -        beast::get_lowest_layer(stream_).expires_after( -            std::chrono::seconds(30)); - -        // Perform the SSL handshake -        stream_.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, "https handshake"); - -        do_read(); -    } - -    void -    do_read() -    { -        // Make the request empty before reading, -        // otherwise the operation behavior is undefined. -        req_ = {}; -        -        // this is the way to reset the parser. it's necessary. -        // https://github.com/boostorg/beast/issues/927 -        parser_.emplace(); -        parser_->body_limit(1000000000); // 1GB limit - -        // Set the timeout. -        beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - -        // Read a request -        http::async_read(stream_, buffer_, *parser_, -            beast::bind_front_handler( -                &session::on_read, -                shared_from_this())); -    } - -    void -    on_read( -        beast::error_code ec, -        std::size_t bytes_transferred) -    { -        boost::ignore_unused(bytes_transferred); - -        // This means they closed the connection -        if (ec == http::error::end_of_stream) -            return do_close(); - -        if (ec == http::error::partial_message) -         return; // ignore - -        if (ec) -            return fail(ec, "https read"); - -        req_ = parser_->get(); - -        if (websocket::is_upgrade(req_)) -        { -         handle_websocket(); -         return; -        } -         -        // Send the response -        handle_request(); -    } - -    void -    on_write( -        bool close, -        beast::error_code ec, -        std::size_t bytes_transferred -        ) -    { -        boost::ignore_unused(bytes_transferred); - -        if(ec) -            return fail(ec, "https write"); - -        if(close) -        { -            // This means we should close the connection, usually because -            // the response indicated the "Connection: close" semantic. -            return do_close(); -        } - -        // We're done with the response so delete it -        res_ = nullptr; - -        // Read another request -        do_read(); -    } - -    void -    do_close() -    { -        // Set the timeout. -        beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - -        // Perform the SSL shutdown -        stream_.async_shutdown( -            beast::bind_front_handler( -                &session::on_shutdown, -                shared_from_this())); -    } - -    void -    on_shutdown(beast::error_code ec) -    { -        if(ec) -            return fail(ec, "https shutdown"); - -        // At this point the connection is closed gracefully -    } -}; + explicit + session( +     boost::asio::io_context& ioc, +     Server& server): +      ioc_(ioc), +      server_(server) + { + } -//------------------------------------------------------------------------------ + // Start the asynchronous operation + void + run() + { +  // We need to be executing within a strand to perform async operations +  // on the I/O objects in this session. +  net::dispatch( +      derived().stream().get_executor(), +      beast::bind_front_handler( +          &Derived::on_run, +          derived().shared_from_this())); + } -// 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_; -    ::Server& m_server; + void + do_read() + { +  // Make the request empty before reading, +  // otherwise the operation behavior is undefined. +  req_ = {}; +  +  // this is the way to reset the parser. it's necessary. +  // https://github.com/boostorg/beast/issues/927 +  parser_.emplace(); +  parser_->body_limit(1000000000); // 1GB limit -public: -    listener( -        net::io_context& ioc, -        ssl::context& ctx, -        tcp::endpoint endpoint, -        Server& server) : -        ioc_(ioc), -        ctx_(ctx), -        acceptor_(ioc), -        m_server(server) -    { -        beast::error_code ec; - -        // Open the acceptor -        acceptor_.open(endpoint.protocol(), ec); -        if(ec) -        { -            fail(ec, "https open"); -            return; -        } - -        // Allow address reuse -        acceptor_.set_option(net::socket_base::reuse_address(true), ec); -        if(ec) -        { -            fail(ec, "https set_option"); -            return; -        } - -        // Bind to the server address -        acceptor_.bind(endpoint, ec); -        if(ec) -        { -            fail(ec, "https bind"); -            return; -        } - -        // Start listening for connections -        acceptor_.listen( -            net::socket_base::max_listen_connections, ec); -        if(ec) -        { -            fail(ec, "https listen"); -            return; -        } -    } - -    // Start accepting incoming connections -    void -    run() -    { -        do_accept(); -    } +  // Set the timeout. +  beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30)); + +  // Read a request +  http::async_read(derived().stream(), buffer_, *parser_, +      beast::bind_front_handler( +          &session::on_read, +          derived().shared_from_this())); + } + + void + on_read( +     beast::error_code ec, +     std::size_t bytes_transferred) + { +  boost::ignore_unused(bytes_transferred); + +  // This means they closed the connection +  if (ec == http::error::end_of_stream) +   return derived().do_close(); + +  if (ec == http::error::partial_message) +   return; // ignore + +  if (ec) +   return fail(ec, "http read"); + +  req_ = parser_->get(); + +  if (websocket::is_upgrade(req_)) +  { +   handle_websocket(); +   return; +  } +   +  // Send the response +  handle_request(); + } + + void + on_write( +     bool close, +     beast::error_code ec, +     std::size_t bytes_transferred +     ) + { +  boost::ignore_unused(bytes_transferred); + +  if (ec) +   return fail(ec, "http write"); + +  if (close) +  { +   // This means we should close the connection, usually because +   // the response indicated the "Connection: close" semantic. +   return derived().do_close(); +  } + +  // We're done with the response so delete it +  res_ = nullptr; + +  // Read another request +  do_read(); + } -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, "https accept"); -        } -        else -        { -            // Create the session and run it -            std::make_shared<session>( -                ioc_, -                std::move(socket), -                ctx_, -                m_server)->run(); -        } - -        // Accept another connection -        do_accept(); -    }  }; -/*  Load a signed certificate into the ssl context, and configure -    the context for use with a server. -*/ -void load_server_certificate(boost::asio::ssl::context& ctx, const fs::path& cert_path, const fs::path& key_path) +class plain_session: + public session<plain_session>, + public std::enable_shared_from_this<plain_session>  { -    /* -        The certificate was generated from CMD.EXE on Windows 10 using: - -        winpty openssl dhparam -out dh.pem 2048 -        winpty openssl req -newkey rsa:4096 -sha256 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "//C=DE\ST=BY\L=Munich\O=Reichwein\CN=reichwein.it" -    */ - -    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_options( -        boost::asio::ssl::context::default_workarounds | -        boost::asio::ssl::context::no_sslv2 | -        boost::asio::ssl::context::single_dh_use); - -    std::string cert; -    if (cert_path.empty()) { -     // use dummy self signed certificate. Will be replaced by real -     // certificate if configured upon respective session -     cert = -      "-----BEGIN CERTIFICATE-----\n" -      "MIIDnTCCAoWgAwIBAgIULkYtO+2Ddeg+qLZ+aDQpmA5b4L0wDQYJKoZIhvcNAQEL\n" -      "BQAwXjELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0JhdmFyaWExDzANBgNVBAcMBk11\n" -      "bmljaDEVMBMGA1UECgwMUmVpY2h3ZWluIElUMRUwEwYDVQQDDAxyZWljaHdlaW4u\n" -      "aXQwHhcNMjAwNDA1MDgwNzIyWhcNNDcwODIyMDgwNzIyWjBeMQswCQYDVQQGEwJE\n" -      "RTEQMA4GA1UECAwHQmF2YXJpYTEPMA0GA1UEBwwGTXVuaWNoMRUwEwYDVQQKDAxS\n" -      "ZWljaHdlaW4gSVQxFTATBgNVBAMMDHJlaWNod2Vpbi5pdDCCASIwDQYJKoZIhvcN\n" -      "AQEBBQADggEPADCCAQoCggEBALJNb0WLbz+xP+YITMMk+eeK/SIOCRFs/9aZIAyK\n" -      "ParGauxa+8d25mlfJTAo6/G0h3sA240JHyNpOzVOogPU+v4dRWyGO0w5vHVD0caB\n" -      "rDb1eEfmLtqfKLLUL9iPDReUh6WAE7qoNDtfoT551uSMIae1cpPUduVTnSkEgw8k\n" -      "NjJSHYT800jSB2R+e7tJG3ErXDM63R3B8RbitZPoWACjpBxDT+Qrj0fBFS4AWw6b\n" -      "z09uitv0RrgI6CW7xRh3UAdRwEBGHiU6HTIthX6LNgez1UL0sfu1iZ22wNmYZP/S\n" -      "sL3b20WtSH9LN2PRJ4q3AGt6RMbmSGr65ljha9xkTFna0Y8CAwEAAaNTMFEwHQYD\n" -      "VR0OBBYEFKd5/MGFZUAUV502vJ/Kcswax8WVMB8GA1UdIwQYMBaAFKd5/MGFZUAU\n" -      "V502vJ/Kcswax8WVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\n" -      "AIBS4AfM7wiunQ2UZQQ5A0Un99+BLax9e+h11h/jGeJ+/9maY/E9MK6UG9LXoOv2\n" -      "z32Q7Ta2xKeRu6GC/qupwYJ0Xt3LENOfogsaNCAgxKlAN48LGlRyCTvzWsEMh28j\n" -      "RaelWonh2qQoiryKLVnRwrg8g1Bu4v+V437cIBmeZPxf0spEL9EVqlN+iS8plmel\n" -      "7/F4ULdybKGq39tgicuS7JhnY21ZzOFoq0bWnKBbAeTndmuROdb3pEppxW6pwu0q\n" -      "TFdMrSJE38kiQh2O9IchPQbTZ+Rdj0HE9NxStlrNr5bu6rjikRm50/G3JoXpzYdp\n" -      "AN4ZI2QZ6R6Y+TzDixKecNk=\n" -      "-----END CERTIFICATE-----\n" -      ; -    } else { -     cert = File::getFile(cert_path); -    } - -    ctx.use_certificate_chain( -        boost::asio::buffer(cert.data(), cert.size())); - -    std::string key; -    if (key_path == "") { -     // use dummy self signed key. Will be replaced by real -     // certificate if configured upon respective session -     key = -      "-----BEGIN PRIVATE KEY-----\n" -      "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyTW9Fi28/sT/m\n" -      "CEzDJPnniv0iDgkRbP/WmSAMij2qxmrsWvvHduZpXyUwKOvxtId7ANuNCR8jaTs1\n" -      "TqID1Pr+HUVshjtMObx1Q9HGgaw29XhH5i7anyiy1C/Yjw0XlIelgBO6qDQ7X6E+\n" -      "edbkjCGntXKT1HblU50pBIMPJDYyUh2E/NNI0gdkfnu7SRtxK1wzOt0dwfEW4rWT\n" -      "6FgAo6QcQ0/kK49HwRUuAFsOm89Pborb9Ea4COglu8UYd1AHUcBARh4lOh0yLYV+\n" -      "izYHs9VC9LH7tYmdtsDZmGT/0rC929tFrUh/Szdj0SeKtwBrekTG5khq+uZY4Wvc\n" -      "ZExZ2tGPAgMBAAECggEBAK9bJKIa3dCgPB257/TEOtsTgJyrfROcRYkCk9iBZOC9\n" -      "v46wdIrZTwY2wtY4iMPwLoY0c7ijTfJ/nfFxYjmujyK4Gvz+jvcKmWQizP8TrRFo\n" -      "HWFo6o+slFQ8BspO9itIspd7/OtIXgY+qNBO959Sig7sjsEA5eXoc9pRS6vqizq0\n" -      "j4G/UO5Amr/l3ciEJiqMJgZsIVLDKaGlqFTymydSqkB8UHQYWK1kunQxhK4Ldycu\n" -      "hTooQE7tXM0zvoFVV6v1fldV5OFsZk2kPMNtvMO6ZEpOM4rNMlg+vJy8kB1fb3Gs\n" -      "iFE/DCUpZsMSserQMU9/hfrYlndgsFD5Sr1EVGEebhECgYEA1gc9qx+ugdhYTY5j\n" -      "tJDXjOsnw8KY/l/1y+mQ8XNJ9MVdBGy1WB+uWB4teiyJchV49gn2XlKUK2rcCBvZ\n" -      "vC5CwPmFi2t70JezQgnXtDlbR0bARPlRd741i4rBpD7hEiZNCTOd2HFBpUg/CGWN\n" -      "E4n1ksazBm6jvv3Jo6WAa07Z390CgYEA1USrFqmc/fKGQpTCdH0qYZv3hQtrb1TQ\n" -      "9YnrbhtaC0haPpintZKjvhU3tCd1fPuIDXtMAgaaKSyoGiE2aMvLxt1/eV08BkMi\n" -      "kGIss9poYNi5+6ZD9QAHmHJhzZtVGj8U5L8379XmwxAByiBRVVE8CW1X/e6+iJpz\n" -      "+CLgN+zEVlsCgYEAsuOAdtxXJm4meERwL8b0cvNF3Eh1Sf/42MPTAwzCntSrh3w5\n" -      "InvwY/RtPHWnN/ScksEG7BWHhLafTCPDHJdp8hNcvIhNB68UBDln0loyYePP5pag\n" -      "sj4IUSbb7SUlR989elhrMTKQlM5K6QDAJrmjyVdM4S5urL9A3wgAyzAvyP0CgYAO\n" -      "paGuc8WxdzebWQYl4/bGL2UHgSpGwid7xZYiwMQlZDm2dNuHz+NpCaICwHcEN243\n" -      "ptEojnWGAGgnK0LGXcDIDqxTlICr2W6FRgjV7Vkf1aKoUtn1+KOM58YpzdJBdDWm\n" -      "JC/eS+2GVhIZZLDRUDv0VcsmSIBTd3AhiZumm588YwKBgBZfNqfmHAwIP2pM1wml\n" -      "Ck3vaLLvonghj3iQW9CFJ/SqLOnfT4KJkFObR6oGbxY0RtXsCrmSqidIKgDd0Kkq\n" -      "L6QbHp2j3+16GBdmLNUJlfjBTNPJp69IDKztjeCX7/8JZs79p/LAv+I9Sh4lVw4O\n" -      "IrDprlB0yzP5zigcsAZeViYJ\n" -      "-----END PRIVATE KEY-----\n" -      ; -    } else { -     key = File::getFile(key_path); -    } -    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())); -} + beast::tcp_stream stream_; -int ServerNameError(SSL *s, HTTPS::Server::ctx_type& ctx_map) -{ - std::shared_ptr<ssl::context> ctx{ctx_map.at("")}; - SSL_set_SSL_CTX(s, ctx->native_handle()); - return SSL_CLIENT_HELLO_SUCCESS; // OK for now -} +public: + explicit plain_session( +     boost::asio::io_context& ioc, +     tcp::socket&& socket, +     Server& server): +  session(ioc, server), +  stream_(std::move(socket)) + { + } +  + void on_run() + { +  // We need to be executing within a strand to perform async operations +  // on the I/O objects in this session. Skip ssl handshake for plain http. +  net::dispatch(stream_.get_executor(), +		beast::bind_front_handler( +		    &session::do_read, +		    shared_from_this())); + } -std::string unbracketed(const std::string& s) -{ - if (s.size() >= 2 && s.front() == '[' && s.back() == ']') { -  return s.substr(1, s.size() - 2); - } else { -  return s; + void + do_close() + { +  // Send a TCP shutdown +  beast::error_code ec; +  stream_.socket().shutdown(tcp::socket::shutdown_send, ec); +  // At this point the connection is closed gracefully   } -} -int servername_callback(SSL *s, int *al, void *arg) -{ - HTTPS::Server::ctx_type& ctx_map = *(HTTPS::Server::ctx_type*)arg; - - if (0) { // not really necessary -  int* numbers; -  size_t numbers_size; -  if (SSL_client_hello_get1_extensions_present(s, &numbers, &numbers_size) != 1) { -   std::cout << "Error on SSL_client_hello_get1_extensions_present" << std::endl; -   return ServerNameError(s, ctx_map); -  } -  bool server_name_available {false}; -  for (size_t i = 0; i < numbers_size; i++) -   if (numbers[i] == 0) -    server_name_available = true; + beast::tcp_stream& stream() + { +  return stream_; + } -  OPENSSL_free(numbers); +}; // class -  if (!server_name_available) { -   std::cout << "Error: No server_name available at SSL_client_hello_get1_extensions_present" << std::endl; -   return ServerNameError(s, ctx_map); -  } +class ssl_session: + public session<ssl_session>, + public std::enable_shared_from_this<ssl_session> +{ + beast::ssl_stream<beast::tcp_stream> stream_; +public: + explicit ssl_session( +     boost::asio::io_context& ioc, +     tcp::socket&& socket, +     ssl::context& ctx, +     Server& server): +  session(ioc, server), +  stream_(std::move(socket), ctx) + {   } +  + void on_run() + { +  // Set the timeout +  beast::get_lowest_layer(stream_).expires_after( +      std::chrono::seconds(30)); - const unsigned char* data; - size_t data_size; - // 0 is server_name - if (SSL_client_hello_get0_ext(s, 0, &data, &data_size) != 1) { -  std::cout << "Warning: Error on SSL_client_hello_get0_ext: servername not available. Using dummy ctx." << std::endl; -  return ServerNameError(s, ctx_map); +  // Perform the SSL handshake +  stream_.async_handshake( +      ssl::stream_base::server, +      beast::bind_front_handler( +	  &ssl_session::on_handshake, +	  shared_from_this()));   } - // SNI Server Name, See https://tools.ietf.org/html/rfc6066 (TODO: why are there 5 bytes header?) - std::string server_name {std::string((const char*)data, (size_t)data_size)}; - if (server_name.size() >= 5 && server_name[0] == '\0') -  server_name = server_name.substr(5); + void + on_handshake(beast::error_code ec) + { +  if (ec) +   return fail(ec, "https handshake"); - server_name = unbracketed(server_name); +  do_read(); + } + + void + do_close() + { +  // Set the timeout. +  beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - auto it {ctx_map.find(server_name)}; - std::shared_ptr<ssl::context> ctx{}; - if (it != ctx_map.end()) { -  ctx = it->second; - } else { -  std::cout << "Warning: server_name " << server_name << " (" << server_name.size() << ") not found in list of prepared contexts. Using dummy ctx." << std::endl; -  return ServerNameError(s, ctx_map); +  // Perform the SSL shutdown +  stream_.async_shutdown( +      beast::bind_front_handler( +          &ssl_session::on_shutdown, +          shared_from_this()));   } - SSL_set_SSL_CTX(s, ctx->native_handle()); -  - return SSL_CLIENT_HELLO_SUCCESS; -} + void + on_shutdown(beast::error_code ec) + { +  if (ec) +   return fail(ec, "https shutdown"); -} // anonymous namespace -//------------------------------------------------------------------------------ +  // At this point the connection is closed gracefully + } -namespace HTTPS { + beast::ssl_stream<beast::tcp_stream>& stream() + { +  return stream_; + } -Server::Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics) - : ::Server(config, ioc, socket, plugins, statistics) -{ - load_certificates(); // load initially -} +}; // class -Server::~Server() +// Accepts incoming connections and launches the sessions +template<class Derived>  +class listener  { -} +private: + Derived& derived() + { +  return static_cast<Derived&>(*this); + } +     +protected: + net::io_context& ioc_; + tcp::acceptor acceptor_; + ::Server& server_; + +public: + explicit listener( +        net::io_context& ioc, +        tcp::endpoint endpoint, +        Server& server): +        ioc_(ioc), +        acceptor_(ioc), +        server_(server) + { +  beast::error_code ec; + +  // Open the acceptor +  acceptor_.open(endpoint.protocol(), ec); +  if (ec) +  { +   fail(ec, "http listener open"); +   return; +  } -void Server::load_certificates() +  // Allow address reuse +  acceptor_.set_option(net::socket_base::reuse_address(true), ec); +  if (ec) +  { +   fail(ec, "http listener set_option"); +   return; +  } + +  // Bind to the server address +  acceptor_.bind(endpoint, ec); +  if (ec) +  { +   fail(ec, "http listener bind"); +   return; +  } + +  // Start listening for connections +  acceptor_.listen(net::socket_base::max_listen_connections, ec); +  if (ec) +  { +   fail(ec, "http listener listen"); +   return; +  } + } + + // Start accepting incoming connections + void + run() + { +  do_accept(); + } + +protected: + void + do_accept() + { +  // The new connection gets its own strand +  acceptor_.async_accept( +      net::make_strand(ioc_), +      beast::bind_front_handler( +          &Derived::on_accept, +          derived().shared_from_this())); + } +}; // class + +class plain_listener: + public listener<plain_listener>, + public std::enable_shared_from_this<plain_listener>  { - // initial dummy, before we can add specific ctx w/ certificate - std::shared_ptr<ssl::context> ctx_dummy{std::make_shared<ssl::context>(tls_method)}; - load_server_certificate(*ctx_dummy, "", ""); - SSL_CTX_set_client_hello_cb(ctx_dummy->native_handle(), servername_callback, &m_ctx); - m_ctx.emplace("", ctx_dummy); - - // import the real certificates - for (const auto& serve_site: m_socket.serve_sites) { -  for (const auto& site: m_config.Sites()) { -   if (site.first == serve_site) { -    std::shared_ptr<ssl::context> ctx {std::make_shared<ssl::context>(tls_method)}; - -    std::cout << "Creating SSL context/cert for site " << serve_site << " on port " << m_socket.port << std::endl; - -    load_server_certificate(*ctx, site.second.cert_path, site.second.key_path); -    SSL_CTX_set_client_hello_cb(ctx->native_handle(), servername_callback, &m_ctx); - -    for (const auto& host: site.second.hosts) { -     std::cout << "  Adding Host " << host << std::endl; -     m_ctx.emplace(unbracketed(host), ctx); -    } -   } +public: + explicit plain_listener( +     net::io_context& ioc, +     tcp::endpoint endpoint, +     Server& server): +     listener(ioc, endpoint, server) + { + } + + void + on_accept(beast::error_code ec, tcp::socket socket) + { +  if (ec) { +   fail(ec, "plain listener accept"); +  } else { +   // Create the session and run it +   std::make_shared<plain_session>( +       ioc_, +       std::move(socket), +       server_)->run();    } + +  // Accept another connection +  do_accept();   } -} +}; // class -int Server::start() +class ssl_listener: + public listener<ssl_listener>, + public std::enable_shared_from_this<ssl_listener>  { - auto const address = net::ip::make_address(m_socket.address); - auto const port = static_cast<unsigned short>(std::atoi(m_socket.port.data())); + ssl::context& ctx_; + +public: + explicit ssl_listener( +     net::io_context& ioc, +     ssl::context& ctx, +     tcp::endpoint endpoint, +     Server& server): +     listener(ioc, endpoint, server), +     ctx_(ctx) + { + } - // Create and launch a listening port - std::make_shared<listener>( -     m_ioc, -     *m_ctx[""], -     tcp::endpoint{address, port}, -     *this)->run(); + void + on_accept(beast::error_code ec, tcp::socket socket) + { +  if (ec) { +   fail(ec, "ssl listener accept"); +  } else { +   // Create the session and run it +   std::make_shared<ssl_session>( +       ioc_, +       std::move(socket), +       ctx_, +       server_)->run(); +  } + +  // Accept another connection +  do_accept(); + } +}; // class - return EXIT_SUCCESS; +} // namespace + +void make_listener(net::io_context& ioc, net::ip::address address, unsigned short port, Server& server) +{ + std::make_shared<plain_listener>( +  ioc, +  tcp::endpoint{address, port}, +  server)->run();  } -} // namespace HTTPS +void make_listener(net::io_context& ioc, ssl::context& ctx, net::ip::address address, unsigned short port, Server& server) +{ + std::make_shared<ssl_listener>( +  ioc, +  ctx, +  tcp::endpoint{address, port}, +  server)->run(); +} @@ -15,27 +15,8 @@  #include "config.h"  #include "server.h" -namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp> +// plain / http +void make_listener(boost::asio::io_context& ioc, boost::asio::ip::address address, unsigned short port, Server& server); -namespace HTTPS { - -static const ssl::context_base::method tls_method {ssl::context::tlsv13}; - -class Server: public ::Server -{ -public: - typedef std::unordered_map<std::string, std::shared_ptr<ssl::context>> ctx_type; - -private: - ctx_type m_ctx; - -public: - Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics); - virtual ~Server(); - - void load_certificates(); - - int start() override; -}; - -} +// ssl / https +void make_listener(boost::asio::io_context& ioc, boost::asio::ssl::context& ctx, boost::asio::ip::address address, unsigned short port, Server& server); @@ -17,7 +17,7 @@ namespace response {  response_type generate_response(request_type& req, Server& server); -// Get host:port e.g. reichwein.it:6543 +// Get host:port/path e.g. reichwein.it:6543/path1  std::string get_websocket_address(request_type& req, Server& server);  } // namespace @@ -1,5 +1,6 @@  #include <boost/beast/version.hpp> +#include <boost/asio/ip/tcp.hpp>  #include <boost/beast/core.hpp>  #include <boost/beast/http.hpp>  #include <boost/beast/version.hpp> @@ -10,11 +11,14 @@  #include <boost/config.hpp>  #include <exception> +#include <filesystem>  #include <functional>  #include <iostream>  #include <thread>  #include <vector> +#include <libreichwein/file.h> +  #include "server.h"  #include "http.h" @@ -27,11 +31,23 @@ namespace http = beast::http;           // from <boost/beast/http.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> +namespace fs = std::filesystem; +using namespace Reichwein;  const std::string Server::VersionString{ "Reichwein.IT Webserver "s + std::string{VERSION} };  namespace {   const int32_t stats_timer_seconds { 24 * 60 * 60 }; // save stats once a day + + std::string unbracketed(const std::string& s) + { +  if (s.size() >= 2 && s.front() == '[' && s.back() == ']') { +   return s.substr(1, s.size() - 2); +  } else { +   return s; +  } + } +  } // anonymous namespace  Server::Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics) @@ -47,6 +63,298 @@ Server::~Server()  {  } +Config& Server::GetConfig() +{ + return m_config; +} + +const Socket& Server::GetSocket() +{ + return m_socket; +} + +plugin_type Server::GetPlugin(const std::string& name) +{ + try { +  return m_plugins.at(name); + } catch (const std::out_of_range& ex) { +  std::cout << "Out of range at Server::GetPlugin(): " << name << std::endl; +  std::rethrow_exception(std::current_exception()); + } catch (...) { +  std::cout << "Unknown exception at Server::GetPlugin(): " << name << std::endl; +  std::rethrow_exception(std::current_exception()); + } +} + +Statistics& Server::GetStatistics() +{ + return m_statistics; +} + +namespace HTTP { + +class Server: public ::Server +{ +public: + Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics) +  : ::Server(config, ioc, socket, plugins, statistics) + { + } + + ~Server() override + { + } + + int start() override + { +  auto const address = net::ip::make_address(m_socket.address); +  auto const port = static_cast<unsigned short>(std::atoi(m_socket.port.data())); + +  // Create and launch a listening port +  make_listener(m_ioc, address, port, *this); + +  return EXIT_SUCCESS; + } +}; + +} // namespace HTTP + +namespace { + +/*  Load a signed certificate into the ssl context, and configure +    the context for use with a server. +*/ +void load_server_certificate(boost::asio::ssl::context& ctx, const fs::path& cert_path, const fs::path& key_path) +{ + /* +     The certificate was generated from CMD.EXE on Windows 10 using: + +     winpty openssl dhparam -out dh.pem 2048 +     winpty openssl req -newkey rsa:4096 -sha256 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "//C=DE\ST=BY\L=Munich\O=Reichwein\CN=reichwein.it" + */ + + 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_options( +     boost::asio::ssl::context::default_workarounds | +     boost::asio::ssl::context::no_sslv2 | +     boost::asio::ssl::context::single_dh_use); + + std::string cert; + if (cert_path.empty()) { +  // use dummy self signed certificate. Will be replaced by real +  // certificate if configured upon respective session +  cert = +   "-----BEGIN CERTIFICATE-----\n" +   "MIIDnTCCAoWgAwIBAgIULkYtO+2Ddeg+qLZ+aDQpmA5b4L0wDQYJKoZIhvcNAQEL\n" +   "BQAwXjELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0JhdmFyaWExDzANBgNVBAcMBk11\n" +   "bmljaDEVMBMGA1UECgwMUmVpY2h3ZWluIElUMRUwEwYDVQQDDAxyZWljaHdlaW4u\n" +   "aXQwHhcNMjAwNDA1MDgwNzIyWhcNNDcwODIyMDgwNzIyWjBeMQswCQYDVQQGEwJE\n" +   "RTEQMA4GA1UECAwHQmF2YXJpYTEPMA0GA1UEBwwGTXVuaWNoMRUwEwYDVQQKDAxS\n" +   "ZWljaHdlaW4gSVQxFTATBgNVBAMMDHJlaWNod2Vpbi5pdDCCASIwDQYJKoZIhvcN\n" +   "AQEBBQADggEPADCCAQoCggEBALJNb0WLbz+xP+YITMMk+eeK/SIOCRFs/9aZIAyK\n" +   "ParGauxa+8d25mlfJTAo6/G0h3sA240JHyNpOzVOogPU+v4dRWyGO0w5vHVD0caB\n" +   "rDb1eEfmLtqfKLLUL9iPDReUh6WAE7qoNDtfoT551uSMIae1cpPUduVTnSkEgw8k\n" +   "NjJSHYT800jSB2R+e7tJG3ErXDM63R3B8RbitZPoWACjpBxDT+Qrj0fBFS4AWw6b\n" +   "z09uitv0RrgI6CW7xRh3UAdRwEBGHiU6HTIthX6LNgez1UL0sfu1iZ22wNmYZP/S\n" +   "sL3b20WtSH9LN2PRJ4q3AGt6RMbmSGr65ljha9xkTFna0Y8CAwEAAaNTMFEwHQYD\n" +   "VR0OBBYEFKd5/MGFZUAUV502vJ/Kcswax8WVMB8GA1UdIwQYMBaAFKd5/MGFZUAU\n" +   "V502vJ/Kcswax8WVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\n" +   "AIBS4AfM7wiunQ2UZQQ5A0Un99+BLax9e+h11h/jGeJ+/9maY/E9MK6UG9LXoOv2\n" +   "z32Q7Ta2xKeRu6GC/qupwYJ0Xt3LENOfogsaNCAgxKlAN48LGlRyCTvzWsEMh28j\n" +   "RaelWonh2qQoiryKLVnRwrg8g1Bu4v+V437cIBmeZPxf0spEL9EVqlN+iS8plmel\n" +   "7/F4ULdybKGq39tgicuS7JhnY21ZzOFoq0bWnKBbAeTndmuROdb3pEppxW6pwu0q\n" +   "TFdMrSJE38kiQh2O9IchPQbTZ+Rdj0HE9NxStlrNr5bu6rjikRm50/G3JoXpzYdp\n" +   "AN4ZI2QZ6R6Y+TzDixKecNk=\n" +   "-----END CERTIFICATE-----\n" +   ; + } else { +  cert = File::getFile(cert_path); + } + + ctx.use_certificate_chain( +     boost::asio::buffer(cert.data(), cert.size())); + + std::string key; + if (key_path == "") { +  // use dummy self signed key. Will be replaced by real +  // certificate if configured upon respective session +  key = +   "-----BEGIN PRIVATE KEY-----\n" +   "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyTW9Fi28/sT/m\n" +   "CEzDJPnniv0iDgkRbP/WmSAMij2qxmrsWvvHduZpXyUwKOvxtId7ANuNCR8jaTs1\n" +   "TqID1Pr+HUVshjtMObx1Q9HGgaw29XhH5i7anyiy1C/Yjw0XlIelgBO6qDQ7X6E+\n" +   "edbkjCGntXKT1HblU50pBIMPJDYyUh2E/NNI0gdkfnu7SRtxK1wzOt0dwfEW4rWT\n" +   "6FgAo6QcQ0/kK49HwRUuAFsOm89Pborb9Ea4COglu8UYd1AHUcBARh4lOh0yLYV+\n" +   "izYHs9VC9LH7tYmdtsDZmGT/0rC929tFrUh/Szdj0SeKtwBrekTG5khq+uZY4Wvc\n" +   "ZExZ2tGPAgMBAAECggEBAK9bJKIa3dCgPB257/TEOtsTgJyrfROcRYkCk9iBZOC9\n" +   "v46wdIrZTwY2wtY4iMPwLoY0c7ijTfJ/nfFxYjmujyK4Gvz+jvcKmWQizP8TrRFo\n" +   "HWFo6o+slFQ8BspO9itIspd7/OtIXgY+qNBO959Sig7sjsEA5eXoc9pRS6vqizq0\n" +   "j4G/UO5Amr/l3ciEJiqMJgZsIVLDKaGlqFTymydSqkB8UHQYWK1kunQxhK4Ldycu\n" +   "hTooQE7tXM0zvoFVV6v1fldV5OFsZk2kPMNtvMO6ZEpOM4rNMlg+vJy8kB1fb3Gs\n" +   "iFE/DCUpZsMSserQMU9/hfrYlndgsFD5Sr1EVGEebhECgYEA1gc9qx+ugdhYTY5j\n" +   "tJDXjOsnw8KY/l/1y+mQ8XNJ9MVdBGy1WB+uWB4teiyJchV49gn2XlKUK2rcCBvZ\n" +   "vC5CwPmFi2t70JezQgnXtDlbR0bARPlRd741i4rBpD7hEiZNCTOd2HFBpUg/CGWN\n" +   "E4n1ksazBm6jvv3Jo6WAa07Z390CgYEA1USrFqmc/fKGQpTCdH0qYZv3hQtrb1TQ\n" +   "9YnrbhtaC0haPpintZKjvhU3tCd1fPuIDXtMAgaaKSyoGiE2aMvLxt1/eV08BkMi\n" +   "kGIss9poYNi5+6ZD9QAHmHJhzZtVGj8U5L8379XmwxAByiBRVVE8CW1X/e6+iJpz\n" +   "+CLgN+zEVlsCgYEAsuOAdtxXJm4meERwL8b0cvNF3Eh1Sf/42MPTAwzCntSrh3w5\n" +   "InvwY/RtPHWnN/ScksEG7BWHhLafTCPDHJdp8hNcvIhNB68UBDln0loyYePP5pag\n" +   "sj4IUSbb7SUlR989elhrMTKQlM5K6QDAJrmjyVdM4S5urL9A3wgAyzAvyP0CgYAO\n" +   "paGuc8WxdzebWQYl4/bGL2UHgSpGwid7xZYiwMQlZDm2dNuHz+NpCaICwHcEN243\n" +   "ptEojnWGAGgnK0LGXcDIDqxTlICr2W6FRgjV7Vkf1aKoUtn1+KOM58YpzdJBdDWm\n" +   "JC/eS+2GVhIZZLDRUDv0VcsmSIBTd3AhiZumm588YwKBgBZfNqfmHAwIP2pM1wml\n" +   "Ck3vaLLvonghj3iQW9CFJ/SqLOnfT4KJkFObR6oGbxY0RtXsCrmSqidIKgDd0Kkq\n" +   "L6QbHp2j3+16GBdmLNUJlfjBTNPJp69IDKztjeCX7/8JZs79p/LAv+I9Sh4lVw4O\n" +   "IrDprlB0yzP5zigcsAZeViYJ\n" +   "-----END PRIVATE KEY-----\n" +   ; + } else { +  key = File::getFile(key_path); + } + 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())); +} + +} // namespace + +namespace HTTPS { + +typedef std::unordered_map<std::string, std::shared_ptr<ssl::context>> ctx_type; + +static const ssl::context_base::method tls_method {ssl::context::tlsv13}; + +int ServerNameError(SSL *s, ctx_type& ctx_map) +{ + std::shared_ptr<ssl::context> ctx{ctx_map.at("")}; + SSL_set_SSL_CTX(s, ctx->native_handle()); + return SSL_CLIENT_HELLO_SUCCESS; // OK for now +} + +int servername_callback(SSL *s, int *al, void *arg) +{ + ctx_type& ctx_map = *(ctx_type*)arg; + + if (0) { // not really necessary +  int* numbers; +  size_t numbers_size; +  if (SSL_client_hello_get1_extensions_present(s, &numbers, &numbers_size) != 1) { +   std::cout << "Error on SSL_client_hello_get1_extensions_present" << std::endl; +   return ServerNameError(s, ctx_map); +  } +  bool server_name_available {false}; +  for (size_t i = 0; i < numbers_size; i++) +   if (numbers[i] == 0) +    server_name_available = true; + +  OPENSSL_free(numbers); + +  if (!server_name_available) { +   std::cout << "Error: No server_name available at SSL_client_hello_get1_extensions_present" << std::endl; +   return ServerNameError(s, ctx_map); +  } + } + + const unsigned char* data; + size_t data_size; + // 0 is server_name + if (SSL_client_hello_get0_ext(s, 0, &data, &data_size) != 1) { +  std::cout << "Warning: Error on SSL_client_hello_get0_ext: servername not available. Using dummy ctx." << std::endl; +  return ServerNameError(s, ctx_map); + } + + // SNI Server Name, See https://tools.ietf.org/html/rfc6066 (TODO: why are there 5 bytes header?) + std::string server_name {std::string((const char*)data, (size_t)data_size)}; + if (server_name.size() >= 5 && server_name[0] == '\0') +  server_name = server_name.substr(5); + + server_name = unbracketed(server_name); + + auto it {ctx_map.find(server_name)}; + std::shared_ptr<ssl::context> ctx{}; + if (it != ctx_map.end()) { +  ctx = it->second; + } else { +  std::cout << "Warning: server_name " << server_name << " (" << server_name.size() << ") not found in list of prepared contexts. Using dummy ctx." << std::endl; +  return ServerNameError(s, ctx_map); + } + + SSL_set_SSL_CTX(s, ctx->native_handle()); +  + return SSL_CLIENT_HELLO_SUCCESS; +} + +class Server: public ::Server +{ +private: + ctx_type m_ctx; + +public: + Server(Config& config, boost::asio::io_context& ioc, const Socket& socket, plugins_container_type& plugins, Statistics& statistics): +  ::Server(config, ioc, socket, plugins, statistics) + { +  load_certificates(); // load initially + } + + ~Server() override + { + } + + void load_certificates() + { +  // initial dummy, before we can add specific ctx w/ certificate +  std::shared_ptr<ssl::context> ctx_dummy{std::make_shared<ssl::context>(tls_method)}; +  load_server_certificate(*ctx_dummy, "", ""); +  SSL_CTX_set_client_hello_cb(ctx_dummy->native_handle(), servername_callback, &m_ctx); +  m_ctx.emplace("", ctx_dummy); + +  // import the real certificates +  for (const auto& serve_site: m_socket.serve_sites) { +   for (const auto& site: m_config.Sites()) { +    if (site.first == serve_site) { +     std::shared_ptr<ssl::context> ctx {std::make_shared<ssl::context>(tls_method)}; + +     std::cout << "Creating SSL context/cert for site " << serve_site << " on port " << m_socket.port << std::endl; + +     load_server_certificate(*ctx, site.second.cert_path, site.second.key_path); +     SSL_CTX_set_client_hello_cb(ctx->native_handle(), servername_callback, &m_ctx); + +     for (const auto& host: site.second.hosts) { +      std::cout << "  Adding Host " << host << std::endl; +      m_ctx.emplace(unbracketed(host), ctx); +     } +    } +   } +  } + } + + int start() override + { +  auto const address = net::ip::make_address(m_socket.address); +  auto const port = static_cast<unsigned short>(std::atoi(m_socket.port.data())); + +  // Create and launch a listening port +  make_listener(m_ioc, *m_ctx[""], address, port, *this); + +  return EXIT_SUCCESS; + } + +}; + +} // namespace HTTPS +  int run_server(Config& config, plugins_container_type& plugins)  {   Statistics stats(config.statistics_path()); @@ -106,30 +414,3 @@ int run_server(Config& config, plugins_container_type& plugins)   return EXIT_SUCCESS;   } -Config& Server::GetConfig() -{ - return m_config; -} - -const Socket& Server::GetSocket() -{ - return m_socket; -} - -plugin_type Server::GetPlugin(const std::string& name) -{ - try { -  return m_plugins.at(name); - } catch (const std::out_of_range& ex) { -  std::cout << "Out of range at Server::GetPlugin(): " << name << std::endl; -  std::rethrow_exception(std::current_exception()); - } catch (...) { -  std::cout << "Unknown exception at Server::GetPlugin(): " << name << std::endl; -  std::rethrow_exception(std::current_exception()); - } -} - -Statistics& Server::GetStatistics() -{ - return m_statistics; -} @@ -1,11 +1,14 @@  #pragma once  #include <boost/asio/io_context.hpp> +#include <boost/asio/ssl.hpp>  #include "config.h"  #include "plugin.h"  #include "statistics.h" +namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp> +  using namespace std::string_literals;  // Base class for HTTP and HTTPS classes diff --git a/websocket.cpp b/websocket.cpp index 37d4b59..68ff194 100644 --- a/websocket.cpp +++ b/websocket.cpp @@ -1,5 +1,307 @@ +// +// Websocket, implemented via CRTP for both plain and ssl websockets +//  #include "websocket.h" +namespace beast = boost::beast;         // from <boost/beast.hpp> +namespace http = beast::http;           // from <boost/beast/http.hpp> +namespace net = boost::asio;            // from <boost/asio.hpp> +namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp> +namespace websocket = beast::websocket; +using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp> +using namespace std::placeholders; + +// Server session, asynchronous, proxying, implemented w/ CRTP for plain+ssl variants +template<class Derived> +class websocket_session +{ +private: + Derived& derived() + { +  return static_cast<Derived&>(*this); + } + + boost::asio::io_context& ioc_; + boost::asio::ip::tcp::resolver resolver_; + boost::beast::flat_buffer buffer_in_; + boost::beast::websocket::stream<beast::tcp_stream> ws_app_; + boost::beast::flat_buffer buffer_out_; + std::string host_; + std::string port_; + std::string subprotocol_; + std::string relative_target_; + +public:  + explicit websocket_session(boost::asio::io_context& ioc, std::string&& websocket_address): +  ioc_(ioc), +  resolver_(boost::asio::make_strand(ioc_)), +  ws_app_(boost::asio::make_strand(ioc_)), +  host_{}, +  port_{}, +  subprotocol_{}, +  relative_target_{} + { +  // Parse websocket address host:port : + +  auto colon_pos{websocket_address.find_last_of(':')}; + +  if (colon_pos == std::string::npos) { +   std::cerr << "Warning: Bad websocket address (colon missing): " << websocket_address << std::endl; +   return; +  } +   +  auto slash_pos{websocket_address.find('/')}; +  if (slash_pos == std::string::npos) { +   std::cerr << "Warning: Bad websocket address (slash missing): " << websocket_address << std::endl; +   return; +  } +  if (slash_pos <= colon_pos) { +   std::cerr << "Warning: Bad websocket address: " << websocket_address << std::endl; +   return; +  } + +  host_ = websocket_address.substr(0, colon_pos); +  port_ = websocket_address.substr(colon_pos + 1, slash_pos - (colon_pos + 1)); +  relative_target_ = websocket_address.substr(slash_pos); + } + + // + // The initial setup path + // +  + // Start the asynchronous accept operation + void do_accept_in(request_type req) + { +  // Set suggested timeout settings for the websocket +  derived().ws_in().set_option( +      websocket::stream_base::timeout::suggested( +          beast::role_type::server)); + +  // Set a decorator to change the Server of the handshake +  derived().ws_in().set_option(websocket::stream_base::decorator( +      [](websocket::response_type& res) +      { +          res.set(http::field::server, +              std::string{"Reichwein.IT Webserver"}); +      })); + +  // Forward subprotocol from request to target websocket +  subprotocol_ = std::string{req[http::field::sec_websocket_protocol]}; + +  // Accept the websocket handshake +  derived().ws_in().async_accept( +      req, +      beast::bind_front_handler( +          &websocket_session::on_accept_in, +          derived().shared_from_this())); + } + +private: + void on_accept_in(beast::error_code ec) + { +  if (ec) +   return fail(ec, "accept in"); + +  resolver_.async_resolve(host_, port_, +                          beast::bind_front_handler(&websocket_session::on_resolve_app, derived().shared_from_this())); + } +  + void on_resolve_app(beast::error_code ec, tcp::resolver::results_type results) + { +  if (ec) +   return fail(ec, "resolve app"); + +  beast::get_lowest_layer(ws_app_).async_connect(results, +                          beast::bind_front_handler(&websocket_session::on_connect_app, derived().shared_from_this())); + } + + void on_connect_app(beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) + { +  if (ec) +   return fail(ec, "connect app"); + +  beast::get_lowest_layer(ws_app_).expires_never(); + +  host_ += ':' + std::to_string(endpoint.port()); + +  // Set suggested timeout settings for the websocket +  ws_app_.set_option( +      websocket::stream_base::timeout::suggested( +	  beast::role_type::client)); + +  ws_app_.set_option(boost::beast::websocket::stream_base::decorator( +   [](boost::beast::websocket::request_type& req) +   { +    req.set(boost::beast::http::field::user_agent, "Reichwein.IT Webserver Websocket client"); +   })); +  +  ws_app_.set_option(boost::beast::websocket::stream_base::decorator( +   [this](boost::beast::websocket::request_type& req) +   { +    req.set(boost::beast::http::field::sec_websocket_protocol, subprotocol_); +   })); + +  ws_app_.async_handshake(host_, relative_target_, +                          beast::bind_front_handler(&websocket_session::on_handshake_app, derived().shared_from_this())); + } + + void on_handshake_app(beast::error_code ec) + { +  if (ec) +   return fail(ec, "handshake app"); + +  // Start reading messages from both sides, asynchronously +  do_read_in(); +  do_read_app(); + } + + // + // The input path (client,ws_in_ -> app,ws_app_) via + // +  + void + do_read_in() + { +  // Read a message into our buffer +  derived().ws_in().async_read( +      buffer_in_, +      beast::bind_front_handler( +          &websocket_session::on_read_in, +          derived().shared_from_this())); + } + + void + on_read_in( +     beast::error_code ec, +     std::size_t bytes_transferred) + { +  boost::ignore_unused(bytes_transferred); + +  // This indicates that the websocket_session was closed +  if (ec == websocket::error::closed) +   return; + +  if (ec) +   fail(ec, "read in"); + +  ws_app_.text(derived().ws_in().got_text()); +   +  do_write_app(); + } +  + void do_write_app() + { +  ws_app_.async_write(buffer_in_.data(), +      beast::bind_front_handler( +          &websocket_session::on_write_app, +          derived().shared_from_this())); + } + + void on_write_app(beast::error_code ec, std::size_t bytes_transferred) + { +  boost::ignore_unused(bytes_transferred); + +  if (ec) +   fail(ec, "write app"); + +  buffer_in_.consume(buffer_in_.size()); + +  // Do another read +  do_read_in(); + } + + // + // The output path (app,ws_app_ -> client,ws_in_) + // +  + void do_read_app() + { +  // Read a message into our buffer +  ws_app_.async_read( +      buffer_out_, +      beast::bind_front_handler( +          &websocket_session::on_read_app, +          derived().shared_from_this())); + } + + void on_read_app(beast::error_code ec, std::size_t bytes_transferred) + { +  boost::ignore_unused(bytes_transferred); + +  if (ec == websocket::error::closed) +   return; + +  if (ec) +   fail(ec, "read app"); + +  do_write_out(); + } + + void do_write_out() + { +  derived().ws_in().async_write(buffer_out_.data(), +      beast::bind_front_handler( +          &websocket_session::on_write_out, +          derived().shared_from_this())); + } + + void on_write_out( +     beast::error_code ec, +     std::size_t bytes_transferred) + { +  boost::ignore_unused(bytes_transferred); + +  if(ec) +   return fail(ec, "write out"); + +  // Clear the buffer +  buffer_out_.consume(buffer_out_.size()); + +  // Do another read +  do_read_app(); + } + +}; // class + +class plain_websocket_session: + public websocket_session<plain_websocket_session>, + public std::enable_shared_from_this<plain_websocket_session> +{ + boost::beast::websocket::stream<beast::tcp_stream> ws_in_; + +public: + + explicit plain_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string&& websocket_address): +  websocket_session(ioc, std::move(websocket_address)), +  ws_in_(std::move(stream)) + { + } + + boost::beast::websocket::stream<beast::tcp_stream>& ws_in() + { +  return ws_in_; + } +}; // class + +class ssl_websocket_session: + public websocket_session<ssl_websocket_session>, + public std::enable_shared_from_this<ssl_websocket_session> +{ + boost::beast::websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws_in_; + +public: + + explicit ssl_websocket_session(boost::asio::io_context& ioc, beast::ssl_stream<beast::tcp_stream>&& stream, std::string&& websocket_address): +  websocket_session(ioc, std::move(websocket_address)), +  ws_in_(std::move(stream)) + { + } + + boost::beast::websocket::stream<beast::ssl_stream<beast::tcp_stream>>& ws_in() + { +  return ws_in_; + } +}; // class  void make_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string websocket_address, request_type&& req)  { diff --git a/websocket.h b/websocket.h index e3c2b30..87b5a04 100644 --- a/websocket.h +++ b/websocket.h @@ -20,318 +20,8 @@  #include <boost/asio/strand.hpp>  #include <boost/config.hpp> -#include <algorithm> -#include <cstddef> -#include <cstdlib> -#include <filesystem> -#include <functional> -#include <iostream> -#include <memory> -#include <optional>  #include <string> -#include <thread> -#include <vector> -namespace beast = boost::beast;         // from <boost/beast.hpp> -namespace http = beast::http;           // from <boost/beast/http.hpp> -namespace net = boost::asio;            // from <boost/asio.hpp> -namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp> -namespace websocket = beast::websocket; -using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp> -using namespace std::placeholders; - -// Server session, asynchronous, proxying, implemented w/ CRTP for plain+ssl variants -template<class Derived> -class websocket_session -{ -private: - Derived& derived() - { -  return static_cast<Derived&>(*this); - } - - boost::asio::io_context& ioc_; - boost::asio::ip::tcp::resolver resolver_; - boost::beast::flat_buffer buffer_in_; - boost::beast::websocket::stream<beast::tcp_stream> ws_app_; - boost::beast::flat_buffer buffer_out_; - std::string host_; - std::string port_; - std::string subprotocol_; - std::string relative_target_; - -public:  - explicit websocket_session(boost::asio::io_context& ioc, std::string&& websocket_address): -  ioc_(ioc), -  resolver_(boost::asio::make_strand(ioc_)), -  ws_app_(boost::asio::make_strand(ioc_)), -  host_{}, -  port_{}, -  subprotocol_{}, -  relative_target_{} - { -  // Parse websocket address host:port : - -  auto colon_pos{websocket_address.find_last_of(':')}; - -  if (colon_pos == std::string::npos) { -   std::cerr << "Warning: Bad websocket address (colon missing): " << websocket_address << std::endl; -   return; -  } -   -  auto slash_pos{websocket_address.find('/')}; -  if (slash_pos == std::string::npos) { -   std::cerr << "Warning: Bad websocket address (slash missing): " << websocket_address << std::endl; -   return; -  } -  if (slash_pos <= colon_pos) { -   std::cerr << "Warning: Bad websocket address: " << websocket_address << std::endl; -   return; -  } - -  host_ = websocket_address.substr(0, colon_pos); -  port_ = websocket_address.substr(colon_pos + 1, slash_pos - (colon_pos + 1)); -  relative_target_ = websocket_address.substr(slash_pos); - } - - // - // The initial setup path - // -  - // Start the asynchronous accept operation - void do_accept_in(request_type req) - { -  // Set suggested timeout settings for the websocket -  derived().ws_in().set_option( -      websocket::stream_base::timeout::suggested( -          beast::role_type::server)); - -  // Set a decorator to change the Server of the handshake -  derived().ws_in().set_option(websocket::stream_base::decorator( -      [](websocket::response_type& res) -      { -          res.set(http::field::server, -              std::string{"Reichwein.IT Webserver"}); -      })); - -  // Forward subprotocol from request to target websocket -  subprotocol_ = std::string{req[http::field::sec_websocket_protocol]}; - -  // Accept the websocket handshake -  derived().ws_in().async_accept( -      req, -      beast::bind_front_handler( -          &websocket_session::on_accept_in, -          derived().shared_from_this())); - } - -private: - void on_accept_in(beast::error_code ec) - { -  if (ec) -   return fail(ec, "accept in"); - -  resolver_.async_resolve(host_, port_, -                          beast::bind_front_handler(&websocket_session::on_resolve_app, derived().shared_from_this())); - } -  - void on_resolve_app(beast::error_code ec, tcp::resolver::results_type results) - { -  if (ec) -   return fail(ec, "resolve app"); - -  beast::get_lowest_layer(ws_app_).async_connect(results, -                          beast::bind_front_handler(&websocket_session::on_connect_app, derived().shared_from_this())); - } - - void on_connect_app(beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) - { -  if (ec) -   return fail(ec, "connect app"); - -  beast::get_lowest_layer(ws_app_).expires_never(); - -  host_ += ':' + std::to_string(endpoint.port()); - -  // Set suggested timeout settings for the websocket -  ws_app_.set_option( -      websocket::stream_base::timeout::suggested( -	  beast::role_type::client)); - -  ws_app_.set_option(boost::beast::websocket::stream_base::decorator( -   [](boost::beast::websocket::request_type& req) -   { -    req.set(boost::beast::http::field::user_agent, "Reichwein.IT Webserver Websocket client"); -   })); -  -  ws_app_.set_option(boost::beast::websocket::stream_base::decorator( -   [this](boost::beast::websocket::request_type& req) -   { -    req.set(boost::beast::http::field::sec_websocket_protocol, subprotocol_); -   })); - -  ws_app_.async_handshake(host_, relative_target_, -                          beast::bind_front_handler(&websocket_session::on_handshake_app, derived().shared_from_this())); - } - - void on_handshake_app(beast::error_code ec) - { -  if (ec) -   return fail(ec, "handshake app"); - -  // Start reading messages from both sides, asynchronously -  do_read_in(); -  do_read_app(); - } - - // - // The input path (client,ws_in_ -> app,ws_app_) via - // -  - void - do_read_in() - { -  // Read a message into our buffer -  derived().ws_in().async_read( -      buffer_in_, -      beast::bind_front_handler( -          &websocket_session::on_read_in, -          derived().shared_from_this())); - } - - void - on_read_in( -     beast::error_code ec, -     std::size_t bytes_transferred) - { -  boost::ignore_unused(bytes_transferred); - -  // This indicates that the websocket_session was closed -  if (ec == websocket::error::closed) -   return; - -  if (ec) -   fail(ec, "read in"); - -  ws_app_.text(derived().ws_in().got_text()); -   -  do_write_app(); - } -  - void do_write_app() - { -  ws_app_.async_write(buffer_in_.data(), -      beast::bind_front_handler( -          &websocket_session::on_write_app, -          derived().shared_from_this())); - } - - void on_write_app(beast::error_code ec, std::size_t bytes_transferred) - { -  boost::ignore_unused(bytes_transferred); - -  if (ec) -   fail(ec, "write app"); - -  buffer_in_.consume(buffer_in_.size()); - -  // Do another read -  do_read_in(); - } - - // - // The output path (app,ws_app_ -> client,ws_in_) - // -  - void do_read_app() - { -  // Read a message into our buffer -  ws_app_.async_read( -      buffer_out_, -      beast::bind_front_handler( -          &websocket_session::on_read_app, -          derived().shared_from_this())); - } - - void on_read_app(beast::error_code ec, std::size_t bytes_transferred) - { -  boost::ignore_unused(bytes_transferred); - -  if (ec == websocket::error::closed) -   return; - -  if (ec) -   fail(ec, "read app"); - -  do_write_out(); - } - - void do_write_out() - { -  derived().ws_in().async_write(buffer_out_.data(), -      beast::bind_front_handler( -          &websocket_session::on_write_out, -          derived().shared_from_this())); - } - - void on_write_out( -     beast::error_code ec, -     std::size_t bytes_transferred) - { -  boost::ignore_unused(bytes_transferred); - -  if(ec) -   return fail(ec, "write out"); - -  // Clear the buffer -  buffer_out_.consume(buffer_out_.size()); - -  // Do another read -  do_read_app(); - } - -}; // class - -class plain_websocket_session: - public websocket_session<plain_websocket_session>, - public std::enable_shared_from_this<plain_websocket_session> -{ - boost::beast::websocket::stream<beast::tcp_stream> ws_in_; - -public: - - explicit plain_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string&& websocket_address): -  websocket_session(ioc, std::move(websocket_address)), -  ws_in_(std::move(stream)) - { - } - - boost::beast::websocket::stream<beast::tcp_stream>& ws_in() - { -  return ws_in_; - } -}; // class - -class ssl_websocket_session: - public websocket_session<ssl_websocket_session>, - public std::enable_shared_from_this<ssl_websocket_session> -{ - boost::beast::websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws_in_; - -public: - - explicit ssl_websocket_session(boost::asio::io_context& ioc, beast::ssl_stream<beast::tcp_stream>&& stream, std::string&& websocket_address): -  websocket_session(ioc, std::move(websocket_address)), -  ws_in_(std::move(stream)) - { - } - - boost::beast::websocket::stream<beast::ssl_stream<beast::tcp_stream>>& ws_in() - { -  return ws_in_; - } -}; // class - -void make_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string websocket_address, request_type&& req); -void make_websocket_session(boost::asio::io_context& ioc, beast::ssl_stream<beast::tcp_stream>&& stream, std::string websocket_address, request_type&& req); +void make_websocket_session(boost::asio::io_context& ioc, boost::beast::tcp_stream&& stream, std::string websocket_address, request_type&& req); +void make_websocket_session(boost::asio::io_context& ioc, boost::beast::ssl_stream<beast::tcp_stream>&& stream, std::string websocket_address, request_type&& req);  | 
