From 321116ac907983e432cd43290b4ab5a070bb14b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD?= Date: Fri, 6 Jun 2025 19:59:51 +0300 Subject: [PATCH] =?UTF-8?q?TASK00=20-=20=D0=90=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 53 ++++ CMakeLists.txt | 20 +- src/endpoints_handlers/HandleRequest.h | 58 ++-- src/helpers/helpers.cpp | 51 +++- src/helpers/helpers.h | 11 +- src/listener/Listener.cpp | 71 +++++ src/listener/Listener.h | 24 ++ src/main.cpp | 378 ++----------------------- src/session/HttpSession.cpp | 98 +++++++ src/session/HttpSession.h | 82 ++++++ src/session/WebsocketSession.cpp | 51 ++++ src/session/WebsocketSession.h | 41 +++ tests/helpers/helpers_TEST.cpp | 85 ++++++ 13 files changed, 627 insertions(+), 396 deletions(-) create mode 100644 .clang-format create mode 100644 src/listener/Listener.cpp create mode 100644 src/listener/Listener.h create mode 100644 src/session/HttpSession.cpp create mode 100644 src/session/HttpSession.h create mode 100644 src/session/WebsocketSession.cpp create mode 100644 src/session/WebsocketSession.h create mode 100644 tests/helpers/helpers_TEST.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8a2753a --- /dev/null +++ b/.clang-format @@ -0,0 +1,53 @@ +# Generated from CLion C/C++ Code Style settings +--- +Language: Cpp +BasedOnStyle: LLVM +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignOperands: false +AlignTrailingComments: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: false + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: true + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBraces: Custom +BreakConstructorInitializers: AfterColon +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ContinuationIndentWidth: 2 +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseBlocks: true +InsertNewlineAtEOF: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +PointerAlignment: Left +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +TabWidth: 2 +... diff --git a/CMakeLists.txt b/CMakeLists.txt index 9da1f06..e5130cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.30.5) -project(conan2_template) +project(UpAndDown) set(CMAKE_CXX_STANDARD 17) @@ -15,11 +15,23 @@ find_package(Boost REQUIRED) add_executable(App ./src/main.cpp ./src/helpers/helpers.h - ./src/helpers/helpers.cpp - ./src/endpoints_handlers/HandleRequest.h) + ./src/endpoints_handlers/HandleRequest.h + ./src/session/HttpSession.h + ./src/session/HttpSession.cpp + ./src/session/WebsocketSession.h + ./src/session/WebsocketSession.cpp + ./src/listener/Listener.h + ./src/listener/Listener.cpp +) target_link_libraries(App PRIVATE Boost::boost) if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") -endif () \ No newline at end of file +endif () + +add_executable(HelpersTests ./tests/helpers/helpers_TEST.cpp + ./src/helpers/helpers.h + ./src/helpers/helpers.cpp) +target_link_libraries(HelpersTests PRIVATE Boost::boost) +add_test(HelpersTests HelpersTests) diff --git a/src/endpoints_handlers/HandleRequest.h b/src/endpoints_handlers/HandleRequest.h index 8c5d3de..5ec37c4 100644 --- a/src/endpoints_handlers/HandleRequest.h +++ b/src/endpoints_handlers/HandleRequest.h @@ -1,13 +1,20 @@ +#pragma once + #include -namespace uad { +#include "./../helpers/helpers.h" + +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()}; +void HandleRequest( + boost::beast::string_view doc_root, + boost::beast::http::request>&& req, Send&& send) +{ + auto const bad_request = [&req](boost::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()); @@ -16,9 +23,10 @@ void HandleRequest(boost::beast::string_view doc_root, return res; }; - auto const not_found = [&req](beast::string_view target) { - boost::beast::http::response res{boost::beast::http::status::not_found, - req.version()}; + auto const not_found = [&req](boost::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()); @@ -27,9 +35,10 @@ void HandleRequest(boost::beast::string_view doc_root, 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()}; + auto const server_error = [&req](boost::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()); @@ -38,22 +47,23 @@ void HandleRequest(boost::beast::string_view doc_root, return res; }; - if (req.method() != boost::beast::http::verb::get && req.method() != boost::beast::http::verb::head) + 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) + req.target().find("..") != boost::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::error_code ec; boost::beast::http::file_body::value_type body; - body.open(path.c_str(), beast::file_mode::scan, ec); + body.open(path.c_str(), boost::beast::file_mode::scan, ec); - if (ec == beast::errc::no_such_file_or_directory) + if (ec == boost::beast::errc::no_such_file_or_directory) return send(not_found(req.target())); if (ec) @@ -61,8 +71,10 @@ void HandleRequest(boost::beast::string_view doc_root, 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()}; + 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); @@ -71,12 +83,12 @@ void HandleRequest(boost::beast::string_view doc_root, } boost::beast::http::response res{ - std::piecewise_construct, std::make_tuple(std::move(body)), - std::make_tuple(boost::beast::http::status::ok, req.version())}; + 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)); } -} +} // namespace uad diff --git a/src/helpers/helpers.cpp b/src/helpers/helpers.cpp index 5e6730d..71605c6 100644 --- a/src/helpers/helpers.cpp +++ b/src/helpers/helpers.cpp @@ -1,9 +1,21 @@ +#include +#include + +#include +#include + #include "helpers.h" -namespace uad { -boost::beast::string_view MimeType(boost::beast::string_view path) { +using namespace std; +using namespace boost; + +namespace uad +{ +boost::beast::string_view MimeType(boost::beast::string_view path) +{ using boost::beast::iequals; - auto const ext = [&path] { + auto const ext = [&path] + { auto const pos = path.rfind("."); if (pos == boost::beast::string_view::npos) return boost::beast::string_view{}; @@ -54,7 +66,8 @@ boost::beast::string_view MimeType(boost::beast::string_view path) { return "application/text"; } -std::string PathCat(boost::beast::string_view base, boost::beast::string_view path){ +std::string PathCat(boost::beast::string_view base, boost::beast::string_view path) +{ if (base.empty()) return std::string(path); std::string result(base); @@ -63,7 +76,7 @@ std::string PathCat(boost::beast::string_view base, boost::beast::string_view pa if (result.back() == path_separator) result.resize(result.size() - 1); result.append(path.data(), path.size()); - for (auto &c : result) + for (auto& c : result) if (c == '/') c = path_separator; #else @@ -74,4 +87,32 @@ std::string PathCat(boost::beast::string_view base, boost::beast::string_view pa #endif return result; } + +void Fail(boost::beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; } + + +std::string ToHex(std::byte* src, size_t len) +{ + if (!src || !len) + return ""; + + string ret; + ret.reserve(len * 2); + format formatter = format("%02X"); + + for (size_t i = 0; i < len; ++i) + { + byte target_byte = src[i]; + + formatter % static_cast(target_byte); + ret += formatter.str(); + + formatter.clear(); + } + + return ret; +} +} // namespace uad diff --git a/src/helpers/helpers.h b/src/helpers/helpers.h index 37cb506..d214a65 100644 --- a/src/helpers/helpers.h +++ b/src/helpers/helpers.h @@ -1,7 +1,14 @@ +#pragma once + #include -namespace uad { +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); -} + +void Fail(boost::beast::error_code ec, char const* what); + +std::string ToHex(std::byte* src, size_t len); +} // namespace uad diff --git a/src/listener/Listener.cpp b/src/listener/Listener.cpp new file mode 100644 index 0000000..e51ec53 --- /dev/null +++ b/src/listener/Listener.cpp @@ -0,0 +1,71 @@ +#include + +#include "Listener.h" + +#include "./../helpers/helpers.h" +#include "./../session/HttpSession.h" + +namespace uad +{ +Listener::Listener(boost::asio::io_context& ioc, boost::asio::ip::tcp::endpoint endpoint, + std::shared_ptr const& doc_root) : + ioc_(ioc), acceptor_(boost::asio::make_strand(ioc)), doc_root_(doc_root) +{ + boost::beast::error_code ec; + + acceptor_.open(endpoint.protocol(), ec); + if (ec) + { + Fail(ec, "open"); + return; + } + + acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec); + if (ec) + { + Fail(ec, "set_option"); + return; + } + + acceptor_.bind(endpoint, ec); + if (ec) + { + Fail(ec, "bind"); + return; + } + + acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec); + if (ec) + { + Fail(ec, "listen"); + return; + } +} + +void Listener::Run() +{ + boost::asio::dispatch( + acceptor_.get_executor(), + boost::beast::bind_front_handler(&Listener::DoAccept, this->shared_from_this())); +} + +void Listener::DoAccept() +{ + acceptor_.async_accept(boost::asio::make_strand(ioc_), + boost::beast::bind_front_handler(&Listener::OnAccept, shared_from_this())); +} + +void Listener::OnAccept(boost::beast::error_code ec, boost::asio::ip::tcp::socket socket) +{ + if (ec) + { + Fail(ec, "accept"); + } + else + { + std::make_shared(std::move(socket), doc_root_)->Run(); + } + + DoAccept(); +} +} // namespace uad diff --git a/src/listener/Listener.h b/src/listener/Listener.h new file mode 100644 index 0000000..5475e74 --- /dev/null +++ b/src/listener/Listener.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace uad +{ +class Listener : public std::enable_shared_from_this +{ + boost::asio::io_context& ioc_; + boost::asio::ip::tcp::acceptor acceptor_; + std::shared_ptr doc_root_; + +public: + Listener(boost::asio::io_context& ioc, boost::asio::ip::tcp::endpoint endpoint, + std::shared_ptr const& doc_root); + + void Run(); + +private: + void DoAccept(); + + void OnAccept(boost::beast::error_code ec, boost::asio::ip::tcp::socket socket); +}; +} diff --git a/src/main.cpp b/src/main.cpp index 467ff18..d0ae0ab 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,24 +1,21 @@ +#ifdef WIN32 +#include +#endif + #include -#include -#include #include -#include #include #include -#include #include -#include -#include #include -#include #include #include #include #include #include -#include "./helpers/helpers.h" -#include "./endpoints_handlers/HandleRequest.h" +#include "./session/WebsocketSession.h" +#include "./listener/Listener.h" namespace beast = boost::beast; namespace http = beast::http; @@ -27,343 +24,13 @@ namespace net = boost::asio; using tcp = boost::asio::ip::tcp; using namespace uad; -//------------------------------------------------------------------------------ - -// 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"; +int main(int argc, char* argv[]) +{ + 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]); @@ -371,33 +38,20 @@ 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])); - // The io_context is required for all I/O net::io_context ioc{threads}; - // Create and launch a listening port - std::make_shared(ioc, tcp::endpoint{address, port}, doc_root) - ->run(); + 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(); - }); + signals.async_wait([&](beast::error_code const&, int) { 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(); }); ioc.run(); - // (If we get here, it means we got a SIGINT or SIGTERM) - - // Block until all the threads exit - for (auto &t : v) + for (auto& t : v) t.join(); return EXIT_SUCCESS; diff --git a/src/session/HttpSession.cpp b/src/session/HttpSession.cpp new file mode 100644 index 0000000..c9e37e4 --- /dev/null +++ b/src/session/HttpSession.cpp @@ -0,0 +1,98 @@ +#include + +#include "HttpSession.h" +#include "WebsocketSession.h" +#include "./../endpoints_handlers/HandleRequest.h" + +namespace uad +{ +HttpSession::Queue::Queue(HttpSession& self) : self_(self) +{ + static_assert(limit > 0, "Queue limit must be positive"); + items_.reserve(limit); +} + +bool HttpSession::Queue::IsFull() const { return items_.size() >= limit; } + +bool HttpSession::Queue::OnWrite() +{ + BOOST_ASSERT(!items_.empty()); + auto const was_full = IsFull(); + items_.erase(items_.begin()); + if (!items_.empty()) + (*items_.front())(); + return was_full; +} + +HttpSession::HttpSession(boost::asio::ip::tcp::socket&& socket, + std::shared_ptr const& doc_root) : + stream_(std::move(socket)), doc_root_(doc_root), queue_(*this) +{ +} + +void HttpSession::Run() +{ + boost::asio::dispatch( + stream_.get_executor(), + boost::beast::bind_front_handler(&HttpSession::DoRead, this->shared_from_this())); +} + +void HttpSession::DoRead() +{ + parser_.emplace(); + + parser_->body_limit(10000); + + stream_.expires_after(std::chrono::seconds(30)); + + boost::beast::http::async_read( + stream_, buffer_, *parser_, + boost::beast::bind_front_handler(&HttpSession::OnRead, shared_from_this())); +} + +void HttpSession::OnRead(boost::beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if (ec == boost::beast::http::error::end_of_stream) + return DoClose(); + + if (ec) + return Fail(ec, "read"); + + if (boost::beast::websocket::is_upgrade(parser_->get())) + { + std::make_shared(stream_.release_socket())->DoAccept(parser_->release()); + return; + } + + HandleRequest(*doc_root_, parser_->release(), queue_); + + if (!queue_.IsFull()) + DoRead(); +} + +void HttpSession::OnWrite(bool close, boost::beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if (ec) + return Fail(ec, "write"); + + if (close) + { + return DoClose(); + } + + if (queue_.OnWrite()) + { + DoRead(); + } +} + +void HttpSession::DoClose() +{ + boost::beast::error_code ec; + stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec); +} +} // namespace uad diff --git a/src/session/HttpSession.h b/src/session/HttpSession.h new file mode 100644 index 0000000..1fbf127 --- /dev/null +++ b/src/session/HttpSession.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +namespace uad +{ +class HttpSession : public std::enable_shared_from_this +{ + class Queue + { + enum + { + limit = 8 + }; + + struct work + { + virtual void operator()() = 0; + virtual ~work() = default; + }; + + HttpSession& self_; + std::vector> items_; + + public: + explicit Queue(HttpSession& self); + + bool IsFull() const; + + bool OnWrite(); + + template + void operator()(boost::beast::http::message&& msg) + { + struct work_impl : work + { + HttpSession& self_; + boost::beast::http::message msg_; + + work_impl(HttpSession& self, boost::beast::http::message&& msg) : + self_(self), msg_(std::move(msg)) + { + } + + void operator()() + { + boost::beast::http::async_write(self_.stream_, msg_, + boost::beast::bind_front_handler(&HttpSession::OnWrite, + self_.shared_from_this(), msg_.need_eof())); + } + }; + + items_.push_back(boost::make_unique(self_, std::move(msg))); + + if (items_.size() == 1) + (*items_.front())(); + } + }; + + boost::beast::tcp_stream stream_; + boost::beast::flat_buffer buffer_; + std::shared_ptr doc_root_; + Queue queue_; + boost::optional> parser_; + +public: + HttpSession(boost::asio::ip::tcp::socket&& socket, std::shared_ptr const& doc_root); + + void Run(); + +private: + void DoRead(); + + void OnRead(boost::beast::error_code ec, std::size_t bytes_transferred); + + void OnWrite(bool close, boost::beast::error_code ec, std::size_t bytes_transferred); + + void DoClose(); +}; +} diff --git a/src/session/WebsocketSession.cpp b/src/session/WebsocketSession.cpp new file mode 100644 index 0000000..8f33c81 --- /dev/null +++ b/src/session/WebsocketSession.cpp @@ -0,0 +1,51 @@ +#include "WebsocketSession.h" + +#include "./../helpers/helpers.h" + +namespace uad +{ +WebsocketSession::WebsocketSession(boost::asio::ip::tcp::socket&& socket) : ws_(std::move(socket)) +{ +} + +void WebsocketSession::OnAccept(boost::beast::error_code ec) +{ + if (ec) + return Fail(ec, "accept"); + + DoRead(); +} + +void WebsocketSession::DoRead() +{ + ws_.async_read(buffer_, + boost::beast::bind_front_handler(&WebsocketSession::OnRead, shared_from_this())); +} + +void WebsocketSession::OnRead(boost::beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if (ec == boost::beast::websocket::error::closed) + return; + + if (ec) + Fail(ec, "read"); + + ws_.text(ws_.got_text()); + ws_.async_write(buffer_.data(), + boost::beast::bind_front_handler(&WebsocketSession::OnWrite, shared_from_this())); +} + +void WebsocketSession::OnWrite(boost::beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if (ec) + return Fail(ec, "write"); + + buffer_.consume(buffer_.size()); + + DoRead(); +} +} // namespace uad diff --git a/src/session/WebsocketSession.h b/src/session/WebsocketSession.h new file mode 100644 index 0000000..6347880 --- /dev/null +++ b/src/session/WebsocketSession.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +namespace uad +{ +class WebsocketSession : public std::enable_shared_from_this +{ + boost::beast::websocket::stream ws_; + boost::beast::flat_buffer buffer_; + +public: + explicit WebsocketSession(boost::asio::ip::tcp::socket&& socket); + + template + void DoAccept(boost::beast::http::request> req) + { + ws_.set_option(boost::beast::websocket::stream_base::timeout::suggested(boost::beast::role_type::server)); + + ws_.set_option(boost::beast::websocket::stream_base::decorator( + [](boost::beast::websocket::response_type& res) + { + res.set(boost::beast::http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " advanced-server"); + })); + + ws_.async_accept(req, + boost::beast::bind_front_handler(&WebsocketSession::OnAccept, shared_from_this())); + } + +private: + void OnAccept(boost::beast::error_code ec); + + void DoRead(); + + void OnRead(boost::beast::error_code ec, std::size_t bytes_transferred); + + void OnWrite(boost::beast::error_code ec, std::size_t bytes_transferred); +}; +} diff --git a/tests/helpers/helpers_TEST.cpp b/tests/helpers/helpers_TEST.cpp new file mode 100644 index 0000000..d95afce --- /dev/null +++ b/tests/helpers/helpers_TEST.cpp @@ -0,0 +1,85 @@ +#ifdef WIN32 +#include +#endif + +#define BOOST_TEST_MODULE Helpers + +#include "./../../src/helpers/helpers.h" + +#include + +#include +#include + +using namespace boost; +using namespace std; +using namespace uad; + +BOOST_AUTO_TEST_CASE(ToHex_should_cast_single_byte_to_hex_string) +{ + byte a1 = byte(0); + byte a2 = byte(1); + byte a3 = byte(2); + byte a4 = byte(3); + byte a5 = byte(4); + byte a6 = byte(5); + byte a7 = byte(6); + byte a8 = byte(15); + byte a9 = byte(64); + byte a10 = byte(97); + byte a11 = byte(127); + byte a12 = byte(255); + + BOOST_CHECK_EQUAL(ToHex(&a1, 1), "00"s); + BOOST_CHECK_EQUAL(ToHex(&a2, 1), "01"s); + BOOST_CHECK_EQUAL(ToHex(&a3, 1), "02"s); + BOOST_CHECK_EQUAL(ToHex(&a4, 1), "03"s); + BOOST_CHECK_EQUAL(ToHex(&a5, 1), "04"s); + BOOST_CHECK_EQUAL(ToHex(&a6, 1), "05"s); + BOOST_CHECK_EQUAL(ToHex(&a7, 1), "06"s); + BOOST_CHECK_EQUAL(ToHex(&a8, 1), "0F"s); + BOOST_CHECK_EQUAL(ToHex(&a9, 1), "40"s); + BOOST_CHECK_EQUAL(ToHex(&a10, 1), "61"s); + BOOST_CHECK_EQUAL(ToHex(&a11, 1), "7F"s); + BOOST_CHECK_EQUAL(ToHex(&a12, 1), "FF"s); +} + +BOOST_AUTO_TEST_CASE(ToHex_should_return_empty_string_if_no_arr_or_no_length) +{ + byte a1 = byte(0); + + BOOST_CHECK_EQUAL(ToHex(nullptr, 0), ""s); + BOOST_CHECK_EQUAL(ToHex(&a1, 0), ""s); + BOOST_CHECK_EQUAL(ToHex(nullptr, 1), ""s); +} + +// BOOST_AUTO_TEST_CASE(ToHex_should_cast_bytes_vector_to_hex_string) +// { +// constexpr size_t kSize = 10; +// +// byte a1[kSize] = {byte(0), byte(1), byte(2), byte(3), byte(4), byte(5), byte(6), byte(7), byte(8), byte(9)}; +// byte a2[kSize] = {byte(1), byte(2), byte(3), byte(4), byte(5), byte(6), byte(7), byte(8), byte(9), byte(10)}; +// byte a3[kSize] = {byte(2), byte(3), byte(4), byte(5), byte(6), byte(7), byte(8), byte(9), byte(10), byte(11)}; +// byte a4[kSize] = {byte(3), byte(4), byte(5), byte(6), byte(7), byte(8), byte(9), byte(10), byte(11), byte(12)}; +// byte a5[kSize] = byte(4); +// byte a6[kSize] = byte(5); +// byte a7[kSize] = byte(6); +// byte a8[kSize] = byte(15); +// byte a9[kSize] = byte(64); +// byte a10[kSize] = byte(97); +// byte a11[kSize] = byte(127); +// byte a12[kSize] = byte(255); +// +// BOOST_CHECK_EQUAL(ToHex(&a1, 0), "00"s); +// BOOST_CHECK_EQUAL(ToHex(&a2, 0), "01"s); +// BOOST_CHECK_EQUAL(ToHex(&a3, 0), "02"s); +// BOOST_CHECK_EQUAL(ToHex(&a4, 0), "03"s); +// BOOST_CHECK_EQUAL(ToHex(&a5, 0), "04"s); +// BOOST_CHECK_EQUAL(ToHex(&a6, 0), "05"s); +// BOOST_CHECK_EQUAL(ToHex(&a7, 0), "06"s); +// BOOST_CHECK_EQUAL(ToHex(&a8, 0), "0F"s); +// BOOST_CHECK_EQUAL(ToHex(&a9, 0), "40"s); +// BOOST_CHECK_EQUAL(ToHex(&a10, 0), "61"s); +// BOOST_CHECK_EQUAL(ToHex(&a11, 0), "7F"s); +// BOOST_CHECK_EQUAL(ToHex(&a12, 0), "FF"s); +// }