#include "https.h" #include "config.h" #include "error.h" #include "server.h" #include "response.h" #include "websocket.h" #include "libreichwein/file.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from namespace websocket = beast::websocket; using tcp = boost::asio::ip::tcp; // from using namespace Reichwein; namespace { //------------------------------------------------------------------------------ // Handles an HTTP server connection class session : public std::enable_shared_from_this { boost::asio::io_context& ioc_; beast::ssl_stream stream_; beast::flat_buffer buffer_; Server& m_server; std::optional> parser_; // need to reset parser every time, no other mechanism currently http::request req_; std::shared_ptr res_; // std::shared_ptr void handle_request(::Server& server, request_type&& req) { 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(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())); } 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_)) { beast::get_lowest_layer(stream_).expires_never(); std::make_shared(ioc_, std::move(stream_))->do_accept_in(parser_->release()); 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, "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 } }; //------------------------------------------------------------------------------ // Accepts incoming connections and launches the sessions class listener : public std::enable_shared_from_this { net::io_context& ioc_; ssl::context& ctx_; tcp::acceptor acceptor_; ::Server& m_server; 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(); } 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( 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) { /* 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())); } int ServerNameError(SSL *s, HTTPS::Server::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 } 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; } } 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; 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; } } // anonymous namespace //------------------------------------------------------------------------------ namespace HTTPS { 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 } Server::~Server() { } void Server::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 Server::start() { 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 std::make_shared( m_ioc, *m_ctx[""], tcp::endpoint{address, port}, *this)->run(); return EXIT_SUCCESS; } } // namespace HTTPS