From 00ed7df1a09cad8862f2c586347f4f55c99681e5 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Thu, 12 Jan 2023 15:30:07 +0100 Subject: Consolidate HTTP+HTTPS via CRTP --- server.cpp | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 308 insertions(+), 27 deletions(-) (limited to 'server.cpp') diff --git a/server.cpp b/server.cpp index d22d559..3f6183b 100644 --- a/server.cpp +++ b/server.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -10,11 +11,14 @@ #include #include +#include #include #include #include #include +#include + #include "server.h" #include "http.h" @@ -27,11 +31,23 @@ namespace http = beast::http; // from namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from +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(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> ctx_type; + +static const ssl::context_base::method tls_method {ssl::context::tlsv13}; + +int ServerNameError(SSL *s, ctx_type& ctx_map) +{ + std::shared_ptr 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 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 ctx_dummy{std::make_shared(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 ctx {std::make_shared(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(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; -} -- cgit v1.2.3