#include "https.h" #include "config.h" #include "file.h" #include "server.h" #include #if BOOST_VERSION == 107100 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from namespace { // Return a reasonable mime type based on the extension of a file. beast::string_view mime_type(beast::string_view path) { using beast::iequals; auto const ext = [&path] { auto const pos = path.rfind("."); if(pos == beast::string_view::npos) return beast::string_view{}; return path.substr(pos); }(); if(iequals(ext, ".htm")) return "text/html"; if(iequals(ext, ".html")) return "text/html"; if(iequals(ext, ".php")) return "text/html"; if(iequals(ext, ".css")) return "text/css"; if(iequals(ext, ".txt")) return "text/plain"; if(iequals(ext, ".js")) return "application/javascript"; if(iequals(ext, ".json")) return "application/json"; if(iequals(ext, ".xml")) return "application/xml"; if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; if(iequals(ext, ".flv")) return "video/x-flv"; if(iequals(ext, ".png")) return "image/png"; if(iequals(ext, ".jpe")) return "image/jpeg"; if(iequals(ext, ".jpeg")) return "image/jpeg"; if(iequals(ext, ".jpg")) return "image/jpeg"; if(iequals(ext, ".gif")) return "image/gif"; if(iequals(ext, ".bmp")) return "image/bmp"; if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; if(iequals(ext, ".tiff")) return "image/tiff"; if(iequals(ext, ".tif")) return "image/tiff"; if(iequals(ext, ".svg")) return "image/svg+xml"; if(iequals(ext, ".svgz")) return "image/svg+xml"; return "application/text"; } // Append an HTTP rel-path to a local filesystem path. // The returned path is normalized for the platform. std::string path_cat( beast::string_view base, beast::string_view path) { if(base.empty()) return std::string(path); std::string result(base); #ifdef BOOST_MSVC char constexpr path_separator = '\\'; if(result.back() == path_separator) result.resize(result.size() - 1); result.append(path.data(), path.size()); for(auto& c : result) if(c == '/') c = path_separator; #else char constexpr path_separator = '/'; if(result.back() == path_separator) result.resize(result.size() - 1); result.append(path.data(), path.size()); #endif return result; } // This function produces an HTTP response for the given // request. The type of the response object depends on the // contents of the request, so the interface requires the // caller to pass a generic lambda for receiving the response. template< class Body, class Allocator, class Send> void handle_request( const Config& config, const Socket& socket, http::request>&& req, Send&& send) { // Returns a bad request response auto const bad_request = [&req](beast::string_view why) { http::response res{http::status::bad_request, req.version()}; res.set(http::field::server, VersionString); res.set(http::field::content_type, "text/html"); res.keep_alive(req.keep_alive()); res.body() = std::string(why); res.prepare_payload(); return res; }; // Returns a not found response auto const not_found = [&req](beast::string_view target) { http::response res{http::status::not_found, req.version()}; res.set(http::field::server, VersionString); res.set(http::field::content_type, "text/html"); res.keep_alive(req.keep_alive()); res.body() = "The resource '" + std::string(target) + "' was not found."; res.prepare_payload(); return res; }; // Returns a server error response auto const server_error = [&req](beast::string_view what) { http::response res{http::status::internal_server_error, req.version()}; res.set(http::field::server, VersionString); res.set(http::field::content_type, "text/html"); res.keep_alive(req.keep_alive()); res.body() = "An error occurred: '" + std::string(what) + "'"; res.prepare_payload(); return res; }; // Make sure we can handle the method if( req.method() != http::verb::get && req.method() != http::verb::head) return send(bad_request("Unknown HTTP-method")); // Request path must be absolute and not contain "..". if( req.target().empty() || req.target()[0] != '/' || req.target().find("..") != beast::string_view::npos) return send(bad_request("Illegal request-target")); // Build the path to the requested file std::string host{req["host"]}; // TODO: just use string_view std::string target{req.target()}; std::string path = path_cat(config.DocRoot(socket, host, target), req.target()); if(req.target().back() == '/') path.append("index.html"); // Attempt to open the file beast::error_code ec; http::file_body::value_type body; body.open(path.c_str(), beast::file_mode::scan, ec); // Handle the case where the file doesn't exist if(ec == beast::errc::no_such_file_or_directory) return send(not_found(req.target())); // Handle an unknown error if(ec) return send(server_error(ec.message())); // Cache the size since we need it after the move auto const size = body.size(); // Respond to HEAD request if(req.method() == http::verb::head) { http::response res{http::status::ok, req.version()}; res.set(http::field::server, VersionString); res.set(http::field::content_type, mime_type(path)); res.content_length(size); res.keep_alive(req.keep_alive()); return send(std::move(res)); } // Respond to GET request http::response res{ std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(http::status::ok, req.version())}; res.set(http::field::server, VersionString); res.set(http::field::content_type, mime_type(path)); res.content_length(size); res.keep_alive(req.keep_alive()); return send(std::move(res)); } //------------------------------------------------------------------------------ // Report a failure void fail(beast::error_code ec, char const* what) { // ssl::error::stream_truncated, also known as an SSL "short read", // indicates the peer closed the connection without performing the // required closing handshake (for example, Google does this to // improve performance). Generally this can be a security issue, // but if your communication protocol is self-terminated (as // it is with both HTTP and WebSocket) then you may simply // ignore the lack of close_notify. // // https://github.com/boostorg/beast/issues/38 // // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown // // When a short read would cut off the end of an HTTP message, // Beast returns the error beast::http::error::partial_message. // Therefore, if we see a short read here, it has occurred // after the message has been completed, so it is safe to ignore it. if(ec == net::ssl::error::stream_truncated) return; std::cerr << what << ": " << ec.message() << "\n"; } // Handles an HTTP server connection class session : public std::enable_shared_from_this { // This is the C++11 equivalent of a generic lambda. // The function object is used to send an HTTP message. struct send_lambda { session& self_; explicit send_lambda(session& self) : self_(self) { } template void operator()(http::message&& msg) const { // The lifetime of the message has to extend // for the duration of the async operation so // we use a shared_ptr to manage it. auto sp = std::make_shared< http::message>(std::move(msg)); // Store a type-erased version of the shared // pointer in the class to keep it alive. self_.res_ = sp; // Write the response http::async_write( self_.stream_, *sp, beast::bind_front_handler( &session::on_write, self_.shared_from_this(), sp->need_eof())); } }; beast::ssl_stream stream_; beast::flat_buffer buffer_; const Config& m_config; const Socket& m_socket; http::request req_; std::shared_ptr res_; send_lambda lambda_; public: // Take ownership of the socket explicit session( tcp::socket&& socket, ssl::context& ctx, const Config& config, const Socket& config_socket) : stream_(std::move(socket), ctx) , m_config(config) , m_socket(config_socket) , lambda_(*this) { } // 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, "handshake"); do_read(); } void do_read() { // Make the request empty before reading, // otherwise the operation behavior is undefined. req_ = {}; // Set the timeout. beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); // Read a request http::async_read(stream_, buffer_, req_, 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) return fail(ec, "read"); // Send the response handle_request(m_config, m_socket, std::move(req_), lambda_); } 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() { // 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, "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_; const Config& m_config; const Socket& m_socket; public: listener( net::io_context& ioc, ssl::context& ctx, tcp::endpoint endpoint, const Config& config, const Socket& socket) : ioc_(ioc) , ctx_(ctx) , acceptor_(ioc) , m_config(config) , m_socket(socket) { 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( std::move(socket), ctx_, m_config, m_socket)->run(); } // Accept another connection do_accept(); } }; /* Load a signed certificate into the ssl context, and configure the context for use with a server. For this to work with the browser or operating system, it is necessary to import the "Beast Test CA" certificate into the local certificate store, browser, or operating system depending on your environment Please see the documentation accompanying the Beast certificate for more details. */ void load_server_certificate(boost::asio::ssl::context& ctx, fs::path cert_path, 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:2048 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "//C=US\ST=CA\L=Los Angeles\O=Beast\CN=www.example.com" */ 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_password_callback( [](std::size_t, boost::asio::ssl::context_base::password_purpose) { return "test"; }); 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 == "") { 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 == "") { 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 servername_callback(SSL *s, int *al, void *arg) { int type {SSL_get_servername_type(s)}; std::string server_name {SSL_get_servername(s, type)}; HTTPS::Server::ctx_type* ctx_map = (HTTPS::Server::ctx_type*)arg; ssl::context& ctx = *(ctx_map->at(server_name)); SSL_set_SSL_CTX(s, ctx.native_handle()); return SSL_TLSEXT_ERR_OK; } } // anonymous namespace //------------------------------------------------------------------------------ namespace HTTPS { Server::Server(Config& config, boost::asio::io_context& ioc, const Socket& socket): ::Server(config, ioc), m_socket(socket) { for (const auto& serve_site: socket.serve_sites) { for (const auto& site: config.Sites()) { if (site.name == serve_site) { std::shared_ptr ctx {std::make_shared(tls_method)}; std::cout << "Creating SSL context/cert for site " << serve_site << std::endl; load_server_certificate(*ctx, site.cert_path, site.key_path); SSL_CTX_set_tlsext_servername_callback(ctx->native_handle(), servername_callback); SSL_CTX_set_tlsext_servername_arg(ctx->native_handle(), &m_ctx); for (const auto& host: site.hosts) { std::cout << " Adding Host " << host << std::endl; m_ctx.emplace(host, ctx); } } } } } Server::~Server() { } int Server::start() { auto const address = net::ip::make_address(m_socket.address); auto const port = static_cast(std::atoi(m_socket.port.data())); load_server_certificate(m_ctx_dummy, "", ""); // initial dummy, before we can add specific ctx w/ certificate SSL_CTX_set_tlsext_servername_callback(m_ctx_dummy.native_handle(), servername_callback); SSL_CTX_set_tlsext_servername_arg(m_ctx_dummy.native_handle(), &m_ctx); // Create and launch a listening port std::make_shared( m_ioc, m_ctx_dummy, tcp::endpoint{address, port}, m_config, m_socket)->run(); return EXIT_SUCCESS; } } // namespace HTTPS #endif