// // Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // Official repository: https://github.com/boostorg/beast // //------------------------------------------------------------------------------ // // Example: Advanced server, flex (plain + SSL) // //------------------------------------------------------------------------------ #include "example/common/detect_ssl.hpp" #include "example/common/server_certificate.hpp" #include "example/common/ssl_stream.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using tcp = boost::asio::ip::tcp; // from namespace ssl = boost::asio::ssl; // from namespace http = boost::beast::http; // from namespace websocket = boost::beast::websocket; // from // Return a reasonable mime type based on the extension of a file. boost::beast::string_view mime_type(boost::beast::string_view path) { using boost::beast::iequals; auto const ext = [&path] { auto const pos = path.rfind("."); if(pos == boost::beast::string_view::npos) return boost::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( boost::beast::string_view base, boost::beast::string_view path) { if(base.empty()) return path.to_string(); std::string result = base.to_string(); #if 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( boost::beast::string_view doc_root, http::request>&& req, Send&& send) { // Returns a bad request response auto const bad_request = [&req](boost::beast::string_view why) { http::response res{http::status::bad_request, req.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, "text/html"); res.keep_alive(req.keep_alive()); res.body() = why.to_string(); res.prepare_payload(); return res; }; // Returns a not found response auto const not_found = [&req](boost::beast::string_view target) { http::response res{http::status::not_found, req.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, "text/html"); res.keep_alive(req.keep_alive()); res.body() = "The resource '" + target.to_string() + "' was not found."; res.prepare_payload(); return res; }; // Returns a server error response auto const server_error = [&req](boost::beast::string_view what) { http::response res{http::status::internal_server_error, req.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, "text/html"); res.keep_alive(req.keep_alive()); res.body() = "An error occurred: '" + what.to_string() + "'"; 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("..") != boost::beast::string_view::npos) return send(bad_request("Illegal request-target")); // Build the path to the requested file std::string path = path_cat(doc_root, req.target()); if(req.target().back() == '/') path.append("index.html"); // Attempt to open the file boost::beast::error_code ec; http::file_body::value_type body; body.open(path.c_str(), boost::beast::file_mode::scan, ec); // Handle the case where the file doesn't exist if(ec == boost::system::errc::no_such_file_or_directory) return send(not_found(req.target())); // Handle an unknown error if(ec) return send(server_error(ec.message())); // Respond to HEAD request if(req.method() == http::verb::head) { http::response res{http::status::ok, req.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, mime_type(path)); res.content_length(body.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, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, mime_type(path)); res.content_length(body.size()); res.keep_alive(req.keep_alive()); return send(std::move(res)); } //------------------------------------------------------------------------------ // Report a failure void fail(boost::system::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } //------------------------------------------------------------------------------ // Echoes back all received WebSocket messages. // This uses the Curiously Recurring Template Pattern so that // the same code works with both SSL streams and regular sockets. template class websocket_session { // Access the derived class, this is part of // the Curiously Recurring Template Pattern idiom. Derived& derived() { return static_cast(*this); } boost::beast::multi_buffer buffer_; protected: boost::asio::strand< boost::asio::io_context::executor_type> strand_; boost::asio::steady_timer timer_; public: // Construct the session explicit websocket_session(boost::asio::io_context& ioc) : strand_(ioc.get_executor()) , timer_(ioc, (std::chrono::steady_clock::time_point::max)()) { } // Start the asynchronous operation template void do_accept(http::request> req) { // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Accept the websocket handshake derived().ws().async_accept( req, boost::asio::bind_executor( strand_, std::bind( &websocket_session::on_accept, derived().shared_from_this(), std::placeholders::_1))); } // Called when the timer expires. void on_timer(boost::system::error_code ec) { if(ec && ec != boost::asio::error::operation_aborted) return fail(ec, "timer"); // Verify that the timer really expired since the deadline may have moved. if(timer_.expiry() <= std::chrono::steady_clock::now()) derived().do_timeout(); // Wait on the timer timer_.async_wait( boost::asio::bind_executor( strand_, std::bind( &websocket_session::on_timer, derived().shared_from_this(), std::placeholders::_1))); } void on_accept(boost::system::error_code ec) { // Happens when the timer closes the socket if(ec == boost::asio::error::operation_aborted) return; if(ec) return fail(ec, "accept"); // Read a message do_read(); } void do_read() { // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Read a message into our buffer derived().ws().async_read( buffer_, boost::asio::bind_executor( strand_, std::bind( &websocket_session::on_read, derived().shared_from_this(), std::placeholders::_1, std::placeholders::_2))); } void on_read( boost::system::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); // Happens when the timer closes the socket if(ec == boost::asio::error::operation_aborted) return; // This indicates that the websocket_session was closed if(ec == websocket::error::closed) return; if(ec) fail(ec, "read"); // Echo the message derived().ws().text(derived().ws().got_text()); derived().ws().async_write( buffer_.data(), boost::asio::bind_executor( strand_, std::bind( &websocket_session::on_write, derived().shared_from_this(), std::placeholders::_1, std::placeholders::_2))); } void on_write( boost::system::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); // Happens when the timer closes the socket if(ec == boost::asio::error::operation_aborted) return; if(ec) return fail(ec, "write"); // Clear the buffer buffer_.consume(buffer_.size()); // Do another read do_read(); } }; // Handles a plain WebSocket connection class plain_websocket_session : public websocket_session , public std::enable_shared_from_this { websocket::stream ws_; bool close_ = false; public: // Create the session explicit plain_websocket_session(tcp::socket socket) : websocket_session( socket.get_executor().context()) , ws_(std::move(socket)) { } // Called by the base class websocket::stream& ws() { return ws_; } // Start the asynchronous operation template void run(http::request> req) { // Run the timer. The timer is operated // continuously, this simplifies the code. on_timer({}); // Accept the WebSocket upgrade request do_accept(std::move(req)); } void do_timeout() { // This is so the close can have a timeout if(close_) return; close_ = true; // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Close the WebSocket Connection ws_.async_close( websocket::close_code::normal, boost::asio::bind_executor( strand_, std::bind( &plain_websocket_session::on_close, shared_from_this(), std::placeholders::_1))); } void on_close(boost::system::error_code ec) { // Happens when close times out if(ec == boost::asio::error::operation_aborted) return; if(ec) return fail(ec, "close"); // At this point the connection is gracefully closed } }; // Handles an SSL WebSocket connection class ssl_websocket_session : public websocket_session , public std::enable_shared_from_this { websocket::stream> ws_; boost::asio::strand< boost::asio::io_context::executor_type> strand_; bool eof_ = false; public: // Create the http_session explicit ssl_websocket_session(ssl_stream stream) : websocket_session( stream.get_executor().context()) , ws_(std::move(stream)) , strand_(ws_.get_executor()) { } // Called by the base class websocket::stream>& ws() { return ws_; } // Start the asynchronous operation template void run(http::request> req) { // Run the timer. The timer is operated // continuously, this simplifies the code. on_timer({}); // Accept the WebSocket upgrade request do_accept(std::move(req)); } void do_eof() { eof_ = true; // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Perform the SSL shutdown ws_.next_layer().async_shutdown( boost::asio::bind_executor( strand_, std::bind( &ssl_websocket_session::on_shutdown, shared_from_this(), std::placeholders::_1))); } void on_shutdown(boost::system::error_code ec) { // Happens when the shutdown times out if(ec == boost::asio::error::operation_aborted) return; if(ec) return fail(ec, "shutdown"); // At this point the connection is closed gracefully } void do_timeout() { // If this is true it means we timed out performing the shutdown if(eof_) return; // Start the timer again timer_.expires_at( (std::chrono::steady_clock::time_point::max)()); on_timer({}); do_eof(); } }; template void make_websocket_session( tcp::socket socket, http::request> req) { std::make_shared( std::move(socket))->run(std::move(req)); } template void make_websocket_session( ssl_stream stream, http::request> req) { std::make_shared( std::move(stream))->run(std::move(req)); } //------------------------------------------------------------------------------ // Handles an HTTP server connection. // This uses the Curiously Recurring Template Pattern so that // the same code works with both SSL streams and regular sockets. template class http_session { // Access the derived class, this is part of // the Curiously Recurring Template Pattern idiom. Derived& derived() { return static_cast(*this); } // This queue is used for HTTP pipelining. class queue { enum { // Maximum number of responses we will queue limit = 8 }; // The type-erased, saved work item struct work { virtual ~work() = default; virtual void operator()() = 0; }; http_session& self_; std::vector> items_; public: explicit queue(http_session& self) : self_(self) { static_assert(limit > 0, "queue limit must be positive"); items_.reserve(limit); } // Returns `true` if we have reached the queue limit bool is_full() const { return items_.size() >= limit; } // Called when a message finishes sending // Returns `true` if the caller should initiate a read bool on_write() { BOOST_ASSERT(! items_.empty()); auto const was_full = is_full(); items_.erase(items_.begin()); if(! items_.empty()) (*items_.front())(); return was_full; } // Called by the HTTP handler to send a response. template void operator()(http::message&& msg) { // This holds a work item struct work_impl : work { http_session& self_; http::message msg_; work_impl( http_session& self, http::message&& msg) : self_(self) , msg_(std::move(msg)) { } void operator()() { http::async_write( self_.derived().stream(), msg_, boost::asio::bind_executor( self_.strand_, std::bind( &http_session::on_write, self_.derived().shared_from_this(), std::placeholders::_1, msg_.need_eof()))); } }; // Allocate and store the work items_.emplace_back(new work_impl(self_, std::move(msg))); // If there was no previous work, start this one if(items_.size() == 1) (*items_.front())(); } }; std::string const& doc_root_; http::request req_; queue queue_; protected: boost::asio::steady_timer timer_; boost::asio::strand< boost::asio::io_context::executor_type> strand_; boost::beast::flat_buffer buffer_; public: // Construct the session http_session( boost::asio::io_context& ioc, boost::beast::flat_buffer buffer, std::string const& doc_root) : doc_root_(doc_root) , queue_(*this) , timer_(ioc, (std::chrono::steady_clock::time_point::max)()) , strand_(ioc.get_executor()) , buffer_(std::move(buffer)) { } void do_read() { // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Read a request http::async_read( derived().stream(), buffer_, req_, boost::asio::bind_executor( strand_, std::bind( &http_session::on_read, derived().shared_from_this(), std::placeholders::_1))); } // Called when the timer expires. void on_timer(boost::system::error_code ec) { if(ec && ec != boost::asio::error::operation_aborted) return fail(ec, "timer"); // Verify that the timer really expired since the deadline may have moved. if(timer_.expiry() <= std::chrono::steady_clock::now()) return derived().do_timeout(); // Wait on the timer timer_.async_wait( boost::asio::bind_executor( strand_, std::bind( &http_session::on_timer, derived().shared_from_this(), std::placeholders::_1))); } void on_read(boost::system::error_code ec) { // Happens when the timer closes the socket if(ec == boost::asio::error::operation_aborted) return; // This means they closed the connection if(ec == http::error::end_of_stream) return derived().do_eof(); if(ec) return fail(ec, "read"); // See if it is a WebSocket Upgrade if(websocket::is_upgrade(req_)) { // Transfer the stream to a new WebSocket session return make_websocket_session( derived().release_stream(), std::move(req_)); } // Send the response handle_request(doc_root_, std::move(req_), queue_); // If we aren't at the queue limit, try to pipeline another request if(! queue_.is_full()) do_read(); } void on_write(boost::system::error_code ec, bool close) { // Happens when the timer closes the socket if(ec == boost::asio::error::operation_aborted) return; 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 derived().do_eof(); } // Inform the queue that a write completed if(queue_.on_write()) { // Read another request do_read(); } } }; // Handles a plain HTTP connection class plain_http_session : public http_session , public std::enable_shared_from_this { tcp::socket socket_; boost::asio::strand< boost::asio::io_context::executor_type> strand_; public: // Create the http_session plain_http_session( tcp::socket socket, boost::beast::flat_buffer buffer, std::string const& doc_root) : http_session( socket.get_executor().context(), std::move(buffer), doc_root) , socket_(std::move(socket)) , strand_(socket_.get_executor()) { } // Called by the base class tcp::socket& stream() { return socket_; } // Called by the base class tcp::socket release_stream() { return std::move(socket_); } // Start the asynchronous operation void run() { // Run the timer. The timer is operated // continuously, this simplifies the code. on_timer({}); do_read(); } void do_eof() { // Send a TCP shutdown boost::system::error_code ec; socket_.shutdown(tcp::socket::shutdown_send, ec); // At this point the connection is closed gracefully } void do_timeout() { // Closing the socket cancels all outstanding operations. They // will complete with boost::asio::error::operation_aborted boost::system::error_code ec; socket_.shutdown(tcp::socket::shutdown_both, ec); socket_.close(ec); } }; // Handles an SSL HTTP connection class ssl_http_session : public http_session , public std::enable_shared_from_this { ssl_stream stream_; boost::asio::strand< boost::asio::io_context::executor_type> strand_; bool eof_ = false; public: // Create the http_session ssl_http_session( tcp::socket socket, ssl::context& ctx, boost::beast::flat_buffer buffer, std::string const& doc_root) : http_session( socket.get_executor().context(), std::move(buffer), doc_root) , stream_(std::move(socket), ctx) , strand_(stream_.get_executor()) { } // Called by the base class ssl_stream& stream() { return stream_; } // Called by the base class ssl_stream release_stream() { return std::move(stream_); } // Start the asynchronous operation void run() { // Run the timer. The timer is operated // continuously, this simplifies the code. on_timer({}); // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Perform the SSL handshake // Note, this is the buffered version of the handshake. stream_.async_handshake( ssl::stream_base::server, buffer_.data(), boost::asio::bind_executor( strand_, std::bind( &ssl_http_session::on_handshake, shared_from_this(), std::placeholders::_1, std::placeholders::_2))); } void on_handshake( boost::system::error_code ec, std::size_t bytes_used) { // Happens when the handshake times out if(ec == boost::asio::error::operation_aborted) return; if(ec) return fail(ec, "handshake"); // Consume the portion of the buffer used by the handshake buffer_.consume(bytes_used); do_read(); } void do_eof() { eof_ = true; // Set the timer timer_.expires_after(std::chrono::seconds(15)); // Perform the SSL shutdown stream_.async_shutdown( boost::asio::bind_executor( strand_, std::bind( &ssl_http_session::on_shutdown, shared_from_this(), std::placeholders::_1))); } void on_shutdown(boost::system::error_code ec) { // Happens when the shutdown times out if(ec == boost::asio::error::operation_aborted) return; if(ec) return fail(ec, "shutdown"); // At this point the connection is closed gracefully } void do_timeout() { // If this is true it means we timed out performing the shutdown if(eof_) return; // Start the timer again timer_.expires_at( (std::chrono::steady_clock::time_point::max)()); on_timer({}); do_eof(); } }; //------------------------------------------------------------------------------ // Detects SSL handshakes class detect_session : public std::enable_shared_from_this { tcp::socket socket_; ssl::context& ctx_; boost::asio::strand< boost::asio::io_context::executor_type> strand_; std::string const& doc_root_; boost::beast::flat_buffer buffer_; public: explicit detect_session( tcp::socket socket, ssl::context& ctx, std::string const& doc_root) : socket_(std::move(socket)) , ctx_(ctx) , strand_(socket_.get_executor()) , doc_root_(doc_root) { } // Launch the detector void run() { async_detect_ssl( socket_, buffer_, boost::asio::bind_executor( strand_, std::bind( &detect_session::on_detect, shared_from_this(), std::placeholders::_1, std::placeholders::_2))); } void on_detect(boost::system::error_code ec, boost::tribool result) { if(ec) return fail(ec, "detect"); if(result) { // Launch SSL session std::make_shared( std::move(socket_), ctx_, std::move(buffer_), doc_root_)->run(); return; } // Launch plain session std::make_shared( std::move(socket_), std::move(buffer_), doc_root_)->run(); } }; // Accepts incoming connections and launches the sessions class listener : public std::enable_shared_from_this { ssl::context& ctx_; tcp::acceptor acceptor_; tcp::socket socket_; std::string const& doc_root_; public: listener( boost::asio::io_context& ioc, ssl::context& ctx, tcp::endpoint endpoint, std::string const& doc_root) : ctx_(ctx) , acceptor_(ioc) , socket_(ioc) , doc_root_(doc_root) { boost::system::error_code ec; // Open the acceptor acceptor_.open(endpoint.protocol(), ec); if(ec) { fail(ec, "open"); return; } // Bind to the server address acceptor_.bind(endpoint, ec); if(ec) { fail(ec, "bind"); return; } // Start listening for connections acceptor_.listen( boost::asio::socket_base::max_listen_connections, ec); if(ec) { fail(ec, "listen"); return; } } // Start accepting incoming connections void run() { if(! acceptor_.is_open()) return; do_accept(); } void do_accept() { acceptor_.async_accept( socket_, std::bind( &listener::on_accept, shared_from_this(), std::placeholders::_1)); } void on_accept(boost::system::error_code ec) { if(ec) { fail(ec, "accept"); } else { // Create the detector http_session and run it std::make_shared( std::move(socket_), ctx_, doc_root_)->run(); } // Accept another connection do_accept(); } }; //------------------------------------------------------------------------------ int main(int argc, char* argv[]) { // Check command line arguments. if (argc != 5) { std::cerr << "Usage: advanced-server-flex
\n" << "Example:\n" << " advanced-server-flex 0.0.0.0 8080 . 1\n"; return EXIT_FAILURE; } auto const address = boost::asio::ip::make_address(argv[1]); auto const port = static_cast(std::atoi(argv[2])); std::string const doc_root = argv[3]; auto const threads = std::max(1, std::atoi(argv[4])); // The io_context is required for all I/O boost::asio::io_context ioc{threads}; // The SSL context is required, and holds certificates ssl::context ctx{ssl::context::sslv23}; // This holds the self-signed certificate used by the server load_server_certificate(ctx); // Create and launch a listening port std::make_shared( ioc, ctx, tcp::endpoint{address, port}, doc_root)->run(); // Run the I/O service on the requested number of threads std::vector v; v.reserve(threads - 1); for(auto i = threads - 1; i > 0; --i) v.emplace_back( [&ioc] { ioc.run(); }); ioc.run(); return EXIT_SUCCESS; }