diff --git a/CMakeLists.txt b/CMakeLists.txt index dd82f76..9da1f06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,28 +1,25 @@ -cmake_minimum_required(VERSION 3.11) +cmake_minimum_required(VERSION 3.30.5) +project(conan2_template) -project(App CXX) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 17) -include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) -conan_basic_setup() +if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake-build-debug/conan_toolchain.cmake) +elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "Release") + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake-build-release/conan_toolchain.cmake) +elseif () + message(ERROR!!!) +endif () -find_package(Boost 1.84.0 REQUIRED) -if(Boost_FOUND) - include_directories(${Boost_INCLUDE_DIRS}) -endif() +find_package(Boost REQUIRED) -set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(Threads REQUIRED) +add_executable(App ./src/main.cpp + ./src/helpers/helpers.h + ./src/helpers/helpers.cpp + ./src/endpoints_handlers/HandleRequest.h) -add_executable(application src/main.cpp - src/helpers.h - src/helper.cpp - src/sdk.h - src/Session.h - src/Session.cpp - src/Listener.h - src/Listener.cpp - src/content_type.h - src/RequestHandlers/BasicRequestHandler.cpp - src/RequestHandlers/BasicRequestHandler.h) -target_link_libraries(application PRIVATE Threads::Threads) \ No newline at end of file +target_link_libraries(App PRIVATE Boost::boost) + +if (MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") +endif () \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2132949 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +conan install . -pr ./profiles/debug_profile --output-folder=cmake-build-debug --build=missing + +conan install . -pr ./profiles/release_profile --output-folder=cmake-build-release --build=missing diff --git a/conanfile.txt b/conanfile.txt index af38618..6a55361 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,5 +1,6 @@ [requires] -boost/1.84.0 +boost/1.78.0 [generators] -cmake +CMakeDeps +CMakeToolchain diff --git a/profiles/debug_profile b/profiles/debug_profile new file mode 100644 index 0000000..e0d4d57 --- /dev/null +++ b/profiles/debug_profile @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Debug +compiler=msvc +compiler.cppstd=17 +compiler.runtime=dynamic +compiler.version=193 +os=Windows diff --git a/profiles/release_profile b/profiles/release_profile new file mode 100644 index 0000000..fa6e904 --- /dev/null +++ b/profiles/release_profile @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=msvc +compiler.cppstd=17 +compiler.runtime=dynamic +compiler.version=193 +os=Windows diff --git a/src/endpoints_handlers/HandleRequest.h b/src/endpoints_handlers/HandleRequest.h new file mode 100644 index 0000000..8c5d3de --- /dev/null +++ b/src/endpoints_handlers/HandleRequest.h @@ -0,0 +1,82 @@ +#include + +namespace uad { +template +void HandleRequest(boost::beast::string_view doc_root, + boost::beast::http::request> &&req, + Send &&send) { + auto const bad_request = [&req](beast::string_view why) { + boost::beast::http::response res{boost::beast::http::status::bad_request, + req.version()}; + res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(boost::beast::http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = std::string(why); + res.prepare_payload(); + return res; + }; + + auto const not_found = [&req](beast::string_view target) { + boost::beast::http::response res{boost::beast::http::status::not_found, + req.version()}; + res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(boost::beast::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; + }; + + auto const server_error = [&req](beast::string_view what) { + boost::beast::http::response res{boost::beast::http::status::internal_server_error, + req.version()}; + res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(boost::beast::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; + }; + + if (req.method() != boost::beast::http::verb::get && req.method() != boost::beast::http::verb::head) + return send(bad_request("Unknown boost::beast::HTTP-method")); + + if (req.target().empty() || req.target()[0] != '/' || + req.target().find("..") != beast::string_view::npos) + return send(bad_request("Illegal request-target")); + + std::string path = PathCat(doc_root, req.target()); + if (req.target().back() == '/') + path.append("index.html"); + + beast::error_code ec; + boost::beast::http::file_body::value_type body; + body.open(path.c_str(), beast::file_mode::scan, ec); + + if (ec == beast::errc::no_such_file_or_directory) + return send(not_found(req.target())); + + if (ec) + return send(server_error(ec.message())); + + auto const size = body.size(); + + if (req.method() == boost::beast::http::verb::head) { + boost::beast::http::response res{boost::beast::http::status::ok, req.version()}; + res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(boost::beast::http::field::content_type, MimeType(path)); + res.content_length(size); + res.keep_alive(req.keep_alive()); + return send(std::move(res)); + } + + boost::beast::http::response res{ + std::piecewise_construct, std::make_tuple(std::move(body)), + std::make_tuple(boost::beast::http::status::ok, req.version())}; + res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(boost::beast::http::field::content_type, MimeType(path)); + res.content_length(size); + res.keep_alive(req.keep_alive()); + return send(std::move(res)); +} +} diff --git a/src/helpers/helpers.cpp b/src/helpers/helpers.cpp new file mode 100644 index 0000000..5e6730d --- /dev/null +++ b/src/helpers/helpers.cpp @@ -0,0 +1,77 @@ +#include "helpers.h" + +namespace uad { +boost::beast::string_view MimeType(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"; +} + +std::string PathCat(boost::beast::string_view base, boost::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; +} +} diff --git a/src/helpers/helpers.h b/src/helpers/helpers.h new file mode 100644 index 0000000..37cb506 --- /dev/null +++ b/src/helpers/helpers.h @@ -0,0 +1,7 @@ +#include + +namespace uad { +boost::beast::string_view MimeType(boost::beast::string_view path); + +std::string PathCat(boost::beast::string_view base, boost::beast::string_view path); +} diff --git a/src/main.cpp b/src/main.cpp index f9bf2e5..467ff18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,32 +1,369 @@ -#include "sdk.h" - -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include #include #include -#include "RequestHandlers/BasicRequestHandler.h" -#include "Listener.h" +#include "./helpers/helpers.h" +#include "./endpoints_handlers/HandleRequest.h" namespace beast = boost::beast; namespace http = beast::http; +namespace websocket = beast::websocket; namespace net = boost::asio; using tcp = boost::asio::ip::tcp; using namespace uad; -int main(int argc, char* argv[]) -{ - if (argc != 5) - { - std::cerr << - "Usage: http-server-async
\n" << - "Example:\n" << - " http-server-async 0.0.0.0 8080 . 1\n"; +//------------------------------------------------------------------------------ + +// Report a failure +void fail(beast::error_code ec, char const *what) { + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Echoes back all received WebSocket messages +class websocket_session + : public std::enable_shared_from_this { + websocket::stream ws_; + beast::flat_buffer buffer_; + +public: + // Take ownership of the socket + explicit websocket_session(tcp::socket &&socket) : ws_(std::move(socket)) {} + + // Start the asynchronous accept operation + template + void do_accept(http::request> req) { + // Set suggested timeout settings for the websocket + ws_.set_option( + websocket::stream_base::timeout::suggested(beast::role_type::server)); + + // Set a decorator to change the Server of the handshake + ws_.set_option( + websocket::stream_base::decorator([](websocket::response_type &res) { + res.set(http::field::server, + std::string(BOOST_BEAST_VERSION_STRING) + " advanced-server"); + })); + + // Accept the websocket handshake + ws_.async_accept(req, + beast::bind_front_handler(&websocket_session::on_accept, + shared_from_this())); + } + +private: + void on_accept(beast::error_code ec) { + if (ec) + return fail(ec, "accept"); + + // Read a message + do_read(); + } + + void do_read() { + // Read a message into our buffer + ws_.async_read(buffer_, + beast::bind_front_handler(&websocket_session::on_read, + shared_from_this())); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + // This indicates that the websocket_session was closed + if (ec == websocket::error::closed) + return; + + if (ec) + fail(ec, "read"); + + // Echo the message + ws_.text(ws_.got_text()); + ws_.async_write(buffer_.data(), + beast::bind_front_handler(&websocket_session::on_write, + shared_from_this())); + } + + void on_write(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) + return fail(ec, "write"); + + // Clear the buffer + buffer_.consume(buffer_.size()); + + // Do another read + do_read(); + } +}; + +//------------------------------------------------------------------------------ + +// Handles an HTTP server connection +class http_session : public std::enable_shared_from_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_.stream_, msg_, + beast::bind_front_handler(&http_session::on_write, + self_.shared_from_this(), + msg_.need_eof())); + } + }; + + // Allocate and store the work + items_.push_back(boost::make_unique(self_, std::move(msg))); + + // If there was no previous work, start this one + if (items_.size() == 1) + (*items_.front())(); + } + }; + + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + std::shared_ptr doc_root_; + queue queue_; + + // The parser is stored in an optional container so we can + // construct it from scratch it at the beginning of each new message. + boost::optional> parser_; + +public: + // Take ownership of the socket + http_session(tcp::socket &&socket, + std::shared_ptr const &doc_root) + : stream_(std::move(socket)), doc_root_(doc_root), queue_(*this) {} + + // Start the session + 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(&http_session::do_read, + this->shared_from_this())); + } + +private: + void do_read() { + // Construct a new parser for each message + parser_.emplace(); + + // Apply a reasonable limit to the allowed size + // of the body in bytes to prevent abuse. + parser_->body_limit(10000); + + // Set the timeout. + stream_.expires_after(std::chrono::seconds(30)); + + // Read a request using the parser-oriented interface + http::async_read( + stream_, buffer_, *parser_, + beast::bind_front_handler(&http_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"); + + // See if it is a WebSocket Upgrade + if (websocket::is_upgrade(parser_->get())) { + // Create a websocket session, transferring ownership + // of both the socket and the HTTP request. + std::make_shared(stream_.release_socket()) + ->do_accept(parser_->release()); + return; + } + + // Send the response + HandleRequest(*doc_root_, parser_->release(), queue_); + + // If we aren't at the queue limit, try to pipeline another request + if (!queue_.is_full()) + do_read(); + } + + 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(); + } + + // Inform the queue that a write completed + if (queue_.on_write()) { + // 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() { + // 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(acceptor_.get_executor(), + beast::bind_front_handler(&listener::do_accept, + this->shared_from_this())); + } + +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 http session and run it + std::make_shared(std::move(socket), 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
\n" + << "Example:\n" + << " advanced-server 0.0.0.0 8080 . 1\n"; return EXIT_FAILURE; } auto const address = net::ip::make_address(argv[1]); @@ -34,22 +371,34 @@ int main(int argc, char* argv[]) auto const doc_root = std::make_shared(argv[3]); auto const threads = std::max(1, std::atoi(argv[4])); - net::io_context ioc {threads}; + // The io_context is required for all I/O + net::io_context ioc{threads}; - std::make_shared( - ioc, - tcp::endpoint {address, port}, - doc_root)->Run(); + // Create and launch a listening port + std::make_shared(ioc, tcp::endpoint{address, port}, doc_root) + ->run(); + // Capture SIGINT and SIGTERM to perform a clean shutdown + net::signal_set signals(ioc, SIGINT, SIGTERM); + signals.async_wait([&](beast::error_code const &, int) { + // Stop the `io_context`. This will cause `run()` + // to return immediately, eventually destroying the + // `io_context` and all of the sockets in it. + ioc.stop(); + }); + + // 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(); - }); + v.emplace_back([&ioc] { ioc.run(); }); ioc.run(); + // (If we get here, it means we got a SIGINT or SIGTERM) + + // Block until all the threads exit + for (auto &t : v) + t.join(); + return EXIT_SUCCESS; -} \ No newline at end of file +}