diff options
author | Roland Reichwein <mail@reichwein.it> | 2023-01-12 15:30:07 +0100 |
---|---|---|
committer | Roland Reichwein <mail@reichwein.it> | 2023-01-12 15:30:07 +0100 |
commit | 00ed7df1a09cad8862f2c586347f4f55c99681e5 (patch) | |
tree | e24ef2699affc7630ea42e728e62df7c6686f714 | |
parent | 3cb78411178f8458f889975799060e0bb866d2cf (diff) |
Consolidate HTTP+HTTPS via CRTP
-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); |