#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 using tcp = boost::asio::ip::tcp; // from // 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; } // Return a response for the given request. // // The concrete type of the response message (which depends on the // request), is type-erased in message_generator. template http::message_generator handle_request(beast::string_view doc_root, http::request>&& req) { // 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, BOOST_BEAST_VERSION_STRING); 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, BOOST_BEAST_VERSION_STRING); 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, BOOST_BEAST_VERSION_STRING); 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 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 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 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 not_found(req.target()); // Handle an unknown error if (ec) return 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, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, mime_type(path)); res.content_length(size); res.keep_alive(req.keep_alive()); return 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(size); res.keep_alive(req.keep_alive()); return res; } //------------------------------------------------------------------------------ // Report a failure void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } // Handles an HTTP server connection class session : public std::enable_shared_from_this { beast::tcp_stream stream_; beast::flat_buffer buffer_; std::shared_ptr doc_root_; http::request req_; public: // Take ownership of the stream session(tcp::socket&& socket, std::shared_ptr const& doc_root) : stream_(std::move(socket)) , doc_root_(doc_root) {} // 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. Although not strictly necessary // for single-threaded contexts, this example code is written to be // thread-safe by default. net::dispatch(stream_.get_executor(), beast::bind_front_handler( &session::do_read, shared_from_this())); } void do_read() { // Make the request empty before reading, // otherwise the operation behavior is undefined. req_ = {}; // Set the timeout. 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 send_response(handle_request(*doc_root_, std::move(req_))); } void send_response(http::message_generator&& msg) { bool keep_alive = msg.keep_alive(); // Write the response beast::async_write(stream_, std::move(msg), beast::bind_front_handler( &session::on_write, shared_from_this(), keep_alive)); } void on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec) return fail(ec, "write"); if (!keep_alive) { // This means we should close the connection, usually because // the response indicated the "Connection: close" semantic. return do_close(); } // Read another request do_read(); } void do_close() { // Send a TCP shutdown beast::error_code ec; stream_.socket().shutdown(tcp::socket::shutdown_send, ec); // 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_; tcp::acceptor acceptor_; std::shared_ptr doc_root_; public: listener(net::io_context& ioc, tcp::endpoint endpoint, std::shared_ptr const& doc_root) : ioc_(ioc) , acceptor_(net::make_strand(ioc)) , doc_root_(doc_root) { 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"); return; // To avoid infinite loop } else { // Create the session and run it std::make_shared(std::move(socket), doc_root_)->run(); } // Accept another connection do_accept(); } }; //------------------------------------------------------------------------------