summaryrefslogtreecommitdiffhomepage
path: root/server.cpp
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2023-01-12 15:30:07 +0100
committerRoland Reichwein <mail@reichwein.it>2023-01-12 15:30:07 +0100
commit00ed7df1a09cad8862f2c586347f4f55c99681e5 (patch)
treee24ef2699affc7630ea42e728e62df7c6686f714 /server.cpp
parent3cb78411178f8458f889975799060e0bb866d2cf (diff)
Consolidate HTTP+HTTPS via CRTP
Diffstat (limited to 'server.cpp')
-rw-r--r--server.cpp335
1 files changed, 308 insertions, 27 deletions
diff --git a/server.cpp b/server.cpp
index d22d559..3f6183b 100644
--- a/server.cpp
+++ b/server.cpp
@@ -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;
-}