From 9702b104b4d362d273e868a444ca4a8b7b4afb24 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 15 Aug 2024 12:19:18 +0100 Subject: [PATCH 01/81] WIP: writing server --- src/web/ng/Connection.hpp | 67 ++++++++++++++++++++++++++++++++++ src/web/ng/MessageHandler.hpp | 32 +++++++++++++++++ src/web/ng/Request.hpp | 26 ++++++++++++++ src/web/ng/Response.hpp | 26 ++++++++++++++ src/web/ng/Server.hpp | 68 +++++++++++++++++++++++++++++++++++ 5 files changed, 219 insertions(+) create mode 100644 src/web/ng/Connection.hpp create mode 100644 src/web/ng/MessageHandler.hpp create mode 100644 src/web/ng/Request.hpp create mode 100644 src/web/ng/Response.hpp diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp new file mode 100644 index 000000000..a44cf91d1 --- /dev/null +++ b/src/web/ng/Connection.hpp @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +namespace web::ng { + +class ConnectionContext; + +class Connection { +public: + virtual ~Connection() = default; + + virtual void + send() = 0; + + virtual void + receive() = 0; + + virtual void + close(std::chrono::steady_clock::duration timeout) = 0; + + void + subscribeToDisconnect(); + + ConnectionContext + context() const; + + struct Hash { + size_t + operator()(Connection const& connection) const; + }; +}; + +class ConnectionContext { + Connection& connection_; + +public: + explicit ConnectionContext(Connection& connection); + + ConnectionContext(ConnectionContext&&) = delete; + ConnectionContext(ConnectionContext const&) = default; + + bool + isAdmin() const; +}; + +} // namespace web::ng diff --git a/src/web/ng/MessageHandler.hpp b/src/web/ng/MessageHandler.hpp new file mode 100644 index 000000000..a958b7d99 --- /dev/null +++ b/src/web/ng/MessageHandler.hpp @@ -0,0 +1,32 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "web/ng/Connection.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include + +namespace web::ng { + +using MessageHandler = std::function; + +} // namespace web::ng diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp new file mode 100644 index 000000000..db1b5b24c --- /dev/null +++ b/src/web/ng/Request.hpp @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +namespace web::ng { + +class Request {}; + +} // namespace web::ng diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp new file mode 100644 index 000000000..ca116f9e3 --- /dev/null +++ b/src/web/ng/Response.hpp @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +namespace web::ng { + +class Response {}; + +} // namespace web::ng diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index e69de29bb..407a4f5d5 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/config/Config.hpp" +#include "web/dosguard/DOSGuardInterface.hpp" +#include "web/ng/MessageHandler.hpp" + +#include + +#include +#include +#include +#include + +namespace web::ng { + +class Server { + boost::asio::io_context& ctx_; + std::unique_ptr dosguard_; + std::unordered_map getHandlers_; + std::unordered_map postHandlers_; + std::optional wsHandler_; + +public: + Server( + util::Config const& config, + std::unique_ptr dosguard, + boost::asio::io_context& ctx + ); + + Server(Server const&) = delete; + Server(Server&&) = delete; + + void + run(); + + void + onGet(std::string target, MessageHandler handler); + + void + onPost(std::string target, MessageHandler handler); + + void + onWs(MessageHandler handler); + + void + stop(); +}; + +} // namespace web::ng From a7a70608359e470d971ad56fe890e4d1b8649dc7 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 15 Aug 2024 18:16:57 +0100 Subject: [PATCH 02/81] WIP: writing server --- src/web/ng/Server.cpp | 139 ++++++++++++++++++++++++++++++++++++++++++ src/web/ng/Server.hpp | 20 +++++- 2 files changed, 156 insertions(+), 3 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index e69de29bb..19aec3d4e 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -0,0 +1,139 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/Server.hpp" + +#include "util/config/Config.hpp" +#include "util/log/Logger.hpp" +#include "web/dosguard/DOSGuardInterface.hpp" +#include "web/ng/MessageHandler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace web::ng { + +Server::Server( + util::Config const& config, + std::unique_ptr dosguard, + boost::asio::io_context& ctx +) + : ctx_{ctx}, dosguard_{std::move(dosguard)} +{ + auto const serverConfig = config.section("server"); + + auto const address = boost::asio::ip::make_address(serverConfig.value("ip")); + auto const port = serverConfig.value("port"); + endpoint_ = boost::asio::ip::tcp::endpoint{address, port}; + + auto adminPassword = serverConfig.maybeValue("admin_password"); + auto const localAdmin = serverConfig.maybeValue("local_admin"); + + // Throw config error when localAdmin is true and admin_password is also set + if (localAdmin && localAdmin.value() && adminPassword) { + LOG(log_.error()) << "local_admin is true but admin_password is also set, please specify only one method " + "to authorize admin"; + throw std::logic_error("Admin config error, local_admin and admin_password can not be set together."); + } + // Throw config error when localAdmin is false but admin_password is not set + if (localAdmin && !localAdmin.value() && !adminPassword) { + LOG(log_.error()) << "local_admin is false but admin_password is not set, please specify one method " + "to authorize admin"; + throw std::logic_error("Admin config error, one method must be specified to authorize admin."); + } +} + +std::optional +Server::run() +{ + boost::asio::ip::tcp::acceptor acceptor{ctx_}; + try { + acceptor.open(endpoint_.protocol()); + acceptor.set_option(boost::asio::socket_base::reuse_address(true)); + acceptor.bind(endpoint_); + acceptor.listen(boost::asio::socket_base::max_listen_connections); + } catch (boost::system::system_error const& error) { + return fmt::format("Web server error: error setting up accepto - {}", error.what()); + } + + running_ = boost::asio::spawn( + ctx_, + [this, acceptor = std::move(acceptor)](boost::asio::yield_context yield) mutable { + while (true) { + boost::beast::error_code errorCode; + boost::asio::ip::tcp::socket socket{ctx_.get_executor()}; + + acceptor.async_accept(socket, yield[errorCode]); + if (errorCode) { + LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); + continue; + } + boost::asio::spawn(ctx_, [this, socket = std::move(socket)](boost::asio::yield_context yield) { + this->makeConnection(std::move(socket), yield); + }); + } + }, + boost::asio::use_future + ); + return std::nullopt; +} + +void +Server::onGet(std::string const& target, MessageHandler handler) +{ + getHandlers_[target] = std::move(handler); +} + +void +Server::onPost(std::string const& target, MessageHandler handler) +{ + postHandlers_[target] = std::move(handler); +} + +void +Server::onWs(MessageHandler handler) +{ + wsHandler_ = std::move(handler); +} + +void +Server::stop() +{ +} + +void +Server::makeConnnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) +{ +} + +} // namespace web::ng diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 407a4f5d5..73e0e79ed 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -20,11 +20,15 @@ #pragma once #include "util/config/Config.hpp" +#include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" #include "web/ng/MessageHandler.hpp" #include +#include +#include +#include #include #include #include @@ -33,12 +37,18 @@ namespace web::ng { class Server { + util::Logger log_{"WebServer"}; boost::asio::io_context& ctx_; std::unique_ptr dosguard_; + std::unordered_map getHandlers_; std::unordered_map postHandlers_; std::optional wsHandler_; + boost::asio::ip::tcp::endpoint endpoint_; + + std::future running_; + public: Server( util::Config const& config, @@ -49,20 +59,24 @@ class Server { Server(Server const&) = delete; Server(Server&&) = delete; - void + std::optional run(); void - onGet(std::string target, MessageHandler handler); + onGet(std::string const& target, MessageHandler handler); void - onPost(std::string target, MessageHandler handler); + onPost(std::string const& target, MessageHandler handler); void onWs(MessageHandler handler); void stop(); + +private: + void + makeConnnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); }; } // namespace web::ng From c03eecb95ed8ec2286d3cbe661297509496d2e46 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 16 Aug 2024 17:29:57 +0100 Subject: [PATCH 03/81] WIP: writing server --- src/web/CMakeLists.txt | 1 + src/web/ng/Connection.hpp | 6 ++ src/web/ng/Server.cpp | 131 +++++++++++++++++++++++---- src/web/ng/Server.hpp | 18 +++- src/web/ng/impl/HttpConnection.hpp | 70 ++++++++++++++ src/web/ng/impl/ServerSslContext.cpp | 100 ++++++++++++++++++++ src/web/ng/impl/ServerSslContext.hpp | 38 ++++++++ src/web/ng/impl/WsConnection.hpp | 0 8 files changed, 340 insertions(+), 24 deletions(-) create mode 100644 src/web/ng/impl/HttpConnection.hpp create mode 100644 src/web/ng/impl/ServerSslContext.cpp create mode 100644 src/web/ng/impl/ServerSslContext.hpp create mode 100644 src/web/ng/impl/WsConnection.hpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index cdf882ece..23790eee4 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources( impl/AdminVerificationStrategy.cpp impl/ServerSslContext.cpp ng/Server.cpp + ng/impl/ServerSslContext.cpp ) target_link_libraries(clio_web PUBLIC clio_util) diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index a44cf91d1..03b7f17df 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -21,6 +21,7 @@ #include #include +#include namespace web::ng { @@ -45,6 +46,9 @@ class Connection { ConnectionContext context() const; + std::string const& + tag() const; + struct Hash { size_t operator()(Connection const& connection) const; @@ -64,4 +68,6 @@ class ConnectionContext { isAdmin() const; }; +using ConnectionPtr = std::unique_ptr; + } // namespace web::ng diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 19aec3d4e..66d8097e4 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -19,18 +19,26 @@ #include "web/ng/Server.hpp" +#include "util/Assert.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" +#include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" +#include "web/ng/impl/HttpConnection.hpp" +#include "web/ng/impl/ServerSslContext.hpp" #include #include #include #include #include +#include #include +#include #include +#include +#include #include #include #include @@ -39,6 +47,7 @@ #include #include #include +#include #include namespace web::ng { @@ -50,12 +59,18 @@ Server::Server( ) : ctx_{ctx}, dosguard_{std::move(dosguard)} { + // TODO(kuznetsss): move this into make_Server() auto const serverConfig = config.section("server"); auto const address = boost::asio::ip::make_address(serverConfig.value("ip")); auto const port = serverConfig.value("port"); endpoint_ = boost::asio::ip::tcp::endpoint{address, port}; + auto expectedSslContext = impl::makeServerSslContext(config); + if (not expectedSslContext) + throw std::logic_error{expectedSslContext.error()}; + sslContext_ = std::move(expectedSslContext).value(); + auto adminPassword = serverConfig.maybeValue("admin_password"); auto const localAdmin = serverConfig.maybeValue("local_admin"); @@ -83,28 +98,24 @@ Server::run() acceptor.bind(endpoint_); acceptor.listen(boost::asio::socket_base::max_listen_connections); } catch (boost::system::system_error const& error) { - return fmt::format("Web server error: error setting up accepto - {}", error.what()); + return fmt::format("Web server error: {}", error.what()); } - running_ = boost::asio::spawn( - ctx_, - [this, acceptor = std::move(acceptor)](boost::asio::yield_context yield) mutable { - while (true) { - boost::beast::error_code errorCode; - boost::asio::ip::tcp::socket socket{ctx_.get_executor()}; - - acceptor.async_accept(socket, yield[errorCode]); - if (errorCode) { - LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); - continue; - } - boost::asio::spawn(ctx_, [this, socket = std::move(socket)](boost::asio::yield_context yield) { - this->makeConnection(std::move(socket), yield); - }); + boost::asio::spawn(ctx_, [this, acceptor = std::move(acceptor)](boost::asio::yield_context yield) mutable { + while (true) { + boost::beast::error_code errorCode; + boost::asio::ip::tcp::socket socket{ctx_.get_executor()}; + + acceptor.async_accept(socket, yield[errorCode]); + if (errorCode) { + LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); + continue; } - }, - boost::asio::use_future - ); + boost::asio::spawn(ctx_, [this, socket = std::move(socket)](boost::asio::yield_context yield) mutable { + makeConnection(std::move(socket), yield); + }); // maybe use boost::asio::detached here? + } + }); return std::nullopt; } @@ -132,8 +143,88 @@ Server::stop() } void -Server::makeConnnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) +Server::makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) +{ + auto const logError = [this](std::string_view message, boost::beast::error_code ec) { + LOG(log_.info()) << "Detector failed (" << message << "): " << ec.message(); + }; + + boost::beast::tcp_stream tcpStream{std::move(socket)}; + boost::beast::flat_buffer buffer; + boost::beast::error_code errorCode; + bool const isSsl = boost::beast::async_detect_ssl(tcpStream, buffer, yield[errorCode]); + + if (errorCode == boost::asio::ssl::error::stream_truncated) + return; + + if (errorCode) { + logError("detect", errorCode); + return; + } + + std::string ip; + try { + ip = socket.remote_endpoint().address().to_string(); + } catch (boost::system::system_error const& error) { + logError("cannot get remote endpoint", error.code()); + return; + } + + ConnectionPtr connection; + if (isSsl) { + if (not sslContext_.has_value()) { + logError("SSL is not supported by this server", errorCode); + return; + } + connection = std::make_unique(std::move(socket), *sslContext_); + } else { + connection = std::make_unique(std::move(socket)); + } + + bool upgraded = false; + // connection.fetch() + // if (connection.should_upgrade()) { + // connection = std::move(connection).upgrade(); + // upgraded = true; + // } + + auto connectionTag = connection->tag(); + + { + auto connections = connections_.lock(); + auto [_, inserted] = connections->insert(std::move(connection)); + ASSERT(inserted, "Connection with tag already exists"); + } + + if (upgraded) { + boost::asio::spawn( + ctx_, + [this, connectionTag = std::move(connectionTag)](boost::asio::yield_context yield) mutable { + handleConnectionLoop(std::move(connectionTag), yield); + } + ); + } else { + boost::asio::spawn( + ctx_, + [this, connectionTag = std::move(connectionTag)](boost::asio::yield_context yield) mutable { + handleConnection(std::move(connectionTag), yield); + } + ); + } +} + +void +Server::handleConnection(std::string connectionTag, boost::asio::yield_context yield) +{ + // read request from connection + // process the request + // send response +} + +void +Server::handleConnectionLoop(std::string connectionTag, boost::asio::yield_context yield) { + // loop of handleConnection calls } } // namespace web::ng diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 73e0e79ed..a1d5841b9 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -19,20 +19,23 @@ #pragma once +#include "util/Mutex.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" +#include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" #include #include #include +#include -#include #include #include #include #include +#include namespace web::ng { @@ -40,14 +43,15 @@ class Server { util::Logger log_{"WebServer"}; boost::asio::io_context& ctx_; std::unique_ptr dosguard_; + std::optional sslContext_; std::unordered_map getHandlers_; std::unordered_map postHandlers_; std::optional wsHandler_; - boost::asio::ip::tcp::endpoint endpoint_; + util::Mutex> connections_; - std::future running_; + boost::asio::ip::tcp::endpoint endpoint_; public: Server( @@ -76,7 +80,13 @@ class Server { private: void - makeConnnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); + makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); + + void + handleConnection(std::string connectionTag, boost::asio::yield_context yield); + + void + handleConnectionLoop(std::string connectionTag, boost::asio::yield_context yield); }; } // namespace web::ng diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp new file mode 100644 index 000000000..d61c85901 --- /dev/null +++ b/src/web/ng/impl/HttpConnection.hpp @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "web/ng/Connection.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace web::ng::impl { + +template +class HttpConnection : public Connection { + StreamType stream_; + +public: + HttpConnection(boost::asio::ip::tcp::socket socket) + requires std::is_same_v + : stream_{std::move(socket)} + { + } + + HttpConnection(boost::asio::ip::tcp::socket socket, boost::asio::ssl::context& sslCtx) + requires std::is_same_v> + : stream_{std::move(socket), sslCtx} + { + } + + void + send() override + { + } + + void + receive() override + { + } + void + close(std::chrono::steady_clock::duration) override + { + } +}; + +using PlainHttpConnection = HttpConnection; + +using SslHttpConnection = HttpConnection>; + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/ServerSslContext.cpp b/src/web/ng/impl/ServerSslContext.cpp new file mode 100644 index 000000000..9ef2e72b7 --- /dev/null +++ b/src/web/ng/impl/ServerSslContext.cpp @@ -0,0 +1,100 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/impl/ServerSslContext.hpp" + +#include "util/config/Config.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace web::ng::impl { + +namespace { + +std::optional +readFile(std::string const& path) +{ + std::ifstream const file(path, std::ios::in | std::ios::binary); + if (!file) + return {}; + + std::stringstream contents; + contents << file.rdbuf(); + return std::move(contents).str(); +} + +} // namespace + +std::expected, std::string> +makeServerSslContext(util::Config const& config) +{ + bool const configHasCertFile = config.contains("ssl_cert_file"); + bool const configHasKeyFile = config.contains("ssl_key_file"); + + if (configHasCertFile != configHasKeyFile) + return std::unexpected{"Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together."}; + + if (not configHasCertFile) + return std::nullopt; + + auto const certFilename = config.value("ssl_cert_file"); + auto const keyFilename = config.value("ssl_key_file"); + + return impl::makeServerSslContext(certFilename, keyFilename); +} + +std::expected +makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath) +{ + auto const certContent = readFile(certFilePath); + if (!certContent) + return std::unexpected{"Can't read SSL certificate: " + certFilePath}; + + auto const keyContent = readFile(keyFilePath); + if (!keyContent) + return std::unexpected{"Can't read SSL key: " + keyFilePath}; + + using namespace boost::asio; + + ssl::context ctx{ssl::context::tls_server}; + ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2); + + try { + ctx.use_certificate_chain(buffer(certContent->data(), certContent->size())); + ctx.use_private_key(buffer(keyContent->data(), keyContent->size()), ssl::context::file_format::pem); + } catch (...) { + return std::unexpected{ + fmt::format("Error loading SSL certificate ({}) or SSL key ({}).", certFilePath, keyFilePath) + }; + } + + return ctx; +} + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/ServerSslContext.hpp b/src/web/ng/impl/ServerSslContext.hpp new file mode 100644 index 000000000..da89d8727 --- /dev/null +++ b/src/web/ng/impl/ServerSslContext.hpp @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/config/Config.hpp" + +#include + +#include +#include +#include + +namespace web::ng::impl { + +std::expected, std::string> +makeServerSslContext(util::Config const& config); + +std::expected +makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath); + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp new file mode 100644 index 000000000..e69de29bb From 14f46c48478b5ac82e598328c0dfb405844fb836 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 19 Aug 2024 13:59:02 +0100 Subject: [PATCH 04/81] Add id for Connection --- src/web/CMakeLists.txt | 1 + src/web/ng/Connection.cpp | 49 +++++++++++++++++++++++++++++++++++++++ src/web/ng/Connection.hpp | 8 +++++-- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/web/ng/Connection.cpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 23790eee4..9c9974c90 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources( dosguard/WhitelistHandler.cpp impl/AdminVerificationStrategy.cpp impl/ServerSslContext.cpp + ng/Connection.cpp ng/Server.cpp ng/impl/ServerSslContext.cpp ) diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp new file mode 100644 index 000000000..8750cd649 --- /dev/null +++ b/src/web/ng/Connection.cpp @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/Connection.hpp" + +#include +#include +#include + +namespace web::ng { + +namespace { + +size_t +generateId() +{ + static std::atomic_size_t id{0}; + return id++; +} + +} // namespace + +Connection::Connection() : id_{generateId()} +{ +} + +size_t +Connection::Hash::operator()(Connection const& connection) const +{ + return std::hash{}(connection.id()); +} + +} // namespace web::ng diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 03b7f17df..e83cbe03d 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -28,7 +28,11 @@ namespace web::ng { class ConnectionContext; class Connection { + size_t id_; + public: + Connection(); + virtual ~Connection() = default; virtual void @@ -46,8 +50,8 @@ class Connection { ConnectionContext context() const; - std::string const& - tag() const; + size_t + id() const; struct Hash { size_t From 83cd9706354574c8d82fc92205f9b2595d0fc0d9 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 19 Aug 2024 17:17:01 +0100 Subject: [PATCH 05/81] WIP --- src/web/ng/Connection.hpp | 15 ++++++--- src/web/ng/Request.hpp | 15 ++++++++- src/web/ng/Server.cpp | 66 ++++++++++++++++++++++++++------------- src/web/ng/Server.hpp | 13 ++++++-- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index e83cbe03d..2ef9aefde 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -19,8 +19,15 @@ #pragma once +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include + #include #include +#include +#include #include namespace web::ng { @@ -36,10 +43,10 @@ class Connection { virtual ~Connection() = default; virtual void - send() = 0; + send(Response response, boost::asio::yield_context yield) = 0; - virtual void - receive() = 0; + virtual std::expected + receive(boost::asio::yield_context yield) = 0; virtual void close(std::chrono::steady_clock::duration timeout) = 0; @@ -60,7 +67,7 @@ class Connection { }; class ConnectionContext { - Connection& connection_; + std::reference_wrapper connection_; public: explicit ConnectionContext(Connection& connection); diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index db1b5b24c..f1bfe00b3 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -19,8 +19,21 @@ #pragma once +#include + namespace web::ng { -class Request {}; +class Request { +public: + enum class HttpMethod { GET, POST, WEBSOCKET, UNSUPPORTED }; + + HttpMethod + httpMethod() const; + + std::string const& + target() const; +}; + +class RequestError {}; } // namespace web::ng diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 66d8097e4..01e46e430 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -25,6 +25,8 @@ #include "web/dosguard/DOSGuardInterface.hpp" #include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" #include "web/ng/impl/HttpConnection.hpp" #include "web/ng/impl/ServerSslContext.hpp" @@ -43,7 +45,9 @@ #include #include +#include #include +#include #include #include #include @@ -188,43 +192,63 @@ Server::makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_c // upgraded = true; // } - auto connectionTag = connection->tag(); + Connection* connectionPtr = connection.get(); { - auto connections = connections_.lock(); - auto [_, inserted] = connections->insert(std::move(connection)); - ASSERT(inserted, "Connection with tag already exists"); + auto connections = connections_.lock(); + auto [it, inserted] = connections->insert(std::move(connection)); + ASSERT(inserted, "Connection with id {} already exists.", it->get()->id()); } if (upgraded) { - boost::asio::spawn( - ctx_, - [this, connectionTag = std::move(connectionTag)](boost::asio::yield_context yield) mutable { - handleConnectionLoop(std::move(connectionTag), yield); - } - ); + boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { + handleConnectionLoop(connectionRef, yield); + }); } else { - boost::asio::spawn( - ctx_, - [this, connectionTag = std::move(connectionTag)](boost::asio::yield_context yield) mutable { - handleConnection(std::move(connectionTag), yield); - } - ); + boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { + handleConnection(connectionRef, yield); + }); } } void -Server::handleConnection(std::string connectionTag, boost::asio::yield_context yield) +Server::handleConnection(Connection& connection, boost::asio::yield_context yield) { - // read request from connection - // process the request - // send response + auto expectedRequest = connection.receive(yield); + if (not expectedRequest.has_value()) { + } + auto response = handleRequest(std::move(expectedRequest).value()); + connection.send(std::move(response), yield); } void -Server::handleConnectionLoop(std::string connectionTag, boost::asio::yield_context yield) +Server::handleConnectionLoop(Connection& connection, boost::asio::yield_context yield) { // loop of handleConnection calls } +Response +Server::handleRequest(Request request, ConnectionContext connectionContext) +{ + auto process = [&connectionContext](Request request, auto& handlersMap) { + auto const it = handlersMap.find(request.target()); + if (it == handlersMap.end()) { + return Response{}; + } + return it->second(std::move(request), connectionContext); + }; + switch (request.httpMethod()) { + case Request::HttpMethod::GET: + return process(std::move(request), getHandlers_); + case Request::HttpMethod::POST: + return process(std::move(request), postHandlers_); + case Request::HttpMethod::WS: + if (wsHandler_) { + return (*wsHandler_)(std::move(request)); + } + default: + return Response{}; + } +} + } // namespace web::ng diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index a1d5841b9..34c00f0a5 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -25,14 +25,18 @@ #include "web/dosguard/DOSGuardInterface.hpp" #include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" #include #include #include #include +#include #include #include +#include #include #include #include @@ -49,7 +53,7 @@ class Server { std::unordered_map postHandlers_; std::optional wsHandler_; - util::Mutex> connections_; + util::Mutex, std::shared_mutex> connections_; boost::asio::ip::tcp::endpoint endpoint_; @@ -83,10 +87,13 @@ class Server { makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); void - handleConnection(std::string connectionTag, boost::asio::yield_context yield); + handleConnection(Connection& connection, boost::asio::yield_context yield); void - handleConnectionLoop(std::string connectionTag, boost::asio::yield_context yield); + handleConnectionLoop(Connection& connection, boost::asio::yield_context yield); + + Response + handleRequest(Request request, ConnectionContext connectionContext); }; } // namespace web::ng From e67804bd35ee9bbfea49eeaf985e21bc137c8241 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 20 Aug 2024 11:55:27 +0100 Subject: [PATCH 06/81] Improve Server building --- src/web/impl/AdminVerificationStrategy.cpp | 20 +++ src/web/impl/AdminVerificationStrategy.hpp | 6 + src/web/ng/Connection.cpp | 5 +- src/web/ng/Connection.hpp | 4 +- src/web/ng/Server.cpp | 147 +++++++++++++-------- src/web/ng/Server.hpp | 26 +++- 6 files changed, 141 insertions(+), 67 deletions(-) diff --git a/src/web/impl/AdminVerificationStrategy.cpp b/src/web/impl/AdminVerificationStrategy.cpp index 33332741c..8c65bbada 100644 --- a/src/web/impl/AdminVerificationStrategy.cpp +++ b/src/web/impl/AdminVerificationStrategy.cpp @@ -20,6 +20,7 @@ #include "web/impl/AdminVerificationStrategy.hpp" #include "util/JsonUtils.hpp" +#include "util/config/Config.hpp" #include #include @@ -79,4 +80,23 @@ make_AdminVerificationStrategy(std::optional password) return std::make_shared(); } +std::expected, std::string> +make_AdminVerificationStrategy(util::Config const& serverConfig) +{ + auto adminPassword = serverConfig.maybeValue("admin_password"); + auto const localAdmin = serverConfig.maybeValue("local_admin"); + + // Return error when localAdmin is true and admin_password is also set + if (localAdmin && localAdmin.value() && adminPassword) { + return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."}; + } + + // Return error when localAdmin is false but admin_password is not set + if (localAdmin && !localAdmin.value() && !adminPassword) { + return std::unexpected{"Admin config error, one method must be specified to authorize admin."}; + } + + return make_AdminVerificationStrategy(std::move(adminPassword)); +} + } // namespace web::impl diff --git a/src/web/impl/AdminVerificationStrategy.hpp b/src/web/impl/AdminVerificationStrategy.hpp index 0a2d8a19b..fd7726123 100644 --- a/src/web/impl/AdminVerificationStrategy.hpp +++ b/src/web/impl/AdminVerificationStrategy.hpp @@ -19,10 +19,13 @@ #pragma once +#include "util/config/Config.hpp" + #include #include #include +#include #include #include #include @@ -82,4 +85,7 @@ class PasswordAdminVerificationStrategy : public AdminVerificationStrategy { std::shared_ptr make_AdminVerificationStrategy(std::optional password); +std::expected, std::string> +make_AdminVerificationStrategy(util::Config const& serverConfig); + } // namespace web::impl diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index 8750cd649..0072927ad 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace web::ng { @@ -41,9 +42,9 @@ Connection::Connection() : id_{generateId()} } size_t -Connection::Hash::operator()(Connection const& connection) const +Connection::Hash::operator()(std::unique_ptr const& connection) const { - return std::hash{}(connection.id()); + return std::hash{}(connection->id()); } } // namespace web::ng diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 2ef9aefde..2b780cdf9 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -62,10 +62,12 @@ class Connection { struct Hash { size_t - operator()(Connection const& connection) const; + operator()(std::unique_ptr const& connection) const; }; }; +using ConnectionPtr = std::unique_ptr; + class ConnectionContext { std::reference_wrapper connection_; diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 01e46e430..ba1577b26 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -20,13 +20,13 @@ #include "web/ng/Server.hpp" #include "util/Assert.hpp" +#include "util/Mutex.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" +#include "web/impl/AdminVerificationStrategy.hpp" #include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" -#include "web/ng/Request.hpp" -#include "web/ng/Response.hpp" #include "web/ng/impl/HttpConnection.hpp" #include "web/ng/impl/ServerSslContext.hpp" @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -47,49 +48,47 @@ #include #include -#include #include -#include +#include #include #include #include namespace web::ng { -Server::Server( - util::Config const& config, - std::unique_ptr dosguard, - boost::asio::io_context& ctx -) - : ctx_{ctx}, dosguard_{std::move(dosguard)} +namespace { + +std::expected +makeEndpoint(util::Config const& serverConfig) { - // TODO(kuznetsss): move this into make_Server() - auto const serverConfig = config.section("server"); + auto const ip = serverConfig.maybeValue("ip"); + if (not ip.has_value()) + return std::unexpected{"Missing 'ip` in server config."}; - auto const address = boost::asio::ip::make_address(serverConfig.value("ip")); - auto const port = serverConfig.value("port"); - endpoint_ = boost::asio::ip::tcp::endpoint{address, port}; + auto const address = boost::asio::ip::make_address(*ip); + auto const port = serverConfig.maybeValue("port"); + if (not port.has_value()) + return std::unexpected{"Missing 'port` in server config."}; - auto expectedSslContext = impl::makeServerSslContext(config); - if (not expectedSslContext) - throw std::logic_error{expectedSslContext.error()}; - sslContext_ = std::move(expectedSslContext).value(); + return boost::asio::ip::tcp::endpoint{address, *port}; +} - auto adminPassword = serverConfig.maybeValue("admin_password"); - auto const localAdmin = serverConfig.maybeValue("local_admin"); +} // namespace - // Throw config error when localAdmin is true and admin_password is also set - if (localAdmin && localAdmin.value() && adminPassword) { - LOG(log_.error()) << "local_admin is true but admin_password is also set, please specify only one method " - "to authorize admin"; - throw std::logic_error("Admin config error, local_admin and admin_password can not be set together."); - } - // Throw config error when localAdmin is false but admin_password is not set - if (localAdmin && !localAdmin.value() && !adminPassword) { - LOG(log_.error()) << "local_admin is false but admin_password is not set, please specify one method " - "to authorize admin"; - throw std::logic_error("Admin config error, one method must be specified to authorize admin."); - } +Server::Server( + boost::asio::io_context& ctx, + boost::asio::ip::tcp::endpoint endpoint, + std::optional sslContext, + std::shared_ptr adminVerificationStrategy, + std::unique_ptr dosguard +) + : ctx_{ctx} + , dosguard_{std::move(dosguard)} + , adminVerificationStrategy_(std::move(adminVerificationStrategy)) + , sslContext_{std::move(sslContext)} + , connections_{std::make_unique>()} + , endpoint_{std::move(endpoint)} +{ } std::optional @@ -214,11 +213,11 @@ Server::makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_c void Server::handleConnection(Connection& connection, boost::asio::yield_context yield) { - auto expectedRequest = connection.receive(yield); - if (not expectedRequest.has_value()) { - } - auto response = handleRequest(std::move(expectedRequest).value()); - connection.send(std::move(response), yield); + // auto expectedRequest = connection.receive(yield); + // if (not expectedRequest.has_value()) { + // } + // auto response = handleRequest(std::move(expectedRequest).value()); + // connection.send(std::move(response), yield); } void @@ -227,28 +226,60 @@ Server::handleConnectionLoop(Connection& connection, boost::asio::yield_context // loop of handleConnection calls } -Response -Server::handleRequest(Request request, ConnectionContext connectionContext) +// Response +// Server::handleRequest(Request request, ConnectionContext connectionContext) +// { +// auto process = [&connectionContext](Request request, auto& handlersMap) { +// auto const it = handlersMap.find(request.target()); +// if (it == handlersMap.end()) { +// return Response{}; +// } +// return it->second(std::move(request), connectionContext); +// }; +// switch (request.httpMethod()) { +// case Request::HttpMethod::GET: +// return process(std::move(request), getHandlers_); +// case Request::HttpMethod::POST: +// return process(std::move(request), postHandlers_); +// case Request::HttpMethod::WS: +// if (wsHandler_) { +// return (*wsHandler_)(std::move(request)); +// } +// default: +// return Response{}; +// } +// } + +std::expected +make_Server( + util::Config const& config, + boost::asio::io_context& context, + std::unique_ptr dosguard +) { - auto process = [&connectionContext](Request request, auto& handlersMap) { - auto const it = handlersMap.find(request.target()); - if (it == handlersMap.end()) { - return Response{}; - } - return it->second(std::move(request), connectionContext); - }; - switch (request.httpMethod()) { - case Request::HttpMethod::GET: - return process(std::move(request), getHandlers_); - case Request::HttpMethod::POST: - return process(std::move(request), postHandlers_); - case Request::HttpMethod::WS: - if (wsHandler_) { - return (*wsHandler_)(std::move(request)); - } - default: - return Response{}; + auto const serverConfig = config.section("server"); + + auto endpoint = makeEndpoint(serverConfig); + if (not endpoint.has_value()) + return std::unexpected{std::move(endpoint).error()}; + + auto expectedSslContext = impl::makeServerSslContext(config); + if (not expectedSslContext) + return std::unexpected{std::move(expectedSslContext).error()}; + + auto adminVerificationStrategy = web::impl::make_AdminVerificationStrategy(serverConfig); + if (not adminVerificationStrategy) { + return std::unexpected{std::move(adminVerificationStrategy).error()}; } + + return Server{ + context, + std::move(endpoint).value(), + std::move(expectedSslContext).value(), + std::move(adminVerificationStrategy).value(), + std::move(dosguard) + + }; } } // namespace web::ng diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 34c00f0a5..3499409b0 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -23,6 +23,7 @@ #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" +#include "web/impl/AdminVerificationStrategy.hpp" #include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" #include "web/ng/Request.hpp" @@ -34,6 +35,7 @@ #include #include +#include #include #include #include @@ -45,27 +47,32 @@ namespace web::ng { class Server { util::Logger log_{"WebServer"}; - boost::asio::io_context& ctx_; + std::reference_wrapper ctx_; + std::unique_ptr dosguard_; + std::shared_ptr adminVerificationStrategy_; std::optional sslContext_; std::unordered_map getHandlers_; std::unordered_map postHandlers_; std::optional wsHandler_; - util::Mutex, std::shared_mutex> connections_; + using ConnectionsSet = std::unordered_set; + std::unique_ptr> connections_; boost::asio::ip::tcp::endpoint endpoint_; public: Server( - util::Config const& config, - std::unique_ptr dosguard, - boost::asio::io_context& ctx + boost::asio::io_context& ctx, + boost::asio::ip::tcp::endpoint endpoint, + std::optional sslContext, + std::shared_ptr adminVerificationStrategy, + std::unique_ptr dosguard ); Server(Server const&) = delete; - Server(Server&&) = delete; + Server(Server&&) = default; std::optional run(); @@ -96,4 +103,11 @@ class Server { handleRequest(Request request, ConnectionContext connectionContext); }; +std::expected +make_Server( + util::Config const& config, + boost::asio::io_context& context, + std::unique_ptr dosguard +); + } // namespace web::ng From bbb9d425ccec6f4534a7f1fd37bc2a5630040c55 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 20 Aug 2024 13:39:03 +0100 Subject: [PATCH 07/81] Separate creating acceptor --- src/web/ng/Server.cpp | 46 ++++++++++++++++++++++++++++++------------- src/web/ng/Server.hpp | 2 ++ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index ba1577b26..5bc565392 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -30,6 +30,7 @@ #include "web/ng/impl/HttpConnection.hpp" #include "web/ng/impl/ServerSslContext.hpp" +#include #include #include #include @@ -73,6 +74,21 @@ makeEndpoint(util::Config const& serverConfig) return boost::asio::ip::tcp::endpoint{address, *port}; } +std::expected +makeAcceptor(boost::asio::io_context& context, boost::asio::ip::tcp::endpoint const& endpoint) +{ + boost::asio::ip::tcp::acceptor acceptor{context}; + try { + acceptor.open(endpoint.protocol()); + acceptor.set_option(boost::asio::socket_base::reuse_address(true)); + acceptor.bind(endpoint); + acceptor.listen(boost::asio::socket_base::max_listen_connections); + } catch (boost::system::system_error const& error) { + return std::unexpected{fmt::format("Error creating TCP acceptor: {}", error.what())}; + } + return std::move(acceptor); +} + } // namespace Server::Server( @@ -94,29 +110,28 @@ Server::Server( std::optional Server::run() { - boost::asio::ip::tcp::acceptor acceptor{ctx_}; - try { - acceptor.open(endpoint_.protocol()); - acceptor.set_option(boost::asio::socket_base::reuse_address(true)); - acceptor.bind(endpoint_); - acceptor.listen(boost::asio::socket_base::max_listen_connections); - } catch (boost::system::system_error const& error) { - return fmt::format("Web server error: {}", error.what()); - } + auto acceptor = makeAcceptor(ctx_.get(), endpoint_); + if (not acceptor.has_value()) + return std::move(acceptor).error(); + running_ = true; boost::asio::spawn(ctx_, [this, acceptor = std::move(acceptor)](boost::asio::yield_context yield) mutable { while (true) { boost::beast::error_code errorCode; - boost::asio::ip::tcp::socket socket{ctx_.get_executor()}; + boost::asio::ip::tcp::socket socket{ctx_.get().get_executor()}; - acceptor.async_accept(socket, yield[errorCode]); + acceptor->async_accept(socket, yield[errorCode]); if (errorCode) { LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); continue; } - boost::asio::spawn(ctx_, [this, socket = std::move(socket)](boost::asio::yield_context yield) mutable { - makeConnection(std::move(socket), yield); - }); // maybe use boost::asio::detached here? + boost::asio::spawn( + ctx_.get(), + [this, socket = std::move(socket)](boost::asio::yield_context yield) mutable { + makeConnection(std::move(socket), yield); + }, + boost::asio::detached + ); } }); return std::nullopt; @@ -125,18 +140,21 @@ Server::run() void Server::onGet(std::string const& target, MessageHandler handler) { + ASSERT(not running_, "Adding a GET handler is not allowed when Server is running."); getHandlers_[target] = std::move(handler); } void Server::onPost(std::string const& target, MessageHandler handler) { + ASSERT(not running_, "Adding a POST handler is not allowed when Server is running."); postHandlers_[target] = std::move(handler); } void Server::onWs(MessageHandler handler) { + ASSERT(not running_, "Adding a Websocket handler is not allowed when Server is running."); wsHandler_ = std::move(handler); } diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 3499409b0..c8e8ef23a 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -62,6 +62,8 @@ class Server { boost::asio::ip::tcp::endpoint endpoint_; + bool running_{false}; + public: Server( boost::asio::io_context& ctx, From e8d04c25aa9a2e25be07f69b56fa6ae7ee76f967 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 20 Aug 2024 18:04:21 +0100 Subject: [PATCH 08/81] WIP: writing server --- src/web/ng/Connection.cpp | 4 +- src/web/ng/Connection.hpp | 7 +- src/web/ng/Server.cpp | 202 ++++++++++++++++++----------- src/web/ng/Server.hpp | 12 +- src/web/ng/impl/HttpConnection.hpp | 43 ++++-- 5 files changed, 173 insertions(+), 95 deletions(-) diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index 0072927ad..eb00e06c9 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include namespace web::ng { @@ -37,7 +39,7 @@ generateId() } // namespace -Connection::Connection() : id_{generateId()} +Connection::Connection(std::string ip) : id_{generateId()}, ip_{std::move(ip)} { } diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 2b780cdf9..c49b75a33 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -29,6 +29,7 @@ #include #include #include +#include namespace web::ng { @@ -36,12 +37,16 @@ class ConnectionContext; class Connection { size_t id_; + std::string ip_; // client ip public: - Connection(); + Connection(std::string ip); virtual ~Connection() = default; + virtual bool + upgraded() const = 0; + virtual void send(Response response, boost::asio::yield_context yield) = 0; diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 5bc565392..3beafa987 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -49,6 +49,7 @@ #include #include +#include #include #include #include @@ -89,6 +90,67 @@ makeAcceptor(boost::asio::io_context& context, boost::asio::ip::tcp::endpoint co return std::move(acceptor); } +std::expected +extractIp(boost::asio::ip::tcp::socket const& socket) +{ + std::string ip; + try { + ip = socket.remote_endpoint().address().to_string(); + } catch (boost::system::system_error const& error) { + return std::unexpected{error}; + } + return ip; +} + +struct SslDetectionResult { + boost::asio::ip::tcp::socket socket; + bool isSsl; + boost::beast::flat_buffer buffer; +}; + +std::expected, std::string> +detectSsl(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) +{ + boost::beast::tcp_stream tcpStream{std::move(socket)}; + boost::beast::flat_buffer buffer; + boost::beast::error_code errorCode; + bool const isSsl = boost::beast::async_detect_ssl(tcpStream, buffer, yield[errorCode]); + + if (errorCode == boost::asio::ssl::error::stream_truncated) + return std::nullopt; + + if (errorCode) + return std::unexpected{fmt::format("Detector failed (detect): {}", errorCode.message())}; + + return SslDetectionResult{.socket = tcpStream.release_socket(), .isSsl = isSsl, .buffer = std::move(buffer)}; +} + +std::expected +makeConnection( + SslDetectionResult sslDetectionResult, + std::optional& sslContext, + std::string ip, + boost::asio::yield_context yield +) +{ + impl::UpgradableConnectionPtr connection; + if (sslDetectionResult.isSsl) { + if (not sslContext.has_value()) + return std::unexpected{"SSL is not supported by this server"}; + + connection = + std::make_unique(std::move(sslDetectionResult.socket), std::move(ip), *sslContext); + } else { + connection = std::make_unique(std::move(sslDetectionResult.socket), std::move(ip)); + } + + connection->fetch(yield); + if (connection->isUpgradeRequested()) + return connection->upgrade(); + + return connection; +} + } // namespace Server::Server( @@ -107,6 +169,27 @@ Server::Server( { } +void +Server::onGet(std::string const& target, MessageHandler handler) +{ + ASSERT(not running_, "Adding a GET handler is not allowed when Server is running."); + getHandlers_[target] = std::move(handler); +} + +void +Server::onPost(std::string const& target, MessageHandler handler) +{ + ASSERT(not running_, "Adding a POST handler is not allowed when Server is running."); + postHandlers_[target] = std::move(handler); +} + +void +Server::onWs(MessageHandler handler) +{ + ASSERT(not running_, "Adding a Websocket handler is not allowed when Server is running."); + wsHandler_ = std::move(handler); +} + std::optional Server::run() { @@ -128,7 +211,7 @@ Server::run() boost::asio::spawn( ctx_.get(), [this, socket = std::move(socket)](boost::asio::yield_context yield) mutable { - makeConnection(std::move(socket), yield); + handleConnection(std::move(socket), yield); }, boost::asio::detached ); @@ -137,99 +220,60 @@ Server::run() return std::nullopt; } -void -Server::onGet(std::string const& target, MessageHandler handler) -{ - ASSERT(not running_, "Adding a GET handler is not allowed when Server is running."); - getHandlers_[target] = std::move(handler); -} - -void -Server::onPost(std::string const& target, MessageHandler handler) -{ - ASSERT(not running_, "Adding a POST handler is not allowed when Server is running."); - postHandlers_[target] = std::move(handler); -} - -void -Server::onWs(MessageHandler handler) -{ - ASSERT(not running_, "Adding a Websocket handler is not allowed when Server is running."); - wsHandler_ = std::move(handler); -} - void Server::stop() { } void -Server::makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) +Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) { - auto const logError = [this](std::string_view message, boost::beast::error_code ec) { - LOG(log_.info()) << "Detector failed (" << message << "): " << ec.message(); - }; - - boost::beast::tcp_stream tcpStream{std::move(socket)}; - boost::beast::flat_buffer buffer; - boost::beast::error_code errorCode; - bool const isSsl = boost::beast::async_detect_ssl(tcpStream, buffer, yield[errorCode]); - - if (errorCode == boost::asio::ssl::error::stream_truncated) - return; - - if (errorCode) { - logError("detect", errorCode); - return; + auto sslDetectionResultExpected = detectSsl(std::move(socket), yield); + if (not sslDetectionResultExpected) { + LOG(log_.info()) << sslDetectionResultExpected.error(); } + auto sslDetectionResult = std::move(sslDetectionResultExpected).value(); + if (not sslDetectionResult) + return; // stream truncated, probably user disconnected - std::string ip; - try { - ip = socket.remote_endpoint().address().to_string(); - } catch (boost::system::system_error const& error) { - logError("cannot get remote endpoint", error.code()); + auto ip = extractIp(sslDetectionResult->socket); + if (not ip.has_value()) { + LOG(log_.info()) << "Cannot get remote endpoint: " << ip.error().what(); return; } - ConnectionPtr connection; - if (isSsl) { - if (not sslContext_.has_value()) { - logError("SSL is not supported by this server", errorCode); - return; - } - connection = std::make_unique(std::move(socket), *sslContext_); - } else { - connection = std::make_unique(std::move(socket)); - } - - bool upgraded = false; - // connection.fetch() - // if (connection.should_upgrade()) { - // connection = std::move(connection).upgrade(); - // upgraded = true; - // } - - Connection* connectionPtr = connection.get(); - - { - auto connections = connections_.lock(); - auto [it, inserted] = connections->insert(std::move(connection)); - ASSERT(inserted, "Connection with id {} already exists.", it->get()->id()); - } - - if (upgraded) { - boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { - handleConnectionLoop(connectionRef, yield); - }); - } else { - boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { - handleConnection(connectionRef, yield); - }); + auto connectionExpected = makeConnection(std::move(sslDetectionResult), sslContext_, std::move(ip).value(), yield); + if (not connectionExpected.has_value()) { + LOG(log_.info()) << "Error creating a connection: " << connectionExpected.error(); } + // insert connection into connections_ + // run processConnection or processConnectionLoop } +// void +// Server::makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) +// { +// Connection* connectionPtr = connection.get(); +// +// { +// auto connections = connections_->lock(); +// auto [it, inserted] = connections->insert(std::move(connection)); +// ASSERT(inserted, "Connection with id {} already exists.", it->get()->id()); +// } +// +// if (upgraded) { +// boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { +// handleConnectionLoop(connectionRef, yield); +// }); +// } else { +// boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { +// handleConnection(connectionRef, yield); +// }); +// } +// } + void -Server::handleConnection(Connection& connection, boost::asio::yield_context yield) +Server::processConnection(Connection& connection, boost::asio::yield_context yield) { // auto expectedRequest = connection.receive(yield); // if (not expectedRequest.has_value()) { @@ -239,7 +283,7 @@ Server::handleConnection(Connection& connection, boost::asio::yield_context yiel } void -Server::handleConnectionLoop(Connection& connection, boost::asio::yield_context yield) +Server::processConnectionLoop(Connection& connection, boost::asio::yield_context yield) { // loop of handleConnection calls } diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index c8e8ef23a..c1870b769 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -76,9 +76,6 @@ class Server { Server(Server const&) = delete; Server(Server&&) = default; - std::optional - run(); - void onGet(std::string const& target, MessageHandler handler); @@ -88,18 +85,21 @@ class Server { void onWs(MessageHandler handler); + std::optional + run(); + void stop(); private: void - makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); + handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); void - handleConnection(Connection& connection, boost::asio::yield_context yield); + processConnection(Connection& connection, boost::asio::yield_context yield); void - handleConnectionLoop(Connection& connection, boost::asio::yield_context yield); + processConnectionLoop(Connection& connection, boost::asio::yield_context yield); Response handleRequest(Request request, ConnectionContext connectionContext); diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index d61c85901..9f4bef02a 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -20,41 +20,68 @@ #pragma once #include "web/ng/Connection.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" #include +#include #include #include #include #include +#include +#include #include namespace web::ng::impl { +class UpgradableConnection : public Connection { +public: + using Connection::Connection; + + virtual bool + isUpgradeRequested() const = 0; + + virtual ConnectionPtr + upgrade() const = 0; + + virtual void + fetch(boost::asio::yield_context yield) = 0; +}; + +using UpgradableConnectionPtr = std::unique_ptr; + template -class HttpConnection : public Connection { +class HttpConnection : public UpgradableConnection { StreamType stream_; public: - HttpConnection(boost::asio::ip::tcp::socket socket) + HttpConnection(boost::asio::ip::tcp::socket socket, std::string ip) requires std::is_same_v - : stream_{std::move(socket)} + : UpgradableConnection(std::move(ip)), stream_{std::move(socket)} { } - HttpConnection(boost::asio::ip::tcp::socket socket, boost::asio::ssl::context& sslCtx) + HttpConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::asio::ssl::context& sslCtx) requires std::is_same_v> - : stream_{std::move(socket), sslCtx} + : UpgradableConnection(std::move(ip)), stream_{std::move(socket), sslCtx} { } - void - send() override + bool + upgraded() const override { + return false; } void - receive() override + send(Response, boost::asio::yield_context) override + { + } + + std::expected + receive(boost::asio::yield_context) override { } void From ba9176b78cfcabf6071bece4fc1ec5fea54758f9 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 23 Aug 2024 13:44:53 +0100 Subject: [PATCH 09/81] Fix build, finish new connection handling --- src/web/ng/Connection.cpp | 8 -------- src/web/ng/Connection.hpp | 7 +------ src/web/ng/Server.cpp | 33 ++++++++++++++++++++++++------ src/web/ng/Server.hpp | 8 +++++--- src/web/ng/impl/HttpConnection.hpp | 21 ++++++++++++++++++- 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index eb00e06c9..8ee6fee3e 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -21,8 +21,6 @@ #include #include -#include -#include #include #include @@ -43,10 +41,4 @@ Connection::Connection(std::string ip) : id_{generateId()}, ip_{std::move(ip)} { } -size_t -Connection::Hash::operator()(std::unique_ptr const& connection) const -{ - return std::hash{}(connection->id()); -} - } // namespace web::ng diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index c49b75a33..dcb0c13bb 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -45,7 +45,7 @@ class Connection { virtual ~Connection() = default; virtual bool - upgraded() const = 0; + wasUpgraded() const = 0; virtual void send(Response response, boost::asio::yield_context yield) = 0; @@ -64,11 +64,6 @@ class Connection { size_t id() const; - - struct Hash { - size_t - operator()(std::unique_ptr const& connection) const; - }; }; using ConnectionPtr = std::unique_ptr; diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 3beafa987..45a960a57 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -48,12 +48,12 @@ #include #include +#include #include #include #include #include #include -#include #include namespace web::ng { @@ -141,7 +141,7 @@ makeConnection( connection = std::make_unique(std::move(sslDetectionResult.socket), std::move(ip), *sslContext); } else { - connection = std::make_unique(std::move(sslDetectionResult.socket), std::move(ip)); + connection = std::make_unique(std::move(sslDetectionResult.socket), std::move(ip)); } connection->fetch(yield); @@ -164,7 +164,7 @@ Server::Server( , dosguard_{std::move(dosguard)} , adminVerificationStrategy_(std::move(adminVerificationStrategy)) , sslContext_{std::move(sslContext)} - , connections_{std::make_unique>()} + , connections_{std::make_unique>()} , endpoint_{std::move(endpoint)} { } @@ -231,6 +231,7 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield auto sslDetectionResultExpected = detectSsl(std::move(socket), yield); if (not sslDetectionResultExpected) { LOG(log_.info()) << sslDetectionResultExpected.error(); + return; } auto sslDetectionResult = std::move(sslDetectionResultExpected).value(); if (not sslDetectionResult) @@ -242,12 +243,22 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield return; } - auto connectionExpected = makeConnection(std::move(sslDetectionResult), sslContext_, std::move(ip).value(), yield); + auto connectionExpected = + makeConnection(std::move(sslDetectionResult).value(), sslContext_, std::move(ip).value(), yield); if (not connectionExpected.has_value()) { LOG(log_.info()) << "Error creating a connection: " << connectionExpected.error(); + return; } - // insert connection into connections_ - // run processConnection or processConnectionLoop + + Connection& connection = insertConnection(std::move(connectionExpected).value()); + + boost::asio::spawn(ctx_, [this, &connection = connection](boost::asio::yield_context yield) { + if (connection.wasUpgraded()) { + processConnectionLoop(connection, yield); + } else { + processConnection(connection, yield); + } + }); } // void @@ -312,6 +323,16 @@ Server::processConnectionLoop(Connection& connection, boost::asio::yield_context // } // } +Connection& +Server::insertConnection(ConnectionPtr connection) +{ + auto connectionsMap = connections_->lock(); + auto const connectionId = connection->id(); + auto [it, inserted] = connectionsMap->emplace(connectionId, std::move(connection)); + ASSERT(inserted, "Connection with id {} already exists", it->second->id()); + return *it->second.get(); +} + std::expected make_Server( util::Config const& config, diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index c1870b769..307cb1462 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -41,7 +41,6 @@ #include #include #include -#include namespace web::ng { @@ -57,8 +56,8 @@ class Server { std::unordered_map postHandlers_; std::optional wsHandler_; - using ConnectionsSet = std::unordered_set; - std::unique_ptr> connections_; + using ConnectionsMap = std::unordered_map; + std::unique_ptr> connections_; boost::asio::ip::tcp::endpoint endpoint_; @@ -103,6 +102,9 @@ class Server { Response handleRequest(Request request, ConnectionContext connectionContext); + + Connection& + insertConnection(ConnectionPtr connection); }; std::expected diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index 9f4bef02a..1c4243629 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -70,7 +71,7 @@ class HttpConnection : public UpgradableConnection { } bool - upgraded() const override + wasUpgraded() const override { return false; } @@ -84,10 +85,28 @@ class HttpConnection : public UpgradableConnection { receive(boost::asio::yield_context) override { } + void close(std::chrono::steady_clock::duration) override { } + + bool + isUpgradeRequested() const override + { + return false; + } + + ConnectionPtr + upgrade() const override + { + return nullptr; + } + + void + fetch(boost::asio::yield_context) override + { + } }; using PlainHttpConnection = HttpConnection; From a6137e8a855976ce488e2213c90fde72fe44fff0 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 23 Aug 2024 13:58:34 +0100 Subject: [PATCH 10/81] Small improvements --- src/web/ng/Server.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 45a960a57..db5f08235 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -198,12 +198,12 @@ Server::run() return std::move(acceptor).error(); running_ = true; - boost::asio::spawn(ctx_, [this, acceptor = std::move(acceptor)](boost::asio::yield_context yield) mutable { + boost::asio::spawn(ctx_, [this, acceptor = std::move(acceptor).value()](boost::asio::yield_context yield) mutable { while (true) { boost::beast::error_code errorCode; boost::asio::ip::tcp::socket socket{ctx_.get().get_executor()}; - acceptor->async_accept(socket, yield[errorCode]); + acceptor.async_accept(socket, yield[errorCode]); if (errorCode) { LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); continue; @@ -326,8 +326,8 @@ Server::processConnectionLoop(Connection& connection, boost::asio::yield_context Connection& Server::insertConnection(ConnectionPtr connection) { - auto connectionsMap = connections_->lock(); auto const connectionId = connection->id(); + auto connectionsMap = connections_->lock(); auto [it, inserted] = connectionsMap->emplace(connectionId, std::move(connection)); ASSERT(inserted, "Connection with id {} already exists", it->second->id()); return *it->second.get(); From 19105351fd5c0cd398a4f007fb66f1306d3e20d1 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 27 Aug 2024 18:03:09 +0100 Subject: [PATCH 11/81] Implement HttpConnection (except upgrade) --- src/web/ng/Connection.hpp | 23 +++++-- src/web/ng/Error.hpp | 28 ++++++++ src/web/ng/Request.hpp | 9 ++- src/web/ng/Response.hpp | 17 ++++- src/web/ng/Server.cpp | 11 +++- src/web/ng/impl/HttpConnection.hpp | 101 +++++++++++++++++++++++------ src/web/ng/impl/WsConnection.hpp | 51 +++++++++++++++ 7 files changed, 207 insertions(+), 33 deletions(-) create mode 100644 src/web/ng/Error.hpp diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index dcb0c13bb..314e2fd50 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -19,16 +19,19 @@ #pragma once +#include "web/ng/Error.hpp" #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" #include +#include #include #include #include #include #include +#include #include namespace web::ng { @@ -36,25 +39,33 @@ namespace web::ng { class ConnectionContext; class Connection { +protected: size_t id_; std::string ip_; // client ip + boost::beast::flat_buffer buffer_; public: - Connection(std::string ip); + static constexpr std::chrono::steady_clock::duration DEFAULT_TIMEOUT = std::chrono::seconds{30}; + + Connection(std::string ip, boost::beast::flat_buffer buffer); virtual ~Connection() = default; virtual bool wasUpgraded() const = 0; - virtual void - send(Response response, boost::asio::yield_context yield) = 0; + virtual std::optional + send( + Response response, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT + ) = 0; - virtual std::expected - receive(boost::asio::yield_context yield) = 0; + virtual std::expected + receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0; virtual void - close(std::chrono::steady_clock::duration timeout) = 0; + close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0; void subscribeToDisconnect(); diff --git a/src/web/ng/Error.hpp b/src/web/ng/Error.hpp new file mode 100644 index 000000000..bca1d708b --- /dev/null +++ b/src/web/ng/Error.hpp @@ -0,0 +1,28 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include + +namespace web::ng { + +using Error = boost::system::error_code; + +} // namespace web::ng diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index f1bfe00b3..831c2de66 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -19,12 +19,19 @@ #pragma once +#include +#include + #include namespace web::ng { class Request { + boost::beast::http::request request_; + public: + explicit Request(boost::beast::http::request request); + enum class HttpMethod { GET, POST, WEBSOCKET, UNSUPPORTED }; HttpMethod @@ -34,6 +41,4 @@ class Request { target() const; }; -class RequestError {}; - } // namespace web::ng diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index ca116f9e3..23cae0e87 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -19,8 +19,23 @@ #pragma once +#include +#include +#include + +#include namespace web::ng { -class Response {}; +class Response { + std::string message_; + boost::beast::http::status status_; + +public: + Response(std::string message, boost::beast::http::status); + virtual ~Response() = default; + + boost::beast::http::response + toHttpResponse() &&; +}; } // namespace web::ng diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index db5f08235..167783f1e 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -138,10 +138,13 @@ makeConnection( if (not sslContext.has_value()) return std::unexpected{"SSL is not supported by this server"}; - connection = - std::make_unique(std::move(sslDetectionResult.socket), std::move(ip), *sslContext); + connection = std::make_unique( + std::move(sslDetectionResult.socket), std::move(ip), std::move(sslDetectionResult.buffer), *sslContext + ); } else { - connection = std::make_unique(std::move(sslDetectionResult.socket), std::move(ip)); + connection = std::make_unique( + std::move(sslDetectionResult.socket), std::move(ip), std::move(sslDetectionResult.buffer) + ); } connection->fetch(yield); @@ -243,6 +246,8 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield return; } + // TODO(kuznetsss): check ip with dosguard here + auto connectionExpected = makeConnection(std::move(sslDetectionResult).value(), sslContext_, std::move(ip).value(), yield); if (not connectionExpected.has_value()) { diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index 1c4243629..bd416c7ee 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -20,6 +20,7 @@ #pragma once #include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" @@ -28,10 +29,17 @@ #include #include #include +#include +#include #include +#include +#include +#include +#include #include #include +#include #include #include @@ -41,32 +49,42 @@ class UpgradableConnection : public Connection { public: using Connection::Connection; - virtual bool - isUpgradeRequested() const = 0; + virtual std::expected + isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) + const = 0; virtual ConnectionPtr upgrade() const = 0; - - virtual void - fetch(boost::asio::yield_context yield) = 0; }; using UpgradableConnectionPtr = std::unique_ptr; +template +concept IsTcpStream = std::is_same_v; + +template +concept IsSslTcpStream = std::is_same_v>; + template class HttpConnection : public UpgradableConnection { StreamType stream_; + std::optional> request_; public: - HttpConnection(boost::asio::ip::tcp::socket socket, std::string ip) - requires std::is_same_v - : UpgradableConnection(std::move(ip)), stream_{std::move(socket)} + HttpConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer) + requires IsTcpStream + : UpgradableConnection(std::move(ip), std::move(buffer)), stream_{std::move(socket)} { } - HttpConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::asio::ssl::context& sslCtx) - requires std::is_same_v> - : UpgradableConnection(std::move(ip)), stream_{std::move(socket), sslCtx} + HttpConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + boost::asio::ssl::context& sslCtx + ) + requires IsSslTcpStream + : UpgradableConnection(std::move(ip), std::move(buffer)), stream_{std::move(socket), sslCtx} { } @@ -76,36 +94,77 @@ class HttpConnection : public UpgradableConnection { return false; } - void - send(Response, boost::asio::yield_context) override + std::optional + send( + Response response, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT + ) override { + auto const httpResponse = std::move(response).toHttpResponse(); + boost::system::error_code error; + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + boost::beast::http::async_write(stream_, httpResponse, yield[error]); + if (error) + return error; + return std::nullopt; } - std::expected - receive(boost::asio::yield_context) override + std::expected + receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override { + if (request_.has_value()) { + Request result{std::move(request_).value()}; + request_.reset(); + return std::move(result); + } + return fetch(yield, timeout); } void - close(std::chrono::steady_clock::duration) override + close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override { + [[maybe_unused]] boost::system::error_code error; + if constexpr (IsSslTcpStream) { + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + stream_.async_shutdown(yield[error]); + } + stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, error); } - bool - isUpgradeRequested() const override + std::expected + isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) + const override { - return false; + auto expectedRequest = fetch(yield, timeout); + if (not expectedRequest.has_value()) + return std::move(expectedRequest).error(); + + request_ = std::move(expectedRequest).value(); + + return boost::beast::websocket::is_upgrade(request_.value()); } ConnectionPtr upgrade() const override { + if constexpr (IsSslTcpStream) { + return std::make_unique(stream_.socket) + } return nullptr; } - void - fetch(boost::asio::yield_context) override +private: + std::expected, Error> + fetch(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) { + boost::beast::http::request request{}; + boost::system::error_code error; + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + boost::beast::http::async_read(stream_, buffer_, request, yield[error]); + if (error) + return std::unexpected{error}; + return std::move(request); } }; diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index e69de29bb..18891c5da 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "web/ng/Connection.hpp" + +#include +#include +#include + +#include + +namespace web::ng::impl { + +template +concept IsWsStream = std::is_same_v>; + +template +concept IsWsSslStream = + std::is_same_v>>; + +template +class WsConnection : public Connection { + StreamType stream_; + +public: + WsConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, boost::beast:: +}; + +using PlainWsConnection = WsConnection>; +using SslWsConnection = + WsConnection>>; + +} // namespace web::ng::impl From 3b0e40f444418b0d17a0ad77ec916e0e17b67c0a Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 28 Aug 2024 18:44:57 +0100 Subject: [PATCH 12/81] WIP: Implementing WsConnection --- src/web/CMakeLists.txt | 1 + src/web/ng/impl/Concepts.hpp | 35 +++++++++ src/web/ng/impl/HttpConnection.hpp | 38 ++++++--- src/web/ng/impl/WsConnection.cpp | 71 +++++++++++++++++ src/web/ng/impl/WsConnection.hpp | 119 +++++++++++++++++++++++++---- 5 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 src/web/ng/impl/Concepts.hpp create mode 100644 src/web/ng/impl/WsConnection.cpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 9c9974c90..163f7cc1e 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -12,6 +12,7 @@ target_sources( ng/Connection.cpp ng/Server.cpp ng/impl/ServerSslContext.cpp + ng/impl/WsConnection.cpp ) target_link_libraries(clio_web PUBLIC clio_util) diff --git a/src/web/ng/impl/Concepts.hpp b/src/web/ng/impl/Concepts.hpp new file mode 100644 index 000000000..801430dd0 --- /dev/null +++ b/src/web/ng/impl/Concepts.hpp @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include + +namespace web::ng::impl { + +template +concept IsTcpStream = std::is_same_v; + +template +concept IsSslTcpStream = std::is_same_v>; + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index bd416c7ee..5e801e366 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -19,10 +19,13 @@ #pragma once +#include "util/Assert.hpp" #include "web/ng/Connection.hpp" #include "web/ng/Error.hpp" #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" +#include "web/ng/impl/Concepts.hpp" +#include "web/ng/impl/WsConnection.hpp" #include #include @@ -53,18 +56,12 @@ class UpgradableConnection : public Connection { isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) const = 0; - virtual ConnectionPtr - upgrade() const = 0; + virtual std::expected + upgrade(std::optional& sslContext, boost::asio::yield_context yield) && = 0; }; using UpgradableConnectionPtr = std::unique_ptr; -template -concept IsTcpStream = std::is_same_v; - -template -concept IsSslTcpStream = std::is_same_v>; - template class HttpConnection : public UpgradableConnection { StreamType stream_; @@ -145,13 +142,30 @@ class HttpConnection : public UpgradableConnection { return boost::beast::websocket::is_upgrade(request_.value()); } - ConnectionPtr - upgrade() const override + std::expected + upgrade( + [[maybe_unused]] std::optional& sslContext, + boost::asio::yield_context yield + ) && + override { + ASSERT(request_.has_value(), "Request must be present to upgrade the connection"); + if constexpr (IsSslTcpStream) { - return std::make_unique(stream_.socket) + ASSERT(sslContext.has_value(), "SSL context must be present to upgrade the connection"); + return make_SslWsConnection( + boost::beast::get_lowest_layer(stream_).release_socket(), + std::move(ip_), + std::move(buffer_), + request_, + sslContext.value(), + yield + ); } - return nullptr; + + return make_PlainWsConnection( + stream_.release_socket(), std::move(ip_), std::move(buffer_), std::move(request_).value(), yield + ); } private: diff --git a/src/web/ng/impl/WsConnection.cpp b/src/web/ng/impl/WsConnection.cpp new file mode 100644 index 000000000..60a065641 --- /dev/null +++ b/src/web/ng/impl/WsConnection.cpp @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/impl/WsConnection.hpp" + +#include "web/ng/Error.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace web::ng::impl { + +std::expected, Error> +make_PlainWsConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + boost::beast::http::request const& request, + boost::asio::yield_context yield +) +{ + auto connection = std::make_unique(std::move(socket), std::move(ip), std::move(buffer)); + auto maybeError = connection->accept(request, yield); + if (maybeError.has_value()) + return std::unexpected{maybeError.value()}; + return std::move(connection); +} + +std::expected, Error> +make_SslWsConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + boost::beast::http::request const& request, + boost::asio::ssl::context& sslContext, + boost::asio::yield_context yield +) +{ + auto connection = + std::make_unique(std::move(socket), std::move(ip), std::move(buffer), sslContext); + auto maybeError = connection->accept(request, yield); + if (maybeError.has_value()) + return std::unexpected{maybeError.value()}; + return std::move(connection); +} + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index 18891c5da..38218250e 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -19,33 +19,126 @@ #pragma once +#include "util/build/Build.hpp" #include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" +#include "web/ng/impl/Concepts.hpp" +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include #include +#include -#include +#include +#include +#include +#include +#include namespace web::ng::impl { -template -concept IsWsStream = std::is_same_v>; - -template -concept IsWsSslStream = - std::is_same_v>>; - template class WsConnection : public Connection { - StreamType stream_; + boost::beast::websocket::stream stream_; public: - WsConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, boost::beast:: + WsConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer) + requires IsTcpStream + : Connection(std::move(ip), std::move(buffer)), stream_(std::move(socket)) + { + } + + WsConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + boost::asio::ssl::context& sslContext + ) + requires IsSslTcpStream + : Connection(std::move(ip), std::move(buffer)), stream_(std::move(socket), sslContext) + { + // Disable the timeout. The websocket::stream uses its own timeout settings. + boost::beast::get_lowest_layer(stream_).expires_never(); + stream_.set_option(boost::beast::websocket::stream_base::timeout::suggested(boost::beast::role_type::server)); + stream_.set_option( + boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::response_type& res) { + res.set(boost::beast::http::field::server, util::build::getClioFullVersionString()); + }) + ); + } + + std::optional + accept( + boost::beast::http::request const& request, + boost::asio::yield_context yield + ) + { + boost::system::error_code error; + stream_.async_accept(request, yield[error]); + if (error) + return Error{error}; + return std::nullopt; + } + + bool + wasUpgraded() const override + { + return true; + } + + std::optional + send( + Response response, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT + ) override + { + return {}; + } + + std::expected + receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override + { + return {}; + } + + void + close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override + { + } }; -using PlainWsConnection = WsConnection>; -using SslWsConnection = - WsConnection>>; +using PlainWsConnection = WsConnection; +using SslWsConnection = WsConnection>; + +std::expected, Error> +make_PlainWsConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + boost::beast::http::request const& request, + boost::asio::yield_context yield +); + +std::expected, Error> +make_SslWsConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + boost::beast::http::request const& request, + boost::asio::ssl::context& sslContext, + boost::asio::yield_context yield +); } // namespace web::ng::impl From 7313707c1c337715f75940621e7549785ecddd66 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 30 Aug 2024 11:42:38 +0100 Subject: [PATCH 13/81] Move withTimeout to a separate util --- src/util/WithTimeout.hpp | 65 +++++++++++++++++++++ src/util/requests/impl/WsConnectionImpl.hpp | 38 ++---------- 2 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 src/util/WithTimeout.hpp diff --git a/src/util/WithTimeout.hpp b/src/util/WithTimeout.hpp new file mode 100644 index 000000000..0afc3b74f --- /dev/null +++ b/src/util/WithTimeout.hpp @@ -0,0 +1,65 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace util { + +/** Perform a cotoutine operation with a timeout. + @param operation The operation to perform. + @param yield The yield context. + @param timeout The timeout duration. + @return The error code of the operation. +*/ +template Operation> +boost::system::error_code +withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) +{ + boost::system::error_code error; + boost::asio::cancellation_signal cancellationSignal; + auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]); + + boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout}; + timer.async_wait([&cancellationSignal](boost::system::error_code errorCode) { + if (!errorCode) + cancellationSignal.emit(boost::asio::cancellation_type::terminal); + }); + operation(cyield); + + // Map error code to timeout + if (error == boost::system::errc::operation_canceled) { + return boost::system::errc::make_error_code(boost::system::errc::timed_out); + } + return error; +} + +} // namespace util diff --git a/src/util/requests/impl/WsConnectionImpl.hpp b/src/util/requests/impl/WsConnectionImpl.hpp index df66bcaf3..2da01002f 100644 --- a/src/util/requests/impl/WsConnectionImpl.hpp +++ b/src/util/requests/impl/WsConnectionImpl.hpp @@ -19,6 +19,7 @@ #pragma once +#include "util/WithTimeout.hpp" #include "util/requests/Types.hpp" #include "util/requests/WsConnection.hpp" @@ -65,15 +66,13 @@ class WsConnectionImpl : public WsConnection { auto operation = [&](auto&& token) { ws_.async_read(buffer, token); }; if (timeout) { - withTimeout(operation, yield[errorCode], *timeout); + errorCode = util::withTimeout(operation, yield[errorCode], *timeout); } else { operation(yield[errorCode]); } - if (errorCode) { - errorCode = mapError(errorCode); + if (errorCode) return std::unexpected{RequestError{"Read error", errorCode}}; - } return boost::beast::buffers_to_string(std::move(buffer).data()); } @@ -88,15 +87,13 @@ class WsConnectionImpl : public WsConnection { boost::beast::error_code errorCode; auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); }; if (timeout) { - withTimeout(operation, yield[errorCode], *timeout); + errorCode = util::withTimeout(operation, yield, *timeout); } else { operation(yield[errorCode]); } - if (errorCode) { - errorCode = mapError(errorCode); + if (errorCode) return RequestError{"Write error", errorCode}; - } return std::nullopt; } @@ -117,31 +114,6 @@ class WsConnectionImpl : public WsConnection { return RequestError{"Close error", errorCode}; return std::nullopt; } - -private: - template - static void - withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) - { - boost::asio::cancellation_signal cancellationSignal; - auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield); - - boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout}; - timer.async_wait([&cancellationSignal](boost::system::error_code errorCode) { - if (!errorCode) - cancellationSignal.emit(boost::asio::cancellation_type::terminal); - }); - operation(cyield); - } - - static boost::system::error_code - mapError(boost::system::error_code const ec) - { - if (ec == boost::system::errc::operation_canceled) { - return boost::system::errc::make_error_code(boost::system::errc::timed_out); - } - return ec; - } }; using PlainWsConnection = WsConnectionImpl>; From e405242c1788fb5551def2bb937a00bd038fd18f Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 23 Sep 2024 14:07:22 +0100 Subject: [PATCH 14/81] Fix Connection constructor --- src/web/ng/Connection.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index 8ee6fee3e..233ff4efb 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -19,6 +19,8 @@ #include "web/ng/Connection.hpp" +#include + #include #include #include @@ -37,7 +39,8 @@ generateId() } // namespace -Connection::Connection(std::string ip) : id_{generateId()}, ip_{std::move(ip)} +Connection::Connection(std::string ip, boost::beast::flat_buffer buffer) + : id_{generateId()}, ip_{std::move(ip)}, buffer_{std::move(buffer)} { } From 2a6b1048cf91a03187786d607829f861150e56d6 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 23 Sep 2024 15:26:30 +0100 Subject: [PATCH 15/81] Fix connection upgrade --- src/util/WithTimeout.hpp | 2 +- src/web/ng/Server.cpp | 32 +++++++----------------------- src/web/ng/impl/HttpConnection.hpp | 7 ++----- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/util/WithTimeout.hpp b/src/util/WithTimeout.hpp index 0afc3b74f..725a27576 100644 --- a/src/util/WithTimeout.hpp +++ b/src/util/WithTimeout.hpp @@ -34,7 +34,7 @@ namespace util { -/** Perform a cotoutine operation with a timeout. +/** Perform a coroutine operation with a timeout. @param operation The operation to perform. @param yield The yield context. @param timeout The timeout duration. diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 167783f1e..3b1c56afe 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -26,6 +26,7 @@ #include "web/dosguard/DOSGuardInterface.hpp" #include "web/impl/AdminVerificationStrategy.hpp" #include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" #include "web/ng/MessageHandler.hpp" #include "web/ng/impl/HttpConnection.hpp" #include "web/ng/impl/ServerSslContext.hpp" @@ -147,9 +148,12 @@ makeConnection( ); } - connection->fetch(yield); - if (connection->isUpgradeRequested()) - return connection->upgrade(); + if (connection->isUpgradeRequested(yield)) { + return connection->upgrade(sslContext, yield) + .or_else([](Error error) -> std::expected { + return std::unexpected{fmt::format("Error upgrading connection: {}", error.what())}; + }); + } return connection; } @@ -266,28 +270,6 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield }); } -// void -// Server::makeConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) -// { -// Connection* connectionPtr = connection.get(); -// -// { -// auto connections = connections_->lock(); -// auto [it, inserted] = connections->insert(std::move(connection)); -// ASSERT(inserted, "Connection with id {} already exists.", it->get()->id()); -// } -// -// if (upgraded) { -// boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { -// handleConnectionLoop(connectionRef, yield); -// }); -// } else { -// boost::asio::spawn(ctx_, [this, &connectionRef = *connectionPtr](boost::asio::yield_context yield) mutable { -// handleConnection(connectionRef, yield); -// }); -// } -// } - void Server::processConnection(Connection& connection, boost::asio::yield_context yield) { diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index 5e801e366..ebe989d70 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -57,7 +57,7 @@ class UpgradableConnection : public Connection { const = 0; virtual std::expected - upgrade(std::optional& sslContext, boost::asio::yield_context yield) && = 0; + upgrade(std::optional& sslContext, boost::asio::yield_context yield) = 0; }; using UpgradableConnectionPtr = std::unique_ptr; @@ -143,10 +143,7 @@ class HttpConnection : public UpgradableConnection { } std::expected - upgrade( - [[maybe_unused]] std::optional& sslContext, - boost::asio::yield_context yield - ) && + upgrade([[maybe_unused]] std::optional& sslContext, boost::asio::yield_context yield) override { ASSERT(request_.has_value(), "Request must be present to upgrade the connection"); From a8e2ac4a18b6075b1cc99019e8062e60ee9faf77 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 24 Sep 2024 17:14:30 +0100 Subject: [PATCH 16/81] Implementing WsConnection --- src/web/CMakeLists.txt | 3 +- src/web/ng/Response.cpp | 81 ++++++++++++++++++++++++++++++ src/web/ng/Response.hpp | 23 +++++++-- src/web/ng/impl/HttpConnection.hpp | 4 +- src/web/ng/impl/WsConnection.hpp | 17 +++++-- 5 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 src/web/ng/Response.cpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 163f7cc1e..ebca93a67 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -10,9 +10,10 @@ target_sources( impl/AdminVerificationStrategy.cpp impl/ServerSslContext.cpp ng/Connection.cpp - ng/Server.cpp ng/impl/ServerSslContext.cpp ng/impl/WsConnection.cpp + ng/Server.cpp + ng/Response.cpp ) target_link_libraries(clio_web PUBLIC clio_util) diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp new file mode 100644 index 000000000..8f4208716 --- /dev/null +++ b/src/web/ng/Response.cpp @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/Response.hpp" + +#include "util/Assert.hpp" +#include "util/build/Build.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace web::ng { + +namespace { + +std::string_view +asString(Response::HttpData::ContentType type) +{ + switch (type) { + case Response::HttpData::ContentType::TEXT_HTML: + return "text/html"; + case Response::HttpData::ContentType::APPLICATION_JSON: + return "application/json"; + } + ASSERT(false, "Unknown content type"); + std::unreachable(); +} + +} // namespace + +Response::Response(std::string message, std::optional httpData) + : message_(std::move(message)), httpData_(httpData) +{ +} + +boost::beast::http::response +Response::intoHttpResponse() && +{ + ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response"); + + boost::beast::http::response result{httpData_->status, httpData_->version}; + result.set(boost::beast::http::field::server, "clio-server-" + util::build::getClioVersionString()); + result.set(boost::beast::http::field::content_type, asString(httpData_->contentType)); + result.keep_alive(httpData_->keepAlive); + result.body() = std::move(message_); + result.prepare_payload(); + return result; +} + +boost::asio::const_buffer +Response::asConstBuffer() const& +{ + ASSERT(not httpData_.has_value(), "Loosing existing http data"); + return boost::asio::buffer(message_.data(), message_.size()); +} + +} // namespace web::ng diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 23cae0e87..e9a58f216 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -19,23 +19,38 @@ #pragma once +#include #include #include #include +#include #include namespace web::ng { class Response { +public: + struct HttpData { + enum class ContentType { APPLICATION_JSON, TEXT_HTML }; + + boost::beast::http::status status; + ContentType contentType; + bool keepAlive; + unsigned int version; + }; + +private: std::string message_; - boost::beast::http::status status_; + std::optional httpData_; public: - Response(std::string message, boost::beast::http::status); - virtual ~Response() = default; + Response(std::string message, std::optional httpData); boost::beast::http::response - toHttpResponse() &&; + intoHttpResponse() &&; + + boost::asio::const_buffer + asConstBuffer() const&; }; } // namespace web::ng diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index ebe989d70..61658ef1d 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -93,12 +93,12 @@ class HttpConnection : public UpgradableConnection { std::optional send( - Response response, + Response const& response, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT ) override { - auto const httpResponse = std::move(response).toHttpResponse(); + auto const httpResponse = response.toHttpResponse(); boost::system::error_code error; boost::beast::get_lowest_layer(stream_).expires_after(timeout); boost::beast::http::async_write(stream_, httpResponse, yield[error]); diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index 38218250e..cf310dce2 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -19,6 +19,7 @@ #pragma once +#include "util/WithTimeout.hpp" #include "util/build/Build.hpp" #include "web/ng/Connection.hpp" #include "web/ng/Error.hpp" @@ -84,10 +85,11 @@ class WsConnection : public Connection { boost::asio::yield_context yield ) { - boost::system::error_code error; + Error error; + // TODO(kuznetsss): save either all headers or just verify admin here stream_.async_accept(request, yield[error]); if (error) - return Error{error}; + return error; return std::nullopt; } @@ -104,13 +106,20 @@ class WsConnection : public Connection { std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT ) override { - return {}; + auto error = util::withTimeout( + [this, &response](auto&& yield) { stream_.async_write(response.asConstBuffer(), yield); }, yield, timeout + ); + if (error) + return std::move(error); + return std::nullopt; } std::expected receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override { - return {}; + auto error = util::withTimeout([this](auto&& yield) { stream_.async_read(buffer_, yield); }, yield, timeout); + if (error) + return std::move(error); } void From 8c15238d49540770bd36247727c28d5fd06c1901 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 25 Sep 2024 17:00:53 +0100 Subject: [PATCH 17/81] Finish implementing WsConnection --- src/web/ng/Request.hpp | 22 +++++++++++++++++++++- src/web/ng/impl/HttpConnection.hpp | 4 ++-- src/web/ng/impl/WsConnection.hpp | 24 ++++++++++++++++-------- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 831c2de66..f1c493dfe 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -19,18 +19,33 @@ #pragma once +#include #include #include +#include +#include #include +#include +#include namespace web::ng { class Request { - boost::beast::http::request request_; +public: + using HttpHeaders = boost::beast::http::request::header_type; + +private: + struct WsData { + std::string request; + std::reference_wrapper headers_; + }; + + std::variant, WsData> data_; public: explicit Request(boost::beast::http::request request); + Request(std::string request, HttpHeaders const& headers); enum class HttpMethod { GET, POST, WEBSOCKET, UNSUPPORTED }; @@ -39,6 +54,11 @@ class Request { std::string const& target() const; + + std::optional + headerValue(boost::beast::http::field headerName) const; + std::optional + headerValue(std::string const& headerName) const; }; } // namespace web::ng diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index 61658ef1d..c5dce94f0 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -93,12 +93,12 @@ class HttpConnection : public UpgradableConnection { std::optional send( - Response const& response, + Response response, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT ) override { - auto const httpResponse = response.toHttpResponse(); + auto const httpResponse = std::move(response).intoHttpResponse(); boost::system::error_code error; boost::beast::get_lowest_layer(stream_).expires_after(timeout); boost::beast::http::async_write(stream_, httpResponse, yield[error]); diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index cf310dce2..fb1bf1bc8 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -52,6 +52,7 @@ namespace web::ng::impl { template class WsConnection : public Connection { boost::beast::websocket::stream stream_; + boost::beast::http::request initialRequest_; public: WsConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer) @@ -64,10 +65,13 @@ class WsConnection : public Connection { boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, - boost::asio::ssl::context& sslContext + boost::asio::ssl::context& sslContext, + boost::beast::http::request initialRequest ) requires IsSslTcpStream - : Connection(std::move(ip), std::move(buffer)), stream_(std::move(socket), sslContext) + : Connection(std::move(ip), std::move(buffer)) + , stream_(std::move(socket), sslContext) + , initialRequest_(std::move(initialRequest)) { // Disable the timeout. The websocket::stream uses its own timeout settings. boost::beast::get_lowest_layer(stream_).expires_never(); @@ -80,14 +84,10 @@ class WsConnection : public Connection { } std::optional - accept( - boost::beast::http::request const& request, - boost::asio::yield_context yield - ) + performHandshake(boost::asio::yield_context yield) { Error error; - // TODO(kuznetsss): save either all headers or just verify admin here - stream_.async_accept(request, yield[error]); + stream_.async_accept(initialRequest_, yield[error]); if (error) return error; return std::nullopt; @@ -120,11 +120,19 @@ class WsConnection : public Connection { auto error = util::withTimeout([this](auto&& yield) { stream_.async_read(buffer_, yield); }, yield, timeout); if (error) return std::move(error); + + return Request{std::string{buffer_.data(), buffer_.size()}, initialRequest_}; } void close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override { + boost::beast::websocket::stream_base::timeout wsTimeout{}; + stream_.get_option(wsTimeout); + wsTimeout.handshake_timeout = timeout; + stream_.set_option(wsTimeout); + + stream_.async_close(yield); } }; From a43337d3d16d5fe5f738dee67f0c86b6c7fe363d Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 26 Sep 2024 14:21:22 +0100 Subject: [PATCH 18/81] Add tags to connections --- src/util/Taggable.hpp | 3 +- src/web/ng/Connection.cpp | 10 ++++- src/web/ng/Connection.hpp | 4 +- src/web/ng/Server.cpp | 72 +++++++++++------------------- src/web/ng/Server.hpp | 6 ++- src/web/ng/impl/HttpConnection.hpp | 37 +++++++++++---- src/web/ng/impl/WsConnection.hpp | 17 +++++-- 7 files changed, 85 insertions(+), 64 deletions(-) diff --git a/src/util/Taggable.hpp b/src/util/Taggable.hpp index 71ce59ba3..0a9c642ca 100644 --- a/src/util/Taggable.hpp +++ b/src/util/Taggable.hpp @@ -241,7 +241,7 @@ class Taggable { using DecoratorType = std::unique_ptr; DecoratorType tagDecorator_; -protected: +public: /** * @brief New Taggable from a specified factory. * @@ -251,7 +251,6 @@ class Taggable { { } -public: virtual ~Taggable() = default; Taggable(Taggable&&) = default; diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index 233ff4efb..d97d64978 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -19,6 +19,8 @@ #include "web/ng/Connection.hpp" +#include "util/Taggable.hpp" + #include #include @@ -39,8 +41,12 @@ generateId() } // namespace -Connection::Connection(std::string ip, boost::beast::flat_buffer buffer) - : id_{generateId()}, ip_{std::move(ip)}, buffer_{std::move(buffer)} +Connection::Connection( + std::string ip, + boost::beast::flat_buffer buffer, + util::TagDecoratorFactory const& tagDecoratorFactory +) + : id_{generateId()}, ip_{std::move(ip)}, buffer_{std::move(buffer)}, taggable_(tagDecoratorFactory) { } diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 314e2fd50..4d6209f87 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -19,6 +19,7 @@ #pragma once +#include "util/Taggable.hpp" #include "web/ng/Error.hpp" #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" @@ -43,11 +44,12 @@ class Connection { size_t id_; std::string ip_; // client ip boost::beast::flat_buffer buffer_; + util::Taggable taggable_; public: static constexpr std::chrono::steady_clock::duration DEFAULT_TIMEOUT = std::chrono::seconds{30}; - Connection(std::string ip, boost::beast::flat_buffer buffer); + Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory); virtual ~Connection() = default; diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 3b1c56afe..a54af336c 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -21,6 +21,7 @@ #include "util/Assert.hpp" #include "util/Mutex.hpp" +#include "util/Taggable.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" @@ -131,6 +132,7 @@ makeConnection( SslDetectionResult sslDetectionResult, std::optional& sslContext, std::string ip, + util::TagDecoratorFactory& tagDecoratorFactory, boost::asio::yield_context yield ) { @@ -140,11 +142,18 @@ makeConnection( return std::unexpected{"SSL is not supported by this server"}; connection = std::make_unique( - std::move(sslDetectionResult.socket), std::move(ip), std::move(sslDetectionResult.buffer), *sslContext + std::move(sslDetectionResult.socket), + std::move(ip), + std::move(sslDetectionResult.buffer), + *sslContext, + tagDecoratorFactory ); } else { connection = std::make_unique( - std::move(sslDetectionResult.socket), std::move(ip), std::move(sslDetectionResult.buffer) + std::move(sslDetectionResult.socket), + std::move(ip), + std::move(sslDetectionResult.buffer), + tagDecoratorFactory ); } @@ -165,7 +174,8 @@ Server::Server( boost::asio::ip::tcp::endpoint endpoint, std::optional sslContext, std::shared_ptr adminVerificationStrategy, - std::unique_ptr dosguard + std::unique_ptr dosguard, + util::TagDecoratorFactory tagDecoratorFactory ) : ctx_{ctx} , dosguard_{std::move(dosguard)} @@ -173,6 +183,7 @@ Server::Server( , sslContext_{std::move(sslContext)} , connections_{std::make_unique>()} , endpoint_{std::move(endpoint)} + , tagDecoratorFactory_{tagDecoratorFactory} { } @@ -252,8 +263,9 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield // TODO(kuznetsss): check ip with dosguard here - auto connectionExpected = - makeConnection(std::move(sslDetectionResult).value(), sslContext_, std::move(ip).value(), yield); + auto connectionExpected = makeConnection( + std::move(sslDetectionResult).value(), sslContext_, std::move(ip).value(), tagDecoratorFactory_, yield + ); if (not connectionExpected.has_value()) { LOG(log_.info()) << "Error creating a connection: " << connectionExpected.error(); return; @@ -262,54 +274,22 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield Connection& connection = insertConnection(std::move(connectionExpected).value()); boost::asio::spawn(ctx_, [this, &connection = connection](boost::asio::yield_context yield) { - if (connection.wasUpgraded()) { - processConnectionLoop(connection, yield); - } else { - processConnection(connection, yield); - } + processConnection(connection, yield); }); } void Server::processConnection(Connection& connection, boost::asio::yield_context yield) { - // auto expectedRequest = connection.receive(yield); - // if (not expectedRequest.has_value()) { + // while (true) { + // auto expectedRequest = connection.receive(yield); + // if (not expectedRequest) { + // LOG(log_.info()) + // } + // // } - // auto response = handleRequest(std::move(expectedRequest).value()); - // connection.send(std::move(response), yield); -} - -void -Server::processConnectionLoop(Connection& connection, boost::asio::yield_context yield) -{ - // loop of handleConnection calls } -// Response -// Server::handleRequest(Request request, ConnectionContext connectionContext) -// { -// auto process = [&connectionContext](Request request, auto& handlersMap) { -// auto const it = handlersMap.find(request.target()); -// if (it == handlersMap.end()) { -// return Response{}; -// } -// return it->second(std::move(request), connectionContext); -// }; -// switch (request.httpMethod()) { -// case Request::HttpMethod::GET: -// return process(std::move(request), getHandlers_); -// case Request::HttpMethod::POST: -// return process(std::move(request), postHandlers_); -// case Request::HttpMethod::WS: -// if (wsHandler_) { -// return (*wsHandler_)(std::move(request)); -// } -// default: -// return Response{}; -// } -// } - Connection& Server::insertConnection(ConnectionPtr connection) { @@ -347,8 +327,8 @@ make_Server( std::move(endpoint).value(), std::move(expectedSslContext).value(), std::move(adminVerificationStrategy).value(), - std::move(dosguard) - + std::move(dosguard), + util::TagDecoratorFactory(config) }; } diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 307cb1462..10ed0fb6a 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -20,6 +20,7 @@ #pragma once #include "util/Mutex.hpp" +#include "util/Taggable.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" @@ -61,6 +62,8 @@ class Server { boost::asio::ip::tcp::endpoint endpoint_; + util::TagDecoratorFactory tagDecoratorFactory_; + bool running_{false}; public: @@ -69,7 +72,8 @@ class Server { boost::asio::ip::tcp::endpoint endpoint, std::optional sslContext, std::shared_ptr adminVerificationStrategy, - std::unique_ptr dosguard + std::unique_ptr dosguard, + util::TagDecoratorFactory tagDecoratorFactory ); Server(Server const&) = delete; diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index c5dce94f0..c3e59a4f7 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -20,6 +20,7 @@ #pragma once #include "util/Assert.hpp" +#include "util/Taggable.hpp" #include "web/ng/Connection.hpp" #include "web/ng/Error.hpp" #include "web/ng/Request.hpp" @@ -57,7 +58,11 @@ class UpgradableConnection : public Connection { const = 0; virtual std::expected - upgrade(std::optional& sslContext, boost::asio::yield_context yield) = 0; + upgrade( + std::optional& sslContext, + util::TagDecoratorFactory const& tagDecoratorFactory, + boost::asio::yield_context yield + ) = 0; }; using UpgradableConnectionPtr = std::unique_ptr; @@ -68,9 +73,14 @@ class HttpConnection : public UpgradableConnection { std::optional> request_; public: - HttpConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer) + HttpConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + util::TagDecoratorFactory const& tagDecoratorFactory + ) requires IsTcpStream - : UpgradableConnection(std::move(ip), std::move(buffer)), stream_{std::move(socket)} + : UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory), stream_{std::move(socket)} { } @@ -78,10 +88,12 @@ class HttpConnection : public UpgradableConnection { boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, - boost::asio::ssl::context& sslCtx + boost::asio::ssl::context& sslCtx, + util::TagDecoratorFactory const& tagDecoratorFactory ) requires IsSslTcpStream - : UpgradableConnection(std::move(ip), std::move(buffer)), stream_{std::move(socket), sslCtx} + : UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory) + , stream_{std::move(socket), sslCtx} { } @@ -143,8 +155,11 @@ class HttpConnection : public UpgradableConnection { } std::expected - upgrade([[maybe_unused]] std::optional& sslContext, boost::asio::yield_context yield) - override + upgrade( + [[maybe_unused]] std::optional& sslContext, + util::TagDecoratorFactory const& tagDecoratorFactory, + boost::asio::yield_context yield + ) override { ASSERT(request_.has_value(), "Request must be present to upgrade the connection"); @@ -156,12 +171,18 @@ class HttpConnection : public UpgradableConnection { std::move(buffer_), request_, sslContext.value(), + tagDecoratorFactory, yield ); } return make_PlainWsConnection( - stream_.release_socket(), std::move(ip_), std::move(buffer_), std::move(request_).value(), yield + stream_.release_socket(), + std::move(ip_), + std::move(buffer_), + std::move(request_).value(), + tagDecoratorFactory, + yield ); } diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index fb1bf1bc8..34cbe66fc 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -19,6 +19,7 @@ #pragma once +#include "util/Taggable.hpp" #include "util/WithTimeout.hpp" #include "util/build/Build.hpp" #include "web/ng/Connection.hpp" @@ -55,9 +56,14 @@ class WsConnection : public Connection { boost::beast::http::request initialRequest_; public: - WsConnection(boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer) + WsConnection( + boost::asio::ip::tcp::socket socket, + std::string ip, + boost::beast::flat_buffer buffer, + util::TagDecoratorFactory const& tagDecoratorFactory + ) requires IsTcpStream - : Connection(std::move(ip), std::move(buffer)), stream_(std::move(socket)) + : Connection(std::move(ip), std::move(buffer), tagDecoratorFactory), stream_(std::move(socket)) { } @@ -66,10 +72,11 @@ class WsConnection : public Connection { std::string ip, boost::beast::flat_buffer buffer, boost::asio::ssl::context& sslContext, - boost::beast::http::request initialRequest + boost::beast::http::request initialRequest, + util::TagDecoratorFactory const& tagDecoratorFactory ) requires IsSslTcpStream - : Connection(std::move(ip), std::move(buffer)) + : Connection(std::move(ip), std::move(buffer), tagDecoratorFactory) , stream_(std::move(socket), sslContext) , initialRequest_(std::move(initialRequest)) { @@ -145,6 +152,7 @@ make_PlainWsConnection( std::string ip, boost::beast::flat_buffer buffer, boost::beast::http::request const& request, + util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield ); @@ -155,6 +163,7 @@ make_SslWsConnection( boost::beast::flat_buffer buffer, boost::beast::http::request const& request, boost::asio::ssl::context& sslContext, + util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield ); From e60273383b02c2d201516d5876baa14ec30827f5 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 27 Sep 2024 17:50:40 +0100 Subject: [PATCH 19/81] WIP: Implementing ConnectionHandler --- src/util/Taggable.hpp | 3 +- src/web/CMakeLists.txt | 1 + src/web/ng/Connection.cpp | 8 +- src/web/ng/Connection.hpp | 8 +- src/web/ng/Request.hpp | 1 + src/web/ng/Response.hpp | 7 +- src/web/ng/Server.cpp | 46 ++----- src/web/ng/Server.hpp | 24 +--- src/web/ng/impl/ConnectionHandler.cpp | 183 ++++++++++++++++++++++++++ src/web/ng/impl/ConnectionHandler.hpp | 114 ++++++++++++++++ 10 files changed, 331 insertions(+), 64 deletions(-) create mode 100644 src/web/ng/impl/ConnectionHandler.cpp create mode 100644 src/web/ng/impl/ConnectionHandler.hpp diff --git a/src/util/Taggable.hpp b/src/util/Taggable.hpp index 0a9c642ca..71ce59ba3 100644 --- a/src/util/Taggable.hpp +++ b/src/util/Taggable.hpp @@ -241,7 +241,7 @@ class Taggable { using DecoratorType = std::unique_ptr; DecoratorType tagDecorator_; -public: +protected: /** * @brief New Taggable from a specified factory. * @@ -251,6 +251,7 @@ class Taggable { { } +public: virtual ~Taggable() = default; Taggable(Taggable&&) = default; diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index ebca93a67..b617b681a 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources( impl/AdminVerificationStrategy.cpp impl/ServerSslContext.cpp ng/Connection.cpp + ng/impl/ConnectionHandler.cpp ng/impl/ServerSslContext.cpp ng/impl/WsConnection.cpp ng/Server.cpp diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index d97d64978..d9a32cec3 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -46,8 +46,14 @@ Connection::Connection( boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory ) - : id_{generateId()}, ip_{std::move(ip)}, buffer_{std::move(buffer)}, taggable_(tagDecoratorFactory) + : util::Taggable(tagDecoratorFactory), id_{generateId()}, ip_{std::move(ip)}, buffer_{std::move(buffer)} { } +std::string const& +Connection::ip() const +{ + return ip_; +} + } // namespace web::ng diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 4d6209f87..e23ed232e 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -39,20 +39,17 @@ namespace web::ng { class ConnectionContext; -class Connection { +class Connection : public util::Taggable { protected: size_t id_; std::string ip_; // client ip boost::beast::flat_buffer buffer_; - util::Taggable taggable_; public: static constexpr std::chrono::steady_clock::duration DEFAULT_TIMEOUT = std::chrono::seconds{30}; Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory); - virtual ~Connection() = default; - virtual bool wasUpgraded() const = 0; @@ -77,6 +74,9 @@ class Connection { size_t id() const; + + std::string const& + ip() const; }; using ConnectionPtr = std::unique_ptr; diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index f1c493dfe..2a27e6d69 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -57,6 +57,7 @@ class Request { std::optional headerValue(boost::beast::http::field headerName) const; + std::optional headerValue(std::string const& headerName) const; }; diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index e9a58f216..6458b4b73 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -19,6 +19,8 @@ #pragma once +#include "web/ng/Request.hpp" + #include #include #include @@ -44,7 +46,10 @@ class Response { std::optional httpData_; public: - Response(std::string message, std::optional httpData); + // WebSocket response + explicit Response(std::string message); + + Response(boost::beast::http::status status, std::string message, Request&& request); boost::beast::http::response intoHttpResponse() &&; diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index a54af336c..1ca20421a 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -20,7 +20,6 @@ #include "web/ng/Server.hpp" #include "util/Assert.hpp" -#include "util/Mutex.hpp" #include "util/Taggable.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" @@ -40,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -52,9 +50,7 @@ #include #include #include -#include #include -#include #include #include @@ -158,7 +154,7 @@ makeConnection( } if (connection->isUpgradeRequested(yield)) { - return connection->upgrade(sslContext, yield) + return connection->upgrade(sslContext, tagDecoratorFactory, yield) .or_else([](Error error) -> std::expected { return std::unexpected{fmt::format("Error upgrading connection: {}", error.what())}; }); @@ -181,7 +177,6 @@ Server::Server( , dosguard_{std::move(dosguard)} , adminVerificationStrategy_(std::move(adminVerificationStrategy)) , sslContext_{std::move(sslContext)} - , connections_{std::make_unique>()} , endpoint_{std::move(endpoint)} , tagDecoratorFactory_{tagDecoratorFactory} { @@ -191,21 +186,21 @@ void Server::onGet(std::string const& target, MessageHandler handler) { ASSERT(not running_, "Adding a GET handler is not allowed when Server is running."); - getHandlers_[target] = std::move(handler); + connectionHandler_.onGet(target, std::move(handler)); } void Server::onPost(std::string const& target, MessageHandler handler) { ASSERT(not running_, "Adding a POST handler is not allowed when Server is running."); - postHandlers_[target] = std::move(handler); + connectionHandler_.onPost(target, std::move(handler)); } void Server::onWs(MessageHandler handler) { ASSERT(not running_, "Adding a Websocket handler is not allowed when Server is running."); - wsHandler_ = std::move(handler); + connectionHandler_.onWs(std::move(handler)); } std::optional @@ -271,33 +266,12 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield return; } - Connection& connection = insertConnection(std::move(connectionExpected).value()); - - boost::asio::spawn(ctx_, [this, &connection = connection](boost::asio::yield_context yield) { - processConnection(connection, yield); - }); -} - -void -Server::processConnection(Connection& connection, boost::asio::yield_context yield) -{ - // while (true) { - // auto expectedRequest = connection.receive(yield); - // if (not expectedRequest) { - // LOG(log_.info()) - // } - // - // } -} - -Connection& -Server::insertConnection(ConnectionPtr connection) -{ - auto const connectionId = connection->id(); - auto connectionsMap = connections_->lock(); - auto [it, inserted] = connectionsMap->emplace(connectionId, std::move(connection)); - ASSERT(inserted, "Connection with id {} already exists", it->second->id()); - return *it->second.get(); + boost::asio::spawn( + ctx_, + [this, connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) mutable { + connectionHandler_.processConnection(std::move(connection), yield); + } + ); } std::expected diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 10ed0fb6a..f7bce7698 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -19,7 +19,6 @@ #pragma once -#include "util/Mutex.hpp" #include "util/Taggable.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" @@ -29,6 +28,7 @@ #include "web/ng/MessageHandler.hpp" #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" +#include "web/ng/impl/ConnectionHandler.hpp" #include #include @@ -39,26 +39,20 @@ #include #include #include -#include #include -#include namespace web::ng { class Server { util::Logger log_{"WebServer"}; + util::Logger perfLog_{"Performance"}; std::reference_wrapper ctx_; std::unique_ptr dosguard_; std::shared_ptr adminVerificationStrategy_; std::optional sslContext_; - std::unordered_map getHandlers_; - std::unordered_map postHandlers_; - std::optional wsHandler_; - - using ConnectionsMap = std::unordered_map; - std::unique_ptr> connections_; + impl::ConnectionHandler connectionHandler_; boost::asio::ip::tcp::endpoint endpoint_; @@ -97,18 +91,6 @@ class Server { private: void handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); - - void - processConnection(Connection& connection, boost::asio::yield_context yield); - - void - processConnectionLoop(Connection& connection, boost::asio::yield_context yield); - - Response - handleRequest(Request request, ConnectionContext connectionContext); - - Connection& - insertConnection(ConnectionPtr connection); }; std::expected diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp new file mode 100644 index 000000000..30a77ef5b --- /dev/null +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -0,0 +1,183 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/impl/ConnectionHandler.hpp" + +#include "util/Assert.hpp" +#include "util/log/Logger.hpp" +#include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" +#include "web/ng/MessageHandler.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace web::ng::impl { + +void +ConnectionHandler::onGet(std::string const& target, MessageHandler handler) +{ + getHandlers_[target] = std::move(handler); +} + +void +ConnectionHandler::onPost(std::string const& target, MessageHandler handler) +{ + postHandlers_[target] = std::move(handler); +} + +void +ConnectionHandler::onWs(MessageHandler handler) +{ + wsHandler_ = std::move(handler); +} + +void +ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::yield_context yield) +{ + auto& connection = insertConnection(std::move(connectionPtr)); + + auto const shouldCloseGracefully = requestResponseLoop(connection, yield); + if (shouldCloseGracefully) { + connection.close(yield); + } + + removeConnection(connection); + // connection reference is not valid anymore +} + +Connection& +ConnectionHandler::insertConnection(ConnectionPtr connection) +{ + auto connectionsMap = connections_->lock(); + auto [it, inserted] = connectionsMap->emplace(connection->id(), std::move(connection)); + ASSERT(inserted, "Connection with id {} already exists", it->second->id()); + return *it->second; +} + +void +ConnectionHandler::removeConnection(Connection const& connection) +{ + auto connectionsMap = connections_->lock(); + auto it = connectionsMap->find(connection.id()); + ASSERT(it != connectionsMap->end(), "Connection with id {} does not exist", connection.id()); + connectionsMap->erase(it); +} + +bool +ConnectionHandler::handleError(Error const& error, Connection const& connection) const +{ + // 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 boost::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 (error == boost::beast::http::error::end_of_stream || error == boost::asio::ssl::error::stream_truncated) + return false; + + // WebSocket connection was gracefully closed + if (error == boost::beast::websocket::error::closed) + return false; + + if (error != boost::asio::error::operation_aborted) { + LOG(log_.error()) << connection.tag() << ": " << error.message() << ": " << error.value(); + } + return true; +} + +bool +ConnectionHandler::requestResponseLoop(Connection& connection, boost::asio::yield_context yield) +{ + // The loop here is infinite because: + // - For websocket connection is persistent so Clio will try to read and respond infinite unless client + // disconnected. + // - When client disconnected connection.send() or connection.receive() will return an error. + // - For http it is still a loop to reuse the connection if keep alive is set. Otherwise client will disconnect and + // an error appears. + // - When server is shutting down it will cancel all operations on the connection so an error appears. + + while (true) { + auto expectedRequest = connection.receive(yield); + if (not expectedRequest) { + return handleError(expectedRequest.error(), connection); + } + + LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip(); + + auto response = handleRequest(std::move(expectedRequest).value(), yield); + + auto const maybeError = connection.send(std::move(response), yield); + if (maybeError.has_value()) { + return handleError(maybeError.value(), connection); + } + } +} + +Response +handleHttpRequest( + Connection const& connection, + std::unordered_map const& handlers, + Request request, + boost::asio::yield_context yield +) +{ + auto it = handlers.find(request.target()); + if (it == handlers.end()) { + return Response{boost::beast::http::status::bad_request, "Bad target", request}; + } + it->second(std::move(request), ) +} + +Response +ConnectionHandler::handleRequest(Request request, boost::asio::yield_context yield) +{ + switch (request.httpMethod()) { + case Request::HttpMethod::GET: + return handleHttpRequest(getHandlers_, std::move(request), yield); + case Request::HttpMethod::POST: + return handleHttpRequest(postHandlers_, std::move(request), yield); + case Request::HttpMethod::WEBSOCKET: + return handleWsRequest(std::move(request), yield); + default: + return Response{http::status::bad_request, "Unsupported http method", request}; + } +} + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/ConnectionHandler.hpp b/src/web/ng/impl/ConnectionHandler.hpp new file mode 100644 index 000000000..1a67e523d --- /dev/null +++ b/src/web/ng/impl/ConnectionHandler.hpp @@ -0,0 +1,114 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/Mutex.hpp" +#include "util/log/Logger.hpp" +#include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" +#include "web/ng/MessageHandler.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace web::ng::impl { + +class ConnectionHandler { + util::Logger log_{"WebServer"}; + util::Logger perfLog_{"Performance"}; + + std::unordered_map getHandlers_; + std::unordered_map postHandlers_; + std::optional wsHandler_; + + using ConnectionsMap = std::unordered_map; + std::unique_ptr> connections_{std::make_unique>()}; + +public: + void + onGet(std::string const& target, MessageHandler handler); + + void + onPost(std::string const& target, MessageHandler handler); + + void + onWs(MessageHandler handler); + + void + processConnection(ConnectionPtr connection, boost::asio::yield_context yield); + +private: + /** + * @brief Insert a connection into the connections map. + * + * @param connection The connection to insert. + * @return A reference to the inserted connection + */ + Connection& + insertConnection(ConnectionPtr connection); + + /** + * @brief Remove a connection from the connections map. + * @note After this call, the connection reference is no longer valid. + * + * @param connection The connection to remove. + */ + void + removeConnection(Connection const& connection); + + /** + * @brief Handle an error. + * + * @param error The error to handle. + * @param connection The connection that caused the error. + * @return True if the connection should be gracefully closed, false otherwise. + */ + bool + handleError(Error const& error, Connection const& connection) const; + + /** + * @brief The request-response loop. + * + * @param connection The connection to handle. + * @param yield The yield context. + * @return True if the connection should be gracefully closed, false otherwise. + */ + bool + requestResponseLoop(Connection& connection, boost::asio::yield_context yield); + + /** + * @brief Handle a request. + * + * @param request The request to handle. + * @param yield The yield context. + * @return The response to send. + */ + Response + handleRequest(Request request, boost::asio::yield_context yield); +}; + +} // namespace web::ng::impl From f924bc1ef333ce55b42aa356d484abbda6e2f5e2 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 30 Sep 2024 15:46:17 +0100 Subject: [PATCH 20/81] Implemented message handling --- src/web/ng/MessageHandler.hpp | 4 +- src/web/ng/Request.hpp | 9 ++-- src/web/ng/Response.hpp | 5 +- src/web/ng/impl/ConnectionHandler.cpp | 69 ++++++++++++++++++--------- src/web/ng/impl/ConnectionHandler.hpp | 3 +- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/web/ng/MessageHandler.hpp b/src/web/ng/MessageHandler.hpp index a958b7d99..7ecba4d44 100644 --- a/src/web/ng/MessageHandler.hpp +++ b/src/web/ng/MessageHandler.hpp @@ -23,10 +23,12 @@ #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" +#include + #include namespace web::ng { -using MessageHandler = std::function; +using MessageHandler = std::function; } // namespace web::ng diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 2a27e6d69..508fa2803 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -47,10 +47,13 @@ class Request { explicit Request(boost::beast::http::request request); Request(std::string request, HttpHeaders const& headers); - enum class HttpMethod { GET, POST, WEBSOCKET, UNSUPPORTED }; + enum class Method { GET, POST, WEBSOCKET, UNSUPPORTED }; - HttpMethod - httpMethod() const; + Method + method() const; + + bool + isHttp() const; std::string const& target() const; diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 6458b4b73..0c35a0b74 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -46,10 +46,7 @@ class Response { std::optional httpData_; public: - // WebSocket response - explicit Response(std::string message); - - Response(boost::beast::http::status status, std::string message, Request&& request); + Response(boost::beast::http::status status, std::string message, Request const& request); boost::beast::http::response intoHttpResponse() &&; diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index 30a77ef5b..3c4d81577 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -34,12 +34,46 @@ #include #include +#include #include #include #include namespace web::ng::impl { +namespace { + +Response +handleHttpRequest( + ConnectionContext const& connectionContext, + std::unordered_map const& handlers, + Request const& request, + boost::asio::yield_context yield +) +{ + auto it = handlers.find(request.target()); + if (it == handlers.end()) { + return Response{boost::beast::http::status::bad_request, "Bad target", request}; + } + return it->second(request, connectionContext, yield); +} + +Response +handleWsRequest( + ConnectionContext connectionContext, + std::optional const& handler, + Request const& request, + boost::asio::yield_context yield +) +{ + if (not handler.has_value()) { + return Response{boost::beast::http::status::bad_request, "WebSocket is not supported by this server", request}; + } + return handler->operator()(request, connectionContext, yield); +} + +} // namespace + void ConnectionHandler::onGet(std::string const& target, MessageHandler handler) { @@ -141,7 +175,7 @@ ConnectionHandler::requestResponseLoop(Connection& connection, boost::asio::yiel LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip(); - auto response = handleRequest(std::move(expectedRequest).value(), yield); + auto response = handleRequest(connection.context(), expectedRequest.value(), yield); auto const maybeError = connection.send(std::move(response), yield); if (maybeError.has_value()) { @@ -151,32 +185,21 @@ ConnectionHandler::requestResponseLoop(Connection& connection, boost::asio::yiel } Response -handleHttpRequest( - Connection const& connection, - std::unordered_map const& handlers, - Request request, +ConnectionHandler::handleRequest( + ConnectionContext const& connectionContext, + Request const& request, boost::asio::yield_context yield ) { - auto it = handlers.find(request.target()); - if (it == handlers.end()) { - return Response{boost::beast::http::status::bad_request, "Bad target", request}; - } - it->second(std::move(request), ) -} - -Response -ConnectionHandler::handleRequest(Request request, boost::asio::yield_context yield) -{ - switch (request.httpMethod()) { - case Request::HttpMethod::GET: - return handleHttpRequest(getHandlers_, std::move(request), yield); - case Request::HttpMethod::POST: - return handleHttpRequest(postHandlers_, std::move(request), yield); - case Request::HttpMethod::WEBSOCKET: - return handleWsRequest(std::move(request), yield); + switch (request.method()) { + case Request::Method::GET: + return handleHttpRequest(connectionContext, getHandlers_, request, yield); + case Request::Method::POST: + return handleHttpRequest(connectionContext, postHandlers_, request, yield); + case Request::Method::WEBSOCKET: + return handleWsRequest(connectionContext, wsHandler_, request, yield); default: - return Response{http::status::bad_request, "Unsupported http method", request}; + return Response{boost::beast::http::status::bad_request, "Unsupported http method", request}; } } diff --git a/src/web/ng/impl/ConnectionHandler.hpp b/src/web/ng/impl/ConnectionHandler.hpp index 1a67e523d..9af99ebd2 100644 --- a/src/web/ng/impl/ConnectionHandler.hpp +++ b/src/web/ng/impl/ConnectionHandler.hpp @@ -103,12 +103,13 @@ class ConnectionHandler { /** * @brief Handle a request. * + * @param connectionContext The connection context. * @param request The request to handle. * @param yield The yield context. * @return The response to send. */ Response - handleRequest(Request request, boost::asio::yield_context yield); + handleRequest(ConnectionContext const& connectionContext, Request const& request, boost::asio::yield_context yield); }; } // namespace web::ng::impl From dd8a4738a9156968e90d549e2def848842970d04 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 30 Sep 2024 17:30:59 +0100 Subject: [PATCH 21/81] Add multiple processing strategies --- docs/examples/config/example-config.json | 9 ++- src/web/ng/Server.cpp | 16 +++++ src/web/ng/Server.hpp | 4 +- src/web/ng/impl/ConnectionHandler.cpp | 86 +++++++++++++++++++++--- src/web/ng/impl/ConnectionHandler.hpp | 17 ++++- 5 files changed, 117 insertions(+), 15 deletions(-) diff --git a/docs/examples/config/example-config.json b/docs/examples/config/example-config.json index 9ef291150..e73e8a174 100644 --- a/docs/examples/config/example-config.json +++ b/docs/examples/config/example-config.json @@ -67,7 +67,14 @@ "admin_password": "xrp", // If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests // It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time - "local_admin": false + "local_admin": false, + "processing_strategy": "parallel", // Could be "sequent" or "parallel". + // For sequent strategy request from one client connection will be processed one by one and the next one will not be read before + // the previous one is processed. For parallel strategy Clio will take all requests and process them in parallel and + // send a reply for each request whenever it is ready. + "parallel_requests_limit": 10 // Optional parameter, used only if "processing_strategy" is "parallel". + It limits the number of requests for one client connection processed in parallel. Infinite if not specified. + }, // Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet. "graceful_period": 10.0, diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 1ca20421a..840b0265e 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -169,6 +169,7 @@ Server::Server( boost::asio::io_context& ctx, boost::asio::ip::tcp::endpoint endpoint, std::optional sslContext, + impl::ConnectionHandler connectionHandler, std::shared_ptr adminVerificationStrategy, std::unique_ptr dosguard, util::TagDecoratorFactory tagDecoratorFactory @@ -177,6 +178,7 @@ Server::Server( , dosguard_{std::move(dosguard)} , adminVerificationStrategy_(std::move(adminVerificationStrategy)) , sslContext_{std::move(sslContext)} + , connectionHandler_{std::move(connectionHandler)} , endpoint_{std::move(endpoint)} , tagDecoratorFactory_{tagDecoratorFactory} { @@ -296,10 +298,24 @@ make_Server( return std::unexpected{std::move(adminVerificationStrategy).error()}; } + impl::ConnectionHandler::ProcessingStrategy processingStrategy{impl::ConnectionHandler::ProcessingStrategy::Parallel + }; + std::optional parallelRequestLimit; + + auto const processingStrategyStr = serverConfig.valueOr("processing_strategy", "parallel"); + if (processingStrategyStr == "sequent") { + processingStrategy = impl::ConnectionHandler::ProcessingStrategy::Sequent; + } else if (processingStrategyStr == "parallel") { + parallelRequestLimit = serverConfig.maybeValue("parallel_requests_limit"); + } else { + return std::unexpected{fmt::format("Invalid 'server.processing_strategy': {}", processingStrategyStr)}; + } + return Server{ context, std::move(endpoint).value(), std::move(expectedSslContext).value(), + impl::ConnectionHandler{processingStrategy, parallelRequestLimit}, std::move(adminVerificationStrategy).value(), std::move(dosguard), util::TagDecoratorFactory(config) diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index f7bce7698..5733aed0d 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -24,10 +24,7 @@ #include "util/log/Logger.hpp" #include "web/dosguard/DOSGuardInterface.hpp" #include "web/impl/AdminVerificationStrategy.hpp" -#include "web/ng/Connection.hpp" #include "web/ng/MessageHandler.hpp" -#include "web/ng/Request.hpp" -#include "web/ng/Response.hpp" #include "web/ng/impl/ConnectionHandler.hpp" #include @@ -65,6 +62,7 @@ class Server { boost::asio::io_context& ctx, boost::asio::ip::tcp::endpoint endpoint, std::optional sslContext, + impl::ConnectionHandler connectionHandler, std::shared_ptr adminVerificationStrategy, std::unique_ptr dosguard, util::TagDecoratorFactory tagDecoratorFactory diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index 3c4d81577..d24fb1b84 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -30,10 +30,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -97,10 +99,18 @@ ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::y { auto& connection = insertConnection(std::move(connectionPtr)); - auto const shouldCloseGracefully = requestResponseLoop(connection, yield); - if (shouldCloseGracefully) { - connection.close(yield); + bool shouldCloseGracefully{false}; + + switch (processingStrategy_) { + case ProcessingStrategy::Sequent: + shouldCloseGracefully = sequentRequestResponseLoop(connection, yield); + break; + case ProcessingStrategy::Parallel: + shouldCloseGracefully = parallelRequestResponseLoop(connection, yield); + break; } + if (shouldCloseGracefully) + connection.close(yield); removeConnection(connection); // connection reference is not valid anymore @@ -157,7 +167,7 @@ ConnectionHandler::handleError(Error const& error, Connection const& connection) } bool -ConnectionHandler::requestResponseLoop(Connection& connection, boost::asio::yield_context yield) +ConnectionHandler::sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield) { // The loop here is infinite because: // - For websocket connection is persistent so Clio will try to read and respond infinite unless client @@ -169,19 +179,75 @@ ConnectionHandler::requestResponseLoop(Connection& connection, boost::asio::yiel while (true) { auto expectedRequest = connection.receive(yield); - if (not expectedRequest) { + if (not expectedRequest) return handleError(expectedRequest.error(), connection); - } LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip(); - auto response = handleRequest(connection.context(), expectedRequest.value(), yield); + auto maybeReturnValue = processRequest(connection, std::move(expectedRequest).value(), yield); + if (maybeReturnValue.has_value()) + return maybeReturnValue.value(); + } +} + +bool +ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield) +{ + // atomic_bool is not needed here because everything happening on coroutine's strand + std::optional closeConnectionGracefully; + size_t ongoingRequestsCounter{0}; - auto const maybeError = connection.send(std::move(response), yield); - if (maybeError.has_value()) { - return handleError(maybeError.value(), connection); + while (not closeConnectionGracefully.has_value()) { + auto expectedRequest = connection.receive(yield); + if (not expectedRequest) + return handleError(expectedRequest.error(), connection); + + ++ongoingRequestsCounter; + if (maxParallelRequests_.has_value() && ongoingRequestsCounter > *maxParallelRequests_) { + connection.send( + Response{ + boost::beast::http::status::too_many_requests, + "Too many request for one session", + expectedRequest.value() + }, + yield + ); + } else { + boost::asio::spawn( + yield, + [this, + &closeConnectionGracefully, + &ongoingRequestsCounter, + &connection, + request = std::move(expectedRequest).value()](boost::asio::yield_context innerYield) mutable { + auto maybeCloseConnectionGracefully = processRequest(connection, std::move(request), innerYield); + if (maybeCloseConnectionGracefully.has_value()) { + if (closeConnectionGracefully.has_value()) { + // Close connection gracefully only if both are true. If at least one is false then + // connection is already closed. + closeConnectionGracefully = *closeConnectionGracefully && *maybeCloseConnectionGracefully; + } else { + closeConnectionGracefully = maybeCloseConnectionGracefully; + } + } + --ongoingRequestsCounter; + } + ); } } + return *closeConnectionGracefully; +} + +std::optional +ConnectionHandler::processRequest(Connection& connection, Request&& request, boost::asio::yield_context yield) +{ + auto response = handleRequest(connection.context(), request, yield); + + auto const maybeError = connection.send(std::move(response), yield); + if (maybeError.has_value()) { + return handleError(maybeError.value(), connection); + } + return std::nullopt; } Response diff --git a/src/web/ng/impl/ConnectionHandler.hpp b/src/web/ng/impl/ConnectionHandler.hpp index 9af99ebd2..cb56178f7 100644 --- a/src/web/ng/impl/ConnectionHandler.hpp +++ b/src/web/ng/impl/ConnectionHandler.hpp @@ -38,9 +38,16 @@ namespace web::ng::impl { class ConnectionHandler { +public: + enum class ProcessingStrategy { Sequent, Parallel }; + +private: util::Logger log_{"WebServer"}; util::Logger perfLog_{"Performance"}; + ProcessingStrategy processingStrategy_; + std::optional maxParallelRequests_; + std::unordered_map getHandlers_; std::unordered_map postHandlers_; std::optional wsHandler_; @@ -49,6 +56,8 @@ class ConnectionHandler { std::unique_ptr> connections_{std::make_unique>()}; public: + ConnectionHandler(ProcessingStrategy processingStrategy, std::optional maxParallelRequests_); + void onGet(std::string const& target, MessageHandler handler); @@ -98,7 +107,13 @@ class ConnectionHandler { * @return True if the connection should be gracefully closed, false otherwise. */ bool - requestResponseLoop(Connection& connection, boost::asio::yield_context yield); + sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield); + + bool + parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield); + + std::optional + processRequest(Connection& connection, Request&& request, boost::asio::yield_context yield); /** * @brief Handle a request. From ac8503a46f76cc707904834f93b7cb2db61e0042 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 30 Sep 2024 17:47:05 +0100 Subject: [PATCH 22/81] Removed DosGuard and AdminVerificationStrategy from Server --- src/web/ng/Server.cpp | 19 +------------------ src/web/ng/Server.hpp | 12 +----------- src/web/ng/impl/ConnectionHandler.cpp | 2 +- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 840b0265e..2c4d10833 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -23,8 +23,6 @@ #include "util/Taggable.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" -#include "web/dosguard/DOSGuardInterface.hpp" -#include "web/impl/AdminVerificationStrategy.hpp" #include "web/ng/Connection.hpp" #include "web/ng/Error.hpp" #include "web/ng/MessageHandler.hpp" @@ -170,13 +168,9 @@ Server::Server( boost::asio::ip::tcp::endpoint endpoint, std::optional sslContext, impl::ConnectionHandler connectionHandler, - std::shared_ptr adminVerificationStrategy, - std::unique_ptr dosguard, util::TagDecoratorFactory tagDecoratorFactory ) : ctx_{ctx} - , dosguard_{std::move(dosguard)} - , adminVerificationStrategy_(std::move(adminVerificationStrategy)) , sslContext_{std::move(sslContext)} , connectionHandler_{std::move(connectionHandler)} , endpoint_{std::move(endpoint)} @@ -277,11 +271,7 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield } std::expected -make_Server( - util::Config const& config, - boost::asio::io_context& context, - std::unique_ptr dosguard -) +make_Server(util::Config const& config, boost::asio::io_context& context) { auto const serverConfig = config.section("server"); @@ -293,11 +283,6 @@ make_Server( if (not expectedSslContext) return std::unexpected{std::move(expectedSslContext).error()}; - auto adminVerificationStrategy = web::impl::make_AdminVerificationStrategy(serverConfig); - if (not adminVerificationStrategy) { - return std::unexpected{std::move(adminVerificationStrategy).error()}; - } - impl::ConnectionHandler::ProcessingStrategy processingStrategy{impl::ConnectionHandler::ProcessingStrategy::Parallel }; std::optional parallelRequestLimit; @@ -316,8 +301,6 @@ make_Server( std::move(endpoint).value(), std::move(expectedSslContext).value(), impl::ConnectionHandler{processingStrategy, parallelRequestLimit}, - std::move(adminVerificationStrategy).value(), - std::move(dosguard), util::TagDecoratorFactory(config) }; } diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 5733aed0d..6b19bf85d 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -22,7 +22,6 @@ #include "util/Taggable.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" -#include "web/dosguard/DOSGuardInterface.hpp" #include "web/impl/AdminVerificationStrategy.hpp" #include "web/ng/MessageHandler.hpp" #include "web/ng/impl/ConnectionHandler.hpp" @@ -34,7 +33,6 @@ #include #include -#include #include #include @@ -45,8 +43,6 @@ class Server { util::Logger perfLog_{"Performance"}; std::reference_wrapper ctx_; - std::unique_ptr dosguard_; - std::shared_ptr adminVerificationStrategy_; std::optional sslContext_; impl::ConnectionHandler connectionHandler_; @@ -63,8 +59,6 @@ class Server { boost::asio::ip::tcp::endpoint endpoint, std::optional sslContext, impl::ConnectionHandler connectionHandler, - std::shared_ptr adminVerificationStrategy, - std::unique_ptr dosguard, util::TagDecoratorFactory tagDecoratorFactory ); @@ -92,10 +86,6 @@ class Server { }; std::expected -make_Server( - util::Config const& config, - boost::asio::io_context& context, - std::unique_ptr dosguard -); +make_Server(util::Config const& config, boost::asio::io_context& context); } // namespace web::ng diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index d24fb1b84..f4193e4e7 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -214,7 +214,7 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as ); } else { boost::asio::spawn( - yield, + yield, // spawn on the same strand [this, &closeConnectionGracefully, &ongoingRequestsCounter, From 4f916f3a465943355ce789099b538da4e506c93c Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 1 Oct 2024 16:14:28 +0100 Subject: [PATCH 23/81] Implemented Request and Response --- src/web/CMakeLists.txt | 1 + src/web/ng/Request.cpp | 123 ++++++++++++++++++++++++++++++++++++++++ src/web/ng/Request.hpp | 14 ++++- src/web/ng/Response.cpp | 28 ++++++++- src/web/ng/Response.hpp | 2 + 5 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 src/web/ng/Request.cpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index b617b681a..e2545d1a6 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources( ng/impl/ServerSslContext.cpp ng/impl/WsConnection.cpp ng/Server.cpp + ng/Request.cpp ng/Response.cpp ) diff --git a/src/web/ng/Request.cpp b/src/web/ng/Request.cpp new file mode 100644 index 000000000..011aab033 --- /dev/null +++ b/src/web/ng/Request.cpp @@ -0,0 +1,123 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/ng/Request.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace web::ng { + +namespace { + +template +std::optional +getHeaderValue(HeadersType const& headers, HeaderNameType const& headerName) +{ + auto it = headers.find(headerName); + if (it == headers.end()) + return std::nullopt; + return it->value(); +} + +} // namespace + +Request::Request(boost::beast::http::request request) : data_{std::move(request)} +{ +} + +Request::Request(std::string request, HttpHeaders const& headers) + : data_{WsData{.request = std::move(request), .headers = headers}} +{ +} + +Request::Method +Request::method() const +{ + if (not isHttp()) + return Method::WEBSOCKET; + + switch (httpRequest().method()) { + case boost::beast::http::verb::get: + return Method::GET; + case boost::beast::http::verb::post: + return Method::POST; + default: + return Method::UNSUPPORTED; + } +} + +bool +Request::isHttp() const +{ + return std::holds_alternative(data_); +} + +std::optional const>> +Request::asHttpRequest() const +{ + if (not isHttp()) + return std::nullopt; + + return httpRequest(); +} + +std::optional +Request::target() const +{ + if (not isHttp()) + return std::nullopt; + + return httpRequest().target(); +} + +std::optional +Request::headerValue(boost::beast::http::field headerName) const +{ + if (not isHttp()) + return getHeaderValue(std::get(data_).headers.get(), headerName); + + return getHeaderValue(httpRequest(), headerName); +} + +std::optional +Request::headerValue(std::string const& headerName) const +{ + if (not isHttp()) + return getHeaderValue(std::get(data_).headers.get(), headerName); + + return getHeaderValue(httpRequest(), headerName); +} + +Request::HttpRequest const& +Request::httpRequest() const +{ + return std::get(data_); +} + +} // namespace web::ng diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 508fa2803..0fa507bcf 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -38,10 +38,11 @@ class Request { private: struct WsData { std::string request; - std::reference_wrapper headers_; + std::reference_wrapper headers; }; - std::variant, WsData> data_; + using HttpRequest = boost::beast::http::request; + std::variant data_; public: explicit Request(boost::beast::http::request request); @@ -55,7 +56,10 @@ class Request { bool isHttp() const; - std::string const& + std::optional const>> + asHttpRequest() const; + + std::optional target() const; std::optional @@ -63,6 +67,10 @@ class Request { std::optional headerValue(std::string const& headerName) const; + +private: + HttpRequest const& + httpRequest() const; }; } // namespace web::ng diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp index 8f4208716..73de2137f 100644 --- a/src/web/ng/Response.cpp +++ b/src/web/ng/Response.cpp @@ -21,12 +21,15 @@ #include "util/Assert.hpp" #include "util/build/Build.hpp" +#include "web/ng/Request.hpp" #include #include #include #include #include +#include +#include #include #include @@ -50,10 +53,31 @@ asString(Response::HttpData::ContentType type) std::unreachable(); } +template +std::optional +makeHttpData(boost::beast::http::status status, Request const& request) +{ + if (request.isHttp()) { + auto const& httpRequest = request.asHttpRequest()->get(); + return Response::HttpData{ + .status = status, + .contentType = std::is_same_v ? Response::HttpData::ContentType::TEXT_HTML + : Response::HttpData::ContentType::APPLICATION_JSON, + .keepAlive = httpRequest.keep_alive(), + .version = httpRequest.version() + }; + } + return std::nullopt; +} } // namespace -Response::Response(std::string message, std::optional httpData) - : message_(std::move(message)), httpData_(httpData) +Response::Response(boost::beast::http::status status, std::string message, Request const& request) + : message_(std::move(message)), httpData_{makeHttpData(status, request)} +{ +} + +Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request) + : message_(boost::json::serialize(message)), httpData_{makeHttpData(status, request)} { } diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 0c35a0b74..86c5db420 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,7 @@ class Response { public: Response(boost::beast::http::status status, std::string message, Request const& request); + Response(boost::beast::http::status status, boost::json::object const& message, Request const& request); boost::beast::http::response intoHttpResponse() &&; From 55416eef8ee108747b7e650f3ceeecf87939456e Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 2 Oct 2024 14:25:51 +0100 Subject: [PATCH 24/81] Fix compilation --- src/util/WithTimeout.hpp | 3 +- src/web/ng/Server.cpp | 39 +++++++++++++------------ src/web/ng/impl/ConnectionHandler.cpp | 25 ++++++++++++++-- src/web/ng/impl/ConnectionHandler.hpp | 20 +++++++++++-- src/web/ng/impl/HttpConnection.hpp | 42 +++++++++++++++++---------- src/web/ng/impl/WsConnection.cpp | 20 ++++++++----- src/web/ng/impl/WsConnection.hpp | 16 ++++++---- 7 files changed, 111 insertions(+), 54 deletions(-) diff --git a/src/util/WithTimeout.hpp b/src/util/WithTimeout.hpp index 725a27576..9d2804774 100644 --- a/src/util/WithTimeout.hpp +++ b/src/util/WithTimeout.hpp @@ -29,7 +29,6 @@ #include #include -#include #include namespace util { @@ -40,7 +39,7 @@ namespace util { @param timeout The timeout duration. @return The error code of the operation. */ -template Operation> +template boost::system::error_code withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) { diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 2c4d10833..91be56a75 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -207,25 +207,28 @@ Server::run() return std::move(acceptor).error(); running_ = true; - boost::asio::spawn(ctx_, [this, acceptor = std::move(acceptor).value()](boost::asio::yield_context yield) mutable { - while (true) { - boost::beast::error_code errorCode; - boost::asio::ip::tcp::socket socket{ctx_.get().get_executor()}; - - acceptor.async_accept(socket, yield[errorCode]); - if (errorCode) { - LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); - continue; + boost::asio::spawn( + ctx_.get(), + [this, acceptor = std::move(acceptor).value()](boost::asio::yield_context yield) mutable { + while (true) { + boost::beast::error_code errorCode; + boost::asio::ip::tcp::socket socket{ctx_.get().get_executor()}; + + acceptor.async_accept(socket, yield[errorCode]); + if (errorCode) { + LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what(); + continue; + } + boost::asio::spawn( + ctx_.get(), + [this, socket = std::move(socket)](boost::asio::yield_context yield) mutable { + handleConnection(std::move(socket), yield); + }, + boost::asio::detached + ); } - boost::asio::spawn( - ctx_.get(), - [this, socket = std::move(socket)](boost::asio::yield_context yield) mutable { - handleConnection(std::move(socket), yield); - }, - boost::asio::detached - ); } - }); + ); return std::nullopt; } @@ -263,7 +266,7 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield } boost::asio::spawn( - ctx_, + ctx_.get(), [this, connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) mutable { connectionHandler_.processConnection(std::move(connection), yield); } diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index f4193e4e7..ee5dba3cc 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include namespace web::ng::impl { @@ -48,12 +48,13 @@ namespace { Response handleHttpRequest( ConnectionContext const& connectionContext, - std::unordered_map const& handlers, + ConnectionHandler::TargetToHandlerMap const& handlers, Request const& request, boost::asio::yield_context yield ) { - auto it = handlers.find(request.target()); + ASSERT(request.target().has_value(), "Got not a HTTP request"); + auto it = handlers.find(*request.target()); if (it == handlers.end()) { return Response{boost::beast::http::status::bad_request, "Bad target", request}; } @@ -76,6 +77,24 @@ handleWsRequest( } // namespace +size_t +ConnectionHandler::StringHash::operator()(char const* str) const +{ + return hash_type{}(str); +} + +size_t +ConnectionHandler::StringHash::operator()(std::string_view str) const +{ + return hash_type{}(str); +} + +size_t +ConnectionHandler::StringHash::operator()(std::string const& str) const +{ + return hash_type{}(str); +} + void ConnectionHandler::onGet(std::string const& target, MessageHandler handler) { diff --git a/src/web/ng/impl/ConnectionHandler.hpp b/src/web/ng/impl/ConnectionHandler.hpp index cb56178f7..8b21a4af2 100644 --- a/src/web/ng/impl/ConnectionHandler.hpp +++ b/src/web/ng/impl/ConnectionHandler.hpp @@ -30,9 +30,11 @@ #include #include +#include #include #include #include +#include #include namespace web::ng::impl { @@ -41,6 +43,20 @@ class ConnectionHandler { public: enum class ProcessingStrategy { Sequent, Parallel }; + struct StringHash { + using hash_type = std::hash; + using is_transparent = void; + + std::size_t + operator()(char const* str) const; + std::size_t + operator()(std::string_view str) const; + std::size_t + operator()(std::string const& str) const; + }; + + using TargetToHandlerMap = std::unordered_map>; + private: util::Logger log_{"WebServer"}; util::Logger perfLog_{"Performance"}; @@ -48,8 +64,8 @@ class ConnectionHandler { ProcessingStrategy processingStrategy_; std::optional maxParallelRequests_; - std::unordered_map getHandlers_; - std::unordered_map postHandlers_; + TargetToHandlerMap getHandlers_; + TargetToHandlerMap postHandlers_; std::optional wsHandler_; using ConnectionsMap = std::unordered_map; diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index c3e59a4f7..a15d070c9 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -54,8 +54,10 @@ class UpgradableConnection : public Connection { using Connection::Connection; virtual std::expected - isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) - const = 0; + isUpgradeRequested( + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT + ) = 0; virtual std::expected upgrade( @@ -127,7 +129,9 @@ class HttpConnection : public UpgradableConnection { request_.reset(); return std::move(result); } - return fetch(yield, timeout); + return fetch(yield, timeout).and_then([](auto httpRequest) -> std::expected { + return Request{std::move(httpRequest)}; + }); } void @@ -138,16 +142,22 @@ class HttpConnection : public UpgradableConnection { boost::beast::get_lowest_layer(stream_).expires_after(timeout); stream_.async_shutdown(yield[error]); } - stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, error); + if constexpr (IsTcpStream) { + stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error); + } else { + boost::beast::get_lowest_layer(stream_).socket().shutdown( + boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error + ); + } } std::expected isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) - const override + override { auto expectedRequest = fetch(yield, timeout); if (not expectedRequest.has_value()) - return std::move(expectedRequest).error(); + return std::unexpected{std::move(expectedRequest).error()}; request_ = std::move(expectedRequest).value(); @@ -169,21 +179,21 @@ class HttpConnection : public UpgradableConnection { boost::beast::get_lowest_layer(stream_).release_socket(), std::move(ip_), std::move(buffer_), - request_, + std::move(request_).value(), sslContext.value(), tagDecoratorFactory, yield ); + } else { + return make_PlainWsConnection( + stream_.release_socket(), + std::move(ip_), + std::move(buffer_), + std::move(request_).value(), + tagDecoratorFactory, + yield + ); } - - return make_PlainWsConnection( - stream_.release_socket(), - std::move(ip_), - std::move(buffer_), - std::move(request_).value(), - tagDecoratorFactory, - yield - ); } private: diff --git a/src/web/ng/impl/WsConnection.cpp b/src/web/ng/impl/WsConnection.cpp index 60a065641..4fa3df281 100644 --- a/src/web/ng/impl/WsConnection.cpp +++ b/src/web/ng/impl/WsConnection.cpp @@ -19,6 +19,7 @@ #include "web/ng/impl/WsConnection.hpp" +#include "util/Taggable.hpp" #include "web/ng/Error.hpp" #include @@ -39,12 +40,15 @@ make_PlainWsConnection( boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, - boost::beast::http::request const& request, + boost::beast::http::request request, + util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield ) { - auto connection = std::make_unique(std::move(socket), std::move(ip), std::move(buffer)); - auto maybeError = connection->accept(request, yield); + auto connection = std::make_unique( + std::move(socket), std::move(ip), std::move(buffer), std::move(request), tagDecoratorFactory + ); + auto maybeError = connection->performHandshake(yield); if (maybeError.has_value()) return std::unexpected{maybeError.value()}; return std::move(connection); @@ -55,14 +59,16 @@ make_SslWsConnection( boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, - boost::beast::http::request const& request, + boost::beast::http::request request, boost::asio::ssl::context& sslContext, + util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield ) { - auto connection = - std::make_unique(std::move(socket), std::move(ip), std::move(buffer), sslContext); - auto maybeError = connection->accept(request, yield); + auto connection = std::make_unique( + std::move(socket), std::move(ip), std::move(buffer), sslContext, std::move(request), tagDecoratorFactory + ); + auto maybeError = connection->performHandshake(yield); if (maybeError.has_value()) return std::unexpected{maybeError.value()}; return std::move(connection); diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index 34cbe66fc..f2532fb42 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -60,10 +61,13 @@ class WsConnection : public Connection { boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, + boost::beast::http::request initialRequest, util::TagDecoratorFactory const& tagDecoratorFactory ) requires IsTcpStream - : Connection(std::move(ip), std::move(buffer), tagDecoratorFactory), stream_(std::move(socket)) + : Connection(std::move(ip), std::move(buffer), tagDecoratorFactory) + , stream_(std::move(socket)) + , initialRequest_(std::move(initialRequest)) { } @@ -126,9 +130,9 @@ class WsConnection : public Connection { { auto error = util::withTimeout([this](auto&& yield) { stream_.async_read(buffer_, yield); }, yield, timeout); if (error) - return std::move(error); + return std::unexpected{error}; - return Request{std::string{buffer_.data(), buffer_.size()}, initialRequest_}; + return Request{std::string{static_cast(buffer_.data().data()), buffer_.size()}, initialRequest_}; } void @@ -139,7 +143,7 @@ class WsConnection : public Connection { wsTimeout.handshake_timeout = timeout; stream_.set_option(wsTimeout); - stream_.async_close(yield); + stream_.async_close(boost::beast::websocket::close_code::normal, yield); } }; @@ -151,7 +155,7 @@ make_PlainWsConnection( boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, - boost::beast::http::request const& request, + boost::beast::http::request request, util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield ); @@ -161,7 +165,7 @@ make_SslWsConnection( boost::asio::ip::tcp::socket socket, std::string ip, boost::beast::flat_buffer buffer, - boost::beast::http::request const& request, + boost::beast::http::request request, boost::asio::ssl::context& sslContext, util::TagDecoratorFactory const& tagDecoratorFactory, boost::asio::yield_context yield From 6fc5d01b3b3217a159990041f01ed150fc33c828 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 2 Oct 2024 14:51:12 +0100 Subject: [PATCH 25/81] Bring changes from develop --- src/util/WithTimeout.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/util/WithTimeout.hpp b/src/util/WithTimeout.hpp index 9d2804774..851e4aaf8 100644 --- a/src/util/WithTimeout.hpp +++ b/src/util/WithTimeout.hpp @@ -30,6 +30,7 @@ #include #include +#include namespace util { @@ -44,15 +45,17 @@ boost::system::error_code withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) { boost::system::error_code error; + auto operationCompleted = std::make_shared(false); boost::asio::cancellation_signal cancellationSignal; auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]); boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout}; - timer.async_wait([&cancellationSignal](boost::system::error_code errorCode) { - if (!errorCode) + timer.async_wait([&cancellationSignal, operationCompleted](boost::system::error_code errorCode) { + if (!errorCode and !*operationCompleted) cancellationSignal.emit(boost::asio::cancellation_type::terminal); }); operation(cyield); + *operationCompleted = true; // Map error code to timeout if (error == boost::system::errc::operation_canceled) { From 6dcfbbd41c11956264e46cddfa5b4069c51d1317 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 2 Oct 2024 16:05:16 +0100 Subject: [PATCH 26/81] Add tests for withTimeout --- src/util/WithTimeout.hpp | 16 ++-- tests/common/CMakeLists.txt | 2 +- .../{WithTimeout.cpp => CallWithTimeout.cpp} | 4 +- .../{WithTimeout.hpp => CallWithTimeout.hpp} | 2 +- tests/unit/CMakeLists.txt | 1 + tests/unit/util/RepeatTests.cpp | 4 +- tests/unit/util/WithTimeout.cpp | 77 +++++++++++++++++++ 7 files changed, 94 insertions(+), 12 deletions(-) rename tests/common/util/{WithTimeout.cpp => CallWithTimeout.cpp} (92%) rename tests/common/util/{WithTimeout.hpp => CallWithTimeout.hpp} (93%) create mode 100644 tests/unit/util/WithTimeout.cpp diff --git a/src/util/WithTimeout.hpp b/src/util/WithTimeout.hpp index 851e4aaf8..0a1b5b864 100644 --- a/src/util/WithTimeout.hpp +++ b/src/util/WithTimeout.hpp @@ -34,12 +34,16 @@ namespace util { -/** Perform a coroutine operation with a timeout. - @param operation The operation to perform. - @param yield The yield context. - @param timeout The timeout duration. - @return The error code of the operation. -*/ +/** + * @brief Perform a coroutine operation with a timeout. + * + * @tparam Operation The operation type to perform. Must be a callable accepting yield context with bound cancellation + * token. + * @param operation The operation to perform. + * @param yield The yield context. + * @param timeout The timeout duration. + * @return The error code of the operation. + */ template boost::system::error_code withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index 10d5f5256..3b9d66310 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -2,7 +2,7 @@ add_library(clio_testing_common) target_sources( clio_testing_common PRIVATE util/StringUtils.cpp util/TestHttpServer.cpp util/TestWsServer.cpp util/TestObject.cpp - util/AssignRandomPort.cpp util/WithTimeout.cpp + util/AssignRandomPort.cpp util/CallWithTimeout.cpp ) include(deps/gtest) diff --git a/tests/common/util/WithTimeout.cpp b/tests/common/util/CallWithTimeout.cpp similarity index 92% rename from tests/common/util/WithTimeout.cpp rename to tests/common/util/CallWithTimeout.cpp index 9604fbde2..0f4710f03 100644 --- a/tests/common/util/WithTimeout.cpp +++ b/tests/common/util/CallWithTimeout.cpp @@ -17,7 +17,7 @@ */ //============================================================================== -#include "util/WithTimeout.hpp" +#include "util/CallWithTimeout.hpp" #include @@ -30,7 +30,7 @@ namespace tests::common::util { void -withTimeout(std::chrono::steady_clock::duration timeout, std::function function) +callWithTimeout(std::chrono::steady_clock::duration timeout, std::function function) { std::promise promise; auto future = promise.get_future(); diff --git a/tests/common/util/WithTimeout.hpp b/tests/common/util/CallWithTimeout.hpp similarity index 93% rename from tests/common/util/WithTimeout.hpp rename to tests/common/util/CallWithTimeout.hpp index 3d7821396..21289762f 100644 --- a/tests/common/util/WithTimeout.hpp +++ b/tests/common/util/CallWithTimeout.hpp @@ -31,6 +31,6 @@ namespace tests::common::util { * @param function The function to run */ void -withTimeout(std::chrono::steady_clock::duration timeout, std::function function); +callWithTimeout(std::chrono::steady_clock::duration timeout, std::function function); } // namespace tests::common::util diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 524a0cefa..8c6e259f5 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -125,6 +125,7 @@ target_sources( util/SignalsHandlerTests.cpp util/TimeUtilsTests.cpp util/TxUtilTests.cpp + util/WithTimeout.cpp # Webserver web/AdminVerificationTests.cpp web/dosguard/DOSGuardTests.cpp diff --git a/tests/unit/util/RepeatTests.cpp b/tests/unit/util/RepeatTests.cpp index f0d5b7cd6..69a6bf8cb 100644 --- a/tests/unit/util/RepeatTests.cpp +++ b/tests/unit/util/RepeatTests.cpp @@ -18,8 +18,8 @@ //============================================================================== #include "util/AsioContextTestFixture.hpp" +#include "util/CallWithTimeout.hpp" #include "util/Repeat.hpp" -#include "util/WithTimeout.hpp" #include #include @@ -41,7 +41,7 @@ struct RepeatTests : SyncAsioContextTest { void withRunningContext(std::function func) { - tests::common::util::withTimeout(std::chrono::seconds{1000}, [this, func = std::move(func)]() { + tests::common::util::callWithTimeout(std::chrono::seconds{1}, [this, func = std::move(func)]() { auto workGuard = boost::asio::make_work_guard(ctx); std::thread thread{[this]() { ctx.run(); }}; func(); diff --git a/tests/unit/util/WithTimeout.cpp b/tests/unit/util/WithTimeout.cpp new file mode 100644 index 000000000..6dcb9c93e --- /dev/null +++ b/tests/unit/util/WithTimeout.cpp @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/WithTimeout.hpp" + +#include "util/AsioContextTestFixture.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct WithTimeoutTests : SyncAsioContextTest { + using CYieldType = boost::asio::cancellation_slot_binder< + boost::asio::basic_yield_context, + boost::asio::cancellation_slot>; + + testing::StrictMock> operationMock; +}; + +TEST_F(WithTimeoutTests, CallsOperation) +{ + EXPECT_CALL(operationMock, Call); + runSpawn([&](boost::asio::yield_context yield) { + auto const error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::seconds{1}); + EXPECT_EQ(error, boost::system::error_code{}); + }); +} + +TEST_F(WithTimeoutTests, TimesOut) +{ + EXPECT_CALL(operationMock, Call).WillOnce([](auto cyield) { + boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield)}; + timer.expires_after(std::chrono::milliseconds{10}); + timer.async_wait(cyield); + }); + runSpawn([&](boost::asio::yield_context yield) { + auto error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::milliseconds{1}); + EXPECT_EQ(error.value(), boost::system::errc::timed_out); + }); +} + +TEST_F(WithTimeoutTests, OperationFailed) +{ + EXPECT_CALL(operationMock, Call).WillOnce([](auto cyield) { + boost::asio::ip::tcp::socket socket{boost::asio::get_associated_executor(cyield)}; + socket.async_send(boost::asio::buffer("test"), cyield); + }); + runSpawn([&](boost::asio::yield_context yield) { + auto error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::seconds{1}); + EXPECT_EQ(error.value(), boost::system::errc::bad_file_descriptor); + }); +} From 05c5e162b1d097514d75a081840e93879ab19855 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 2 Oct 2024 16:35:43 +0100 Subject: [PATCH 27/81] Add tests for AdminVerificationStrategy --- src/web/impl/AdminVerificationStrategy.cpp | 13 ++- tests/unit/CMakeLists.txt | 2 +- .../web/{ => impl}/AdminVerificationTests.cpp | 90 ++++++++++++++++--- 3 files changed, 82 insertions(+), 23 deletions(-) rename tests/unit/web/{ => impl}/AdminVerificationTests.cpp (60%) diff --git a/src/web/impl/AdminVerificationStrategy.cpp b/src/web/impl/AdminVerificationStrategy.cpp index 8c65bbada..4295279ad 100644 --- a/src/web/impl/AdminVerificationStrategy.cpp +++ b/src/web/impl/AdminVerificationStrategy.cpp @@ -85,15 +85,12 @@ make_AdminVerificationStrategy(util::Config const& serverConfig) { auto adminPassword = serverConfig.maybeValue("admin_password"); auto const localAdmin = serverConfig.maybeValue("local_admin"); + bool const localAdminEnabled = localAdmin && localAdmin.value(); - // Return error when localAdmin is true and admin_password is also set - if (localAdmin && localAdmin.value() && adminPassword) { - return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."}; - } - - // Return error when localAdmin is false but admin_password is not set - if (localAdmin && !localAdmin.value() && !adminPassword) { - return std::unexpected{"Admin config error, one method must be specified to authorize admin."}; + if (localAdminEnabled == adminPassword.has_value()) { + if (adminPassword.has_value()) + return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."}; + return std::unexpected{"Admin config error, either local_admin and admin_password must be specified."}; } return make_AdminVerificationStrategy(std::move(adminPassword)); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 8c6e259f5..d76f08b3c 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -127,10 +127,10 @@ target_sources( util/TxUtilTests.cpp util/WithTimeout.cpp # Webserver - web/AdminVerificationTests.cpp web/dosguard/DOSGuardTests.cpp web/dosguard/IntervalSweepHandlerTests.cpp web/dosguard/WhitelistHandlerTests.cpp + web/impl/AdminVerificationTests.cpp web/impl/ServerSslContextTests.cpp web/RPCServerHandlerTests.cpp web/ServerTests.cpp diff --git a/tests/unit/web/AdminVerificationTests.cpp b/tests/unit/web/impl/AdminVerificationTests.cpp similarity index 60% rename from tests/unit/web/AdminVerificationTests.cpp rename to tests/unit/web/impl/AdminVerificationTests.cpp index 942ecfe1a..8b6f610e3 100644 --- a/tests/unit/web/AdminVerificationTests.cpp +++ b/tests/unit/web/impl/AdminVerificationTests.cpp @@ -18,16 +18,17 @@ //============================================================================== #include "util/LoggerFixtures.hpp" +#include "util/config/Config.hpp" #include "web/impl/AdminVerificationStrategy.hpp" #include #include #include +#include #include #include #include -#include namespace http = boost::beast::http; @@ -81,16 +82,7 @@ TEST_F(PasswordAdminVerificationStrategyTest, IsAdminReturnsTrueOnlyForValidPass } struct MakeAdminVerificationStrategyTestParams { - MakeAdminVerificationStrategyTestParams( - std::optional passwordOpt, - bool expectIpStrategy, - bool expectPasswordStrategy - ) - : passwordOpt(std::move(passwordOpt)) - , expectIpStrategy(expectIpStrategy) - , expectPasswordStrategy(expectPasswordStrategy) - { - } + std::string testName; std::optional passwordOpt; bool expectIpStrategy; bool expectPasswordStrategy; @@ -111,8 +103,78 @@ INSTANTIATE_TEST_CASE_P( MakeAdminVerificationStrategyTest, MakeAdminVerificationStrategyTest, testing::Values( - MakeAdminVerificationStrategyTestParams(std::nullopt, true, false), - MakeAdminVerificationStrategyTestParams("p", false, true), - MakeAdminVerificationStrategyTestParams("", false, true) + MakeAdminVerificationStrategyTestParams{ + .testName = "NoPassword", + .passwordOpt = std::nullopt, + .expectIpStrategy = true, + .expectPasswordStrategy = false + }, + MakeAdminVerificationStrategyTestParams{ + .testName = "HasPassword", + .passwordOpt = "p", + .expectIpStrategy = false, + .expectPasswordStrategy = true + }, + MakeAdminVerificationStrategyTestParams{ + .testName = "EmptyPassword", + .passwordOpt = "", + .expectIpStrategy = false, + .expectPasswordStrategy = true + } + ) +); + +struct MakeAdminVerificationStrategyFromConfigTestParams { + std::string testName; + std::string config; + bool expectedError; +}; + +struct MakeAdminVerificationStrategyFromConfigTest + : public testing::TestWithParam {}; + +TEST_P(MakeAdminVerificationStrategyFromConfigTest, ChecksConfig) +{ + util::Config serverConfig{boost::json::parse(GetParam().config)}; + auto const result = web::impl::make_AdminVerificationStrategy(serverConfig); + if (GetParam().expectedError) { + EXPECT_FALSE(result.has_value()); + } +} + +INSTANTIATE_TEST_SUITE_P( + MakeAdminVerificationStrategyFromConfigTest, + MakeAdminVerificationStrategyFromConfigTest, + testing::Values( + MakeAdminVerificationStrategyFromConfigTestParams{ + .testName = "NoPasswordNoLocalAdmin", + .config = "{}", + .expectedError = true + }, + MakeAdminVerificationStrategyFromConfigTestParams{ + .testName = "OnlyPassword", + .config = R"({"admin_password": "password"})", + .expectedError = false + }, + MakeAdminVerificationStrategyFromConfigTestParams{ + .testName = "OnlyLocalAdmin", + .config = R"({"local_admin": true})", + .expectedError = false + }, + MakeAdminVerificationStrategyFromConfigTestParams{ + .testName = "OnlyLocalAdminDisabled", + .config = R"({"local_admin": false})", + .expectedError = true + }, + MakeAdminVerificationStrategyFromConfigTestParams{ + .testName = "LocalAdminAndPassword", + .config = R"({"local_admin": true, "admin_password": "password"})", + .expectedError = true + }, + MakeAdminVerificationStrategyFromConfigTestParams{ + .testName = "LocalAdminDisabledAndPassword", + .config = R"({"local_admin": false, "admin_password": "password"})", + .expectedError = false + } ) ); From 2d34b2819978828624feeaa652e93545725d027a Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 3 Oct 2024 17:54:47 +0100 Subject: [PATCH 28/81] Add tests for ServerSslContext --- src/web/CMakeLists.txt | 2 - src/web/Server.cpp | 48 ----- src/web/Server.hpp | 13 +- src/web/impl/ServerSslContext.cpp | 80 -------- src/web/ng/impl/ServerSslContext.cpp | 27 ++- src/web/ng/impl/ServerSslContext.hpp | 2 +- tests/common/util/TmpFile.hpp | 22 ++- tests/unit/CMakeLists.txt | 9 +- tests/unit/test_data/SslCert.cpp | 105 ++++++++++ .../unit/test_data/SslCert.hpp | 22 ++- tests/unit/test_data/cert.pem | 22 --- tests/unit/test_data/key.pem | 27 --- tests/unit/web/ServerTests.cpp | 25 ++- tests/unit/web/impl/ServerSslContextTests.cpp | 48 ----- .../web/ng/impl/ServerSslContextTests.cpp | 181 ++++++++++++++++++ 15 files changed, 353 insertions(+), 280 deletions(-) delete mode 100644 src/web/Server.cpp delete mode 100644 src/web/impl/ServerSslContext.cpp create mode 100644 tests/unit/test_data/SslCert.cpp rename src/web/impl/ServerSslContext.hpp => tests/unit/test_data/SslCert.hpp (79%) delete mode 100644 tests/unit/test_data/cert.pem delete mode 100644 tests/unit/test_data/key.pem delete mode 100644 tests/unit/web/impl/ServerSslContextTests.cpp create mode 100644 tests/unit/web/ng/impl/ServerSslContextTests.cpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index e2545d1a6..b5b0ae9f0 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -3,12 +3,10 @@ add_library(clio_web) target_sources( clio_web PRIVATE Resolver.cpp - Server.cpp dosguard/DOSGuard.cpp dosguard/IntervalSweepHandler.cpp dosguard/WhitelistHandler.cpp impl/AdminVerificationStrategy.cpp - impl/ServerSslContext.cpp ng/Connection.cpp ng/impl/ConnectionHandler.cpp ng/impl/ServerSslContext.cpp diff --git a/src/web/Server.cpp b/src/web/Server.cpp deleted file mode 100644 index a20d72ee3..000000000 --- a/src/web/Server.cpp +++ /dev/null @@ -1,48 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2024, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "web/Server.hpp" - -#include "util/config/Config.hpp" - -#include - -#include -#include - -namespace web { - -std::expected, std::string> -makeServerSslContext(util::Config const& config) -{ - bool const configHasCertFile = config.contains("ssl_cert_file"); - bool const configHasKeyFile = config.contains("ssl_key_file"); - - if (configHasCertFile != configHasKeyFile) - return std::unexpected{"Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together."}; - - if (not configHasCertFile) - return std::nullopt; - - auto const certFilename = config.value("ssl_cert_file"); - auto const keyFilename = config.value("ssl_key_file"); - - return impl::makeServerSslContext(certFilename, keyFilename); -} -} // namespace web diff --git a/src/web/Server.hpp b/src/web/Server.hpp index 4c57c88ed..ec4556fb6 100644 --- a/src/web/Server.hpp +++ b/src/web/Server.hpp @@ -24,8 +24,8 @@ #include "web/HttpSession.hpp" #include "web/SslHttpSession.hpp" #include "web/dosguard/DOSGuardInterface.hpp" -#include "web/impl/ServerSslContext.hpp" #include "web/interface/Concepts.hpp" +#include "web/ng/impl/ServerSslContext.hpp" #include #include @@ -59,15 +59,6 @@ */ namespace web { -/** - * @brief A helper function to create a server SSL context. - * - * @param config The config to create the context - * @return Optional SSL context or error message if any - */ -std::expected, std::string> -makeServerSslContext(util::Config const& config); - /** * @brief The Detector class to detect if the connection is a ssl or not. * @@ -333,7 +324,7 @@ make_HttpServer( { static util::Logger const log{"WebServer"}; - auto expectedSslContext = makeServerSslContext(config); + auto expectedSslContext = ng::impl::makeServerSslContext(config); if (not expectedSslContext) { LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error(); return nullptr; diff --git a/src/web/impl/ServerSslContext.cpp b/src/web/impl/ServerSslContext.cpp deleted file mode 100644 index 06460f4f4..000000000 --- a/src/web/impl/ServerSslContext.cpp +++ /dev/null @@ -1,80 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2024, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "web/impl/ServerSslContext.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace web::impl { - -namespace { - -std::optional -readFile(std::string const& path) -{ - std::ifstream const file(path, std::ios::in | std::ios::binary); - if (!file) - return {}; - - std::stringstream contents; - contents << file.rdbuf(); - return std::move(contents).str(); -} - -} // namespace - -std::expected -makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath) -{ - auto const certContent = readFile(certFilePath); - if (!certContent) - return std::unexpected{"Can't read SSL certificate: " + certFilePath}; - - auto const keyContent = readFile(keyFilePath); - if (!keyContent) - return std::unexpected{"Can't read SSL key: " + keyFilePath}; - - using namespace boost::asio; - - ssl::context ctx{ssl::context::tls_server}; - ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2); - - try { - ctx.use_certificate_chain(buffer(certContent->data(), certContent->size())); - ctx.use_private_key(buffer(keyContent->data(), keyContent->size()), ssl::context::file_format::pem); - } catch (...) { - return std::unexpected{ - fmt::format("Error loading SSL certificate ({}) or SSL key ({}).", certFilePath, keyFilePath) - }; - } - - return ctx; -} - -} // namespace web::impl diff --git a/src/web/ng/impl/ServerSslContext.cpp b/src/web/ng/impl/ServerSslContext.cpp index 9ef2e72b7..46e9fb2af 100644 --- a/src/web/ng/impl/ServerSslContext.cpp +++ b/src/web/ng/impl/ServerSslContext.cpp @@ -64,34 +64,31 @@ makeServerSslContext(util::Config const& config) return std::nullopt; auto const certFilename = config.value("ssl_cert_file"); + auto const certContent = readFile(certFilename); + if (!certContent) + return std::unexpected{"Can't read SSL certificate: " + certFilename}; + auto const keyFilename = config.value("ssl_key_file"); + auto const keyContent = readFile(keyFilename); + if (!keyContent) + return std::unexpected{"Can't read SSL key: " + keyFilename}; - return impl::makeServerSslContext(certFilename, keyFilename); + return impl::makeServerSslContext(*certContent, *keyContent); } std::expected -makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath) +makeServerSslContext(std::string const& certData, std::string const& keyData) { - auto const certContent = readFile(certFilePath); - if (!certContent) - return std::unexpected{"Can't read SSL certificate: " + certFilePath}; - - auto const keyContent = readFile(keyFilePath); - if (!keyContent) - return std::unexpected{"Can't read SSL key: " + keyFilePath}; - using namespace boost::asio; ssl::context ctx{ssl::context::tls_server}; ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2); try { - ctx.use_certificate_chain(buffer(certContent->data(), certContent->size())); - ctx.use_private_key(buffer(keyContent->data(), keyContent->size()), ssl::context::file_format::pem); + ctx.use_certificate_chain(buffer(certData.data(), certData.size())); + ctx.use_private_key(buffer(keyData.data(), keyData.size()), ssl::context::file_format::pem); } catch (...) { - return std::unexpected{ - fmt::format("Error loading SSL certificate ({}) or SSL key ({}).", certFilePath, keyFilePath) - }; + return std::unexpected{fmt::format("Error loading SSL certificate or SSL key.")}; } return ctx; diff --git a/src/web/ng/impl/ServerSslContext.hpp b/src/web/ng/impl/ServerSslContext.hpp index da89d8727..151f8b06e 100644 --- a/src/web/ng/impl/ServerSslContext.hpp +++ b/src/web/ng/impl/ServerSslContext.hpp @@ -33,6 +33,6 @@ std::expected, std::string> makeServerSslContext(util::Config const& config); std::expected -makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath); +makeServerSslContext(std::string const& certData, std::string const& keyData); } // namespace web::ng::impl diff --git a/tests/common/util/TmpFile.hpp b/tests/common/util/TmpFile.hpp index 71292ed3e..f09647035 100644 --- a/tests/common/util/TmpFile.hpp +++ b/tests/common/util/TmpFile.hpp @@ -25,9 +25,10 @@ #include #include #include +#include struct TmpFile { - std::string const path; + std::string path; TmpFile(std::string_view content) : path{std::tmpnam(nullptr)} { @@ -36,8 +37,25 @@ struct TmpFile { ofs << content; } + TmpFile(TmpFile const&) = delete; + TmpFile(TmpFile&& other) : path{std::move(other.path)} + { + other.path.clear(); + } + TmpFile& + operator=(TmpFile const&) = delete; + TmpFile& + + operator=(TmpFile&& other) + { + if (this != &other) + *this = std::move(other); + return *this; + } + ~TmpFile() { - std::filesystem::remove(path); + if (not path.empty()) + std::filesystem::remove(path); } }; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index d76f08b3c..9855b652b 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -94,6 +94,7 @@ target_sources( rpc/RPCEngineTests.cpp rpc/RPCHelpersTests.cpp rpc/WorkQueueTests.cpp + test_data/SslCert.cpp util/AccountUtilsTests.cpp util/AssertTests.cpp # Async framework @@ -131,7 +132,7 @@ target_sources( web/dosguard/IntervalSweepHandlerTests.cpp web/dosguard/WhitelistHandlerTests.cpp web/impl/AdminVerificationTests.cpp - web/impl/ServerSslContextTests.cpp + web/ng/impl/ServerSslContextTests.cpp web/RPCServerHandlerTests.cpp web/ServerTests.cpp # New Config @@ -144,12 +145,6 @@ target_sources( util/newconfig/ValueViewTests.cpp ) -configure_file(test_data/cert.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/cert.pem COPYONLY) -target_compile_definitions(clio_tests PRIVATE TEST_DATA_SSL_CERT_PATH="tests/unit/test_data/cert.pem") - -configure_file(test_data/key.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/key.pem COPYONLY) -target_compile_definitions(clio_tests PRIVATE TEST_DATA_SSL_KEY_PATH="tests/unit/test_data/key.pem") - # See https://github.com/google/googletest/issues/3475 gtest_discover_tests(clio_tests DISCOVERY_TIMEOUT 90) diff --git a/tests/unit/test_data/SslCert.cpp b/tests/unit/test_data/SslCert.cpp new file mode 100644 index 000000000..ad4bee301 --- /dev/null +++ b/tests/unit/test_data/SslCert.cpp @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/TmpFile.hpp" + +#include + +#include + +namespace tests { + +std::string_view +sslCert() +{ + static auto constexpr CERT = R"( +-----BEGIN CERTIFICATE----- +MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV +BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz +Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN +MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m +pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y +7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w +A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk +PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979 +WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p +AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x +MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq +MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN +BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn +svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO +p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb +8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp +LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+ +PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw== +-----END CERTIFICATE----- +)"; + return CERT; +} + +TmpFile +sslCertFile() +{ + return TmpFile{sslCert()}; +} + +std::string_view +sslKey() +{ + static auto constexpr KEY = R"( +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4 +32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd +g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x +evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC +tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC +oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4 +YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1 +vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ +ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh ++lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk +naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK +nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn +K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z +dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz +fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs +fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7 +zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl +6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf +pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV +zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI +9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya +LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD +KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du +O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7 +Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA== +-----END RSA PRIVATE KEY----- +)"; + return KEY; +} + +TmpFile +sslKeyFile() +{ + return TmpFile{sslKey()}; +} + +} // namespace tests diff --git a/src/web/impl/ServerSslContext.hpp b/tests/unit/test_data/SslCert.hpp similarity index 79% rename from src/web/impl/ServerSslContext.hpp rename to tests/unit/test_data/SslCert.hpp index 06698f732..86c5342e9 100644 --- a/src/web/impl/ServerSslContext.hpp +++ b/tests/unit/test_data/SslCert.hpp @@ -19,14 +19,22 @@ #pragma once -#include +#include "util/TmpFile.hpp" -#include -#include +#include -namespace web::impl { +namespace tests { -std::expected -makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath); +std::string_view +sslCert(); -} // namespace web::impl +TmpFile +sslCertFile(); + +std::string_view +sslKey(); + +TmpFile +sslKeyFile(); + +} // namespace tests diff --git a/tests/unit/test_data/cert.pem b/tests/unit/test_data/cert.pem deleted file mode 100644 index 7ef61709e..000000000 --- a/tests/unit/test_data/cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV -BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz -Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV -UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN -MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m -pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y -7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w -A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk -PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979 -WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p -AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x -MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq -MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN -BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn -svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO -p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb -8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp -LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+ -PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw== ------END CERTIFICATE----- diff --git a/tests/unit/test_data/key.pem b/tests/unit/test_data/key.pem deleted file mode 100644 index ff714e736..000000000 --- a/tests/unit/test_data/key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4 -32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd -g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x -evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC -tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC -oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4 -YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1 -vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ -ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh -+lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk -naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK -nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn -K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z -dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz -fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs -fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7 -zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl -6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf -pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV -zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI -9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya -LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD -KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du -O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7 -Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA== ------END RSA PRIVATE KEY----- diff --git a/tests/unit/web/ServerTests.cpp b/tests/unit/web/ServerTests.cpp index a61b30746..53920406d 100644 --- a/tests/unit/web/ServerTests.cpp +++ b/tests/unit/web/ServerTests.cpp @@ -21,6 +21,7 @@ #include "util/LoggerFixtures.hpp" #include "util/MockPrometheus.hpp" #include "util/TestHttpSyncClient.hpp" +#include "util/TmpFile.hpp" #include "util/config/Config.hpp" #include "util/prometheus/Label.hpp" #include "util/prometheus/Prometheus.hpp" @@ -43,6 +44,7 @@ #include #include #include +#include #include #include @@ -101,14 +103,6 @@ generateJSONDataOverload(std::string_view port) )); } -boost::json::value -addSslConfig(boost::json::value config) -{ - config.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH; - config.as_object()["ssl_cert_file"] = TEST_DATA_SSL_CERT_PATH; - return config; -} - struct WebServerTest : NoLoggerFixture { ~WebServerTest() override { @@ -124,6 +118,14 @@ struct WebServerTest : NoLoggerFixture { runner.emplace([this] { ctx.run(); }); } + boost::json::value + addSslConfig(boost::json::value config) const + { + config.as_object()["ssl_key_file"] = sslKeyFile.path; + config.as_object()["ssl_cert_file"] = sslCertFile.path; + return config; + } + // this ctx is for dos timer boost::asio::io_context ctxSync; std::string const port = std::to_string(tests::util::generateFreePort()); @@ -139,6 +141,9 @@ struct WebServerTest : NoLoggerFixture { // this ctx is for http server boost::asio::io_context ctx; + TmpFile sslCertFile{tests::sslCertFile()}; + TmpFile sslKeyFile{tests::sslKeyFile()}; + private: std::optional work; std::optional runner; @@ -267,7 +272,7 @@ TEST_F(WebServerTest, IncompleteSslConfig) auto e = std::make_shared(); auto jsonConfig = generateJSONWithDynamicPort(port); - jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH; + jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path; auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e); EXPECT_EQ(server, nullptr); @@ -278,7 +283,7 @@ TEST_F(WebServerTest, WrongSslConfig) auto e = std::make_shared(); auto jsonConfig = generateJSONWithDynamicPort(port); - jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH; + jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path; jsonConfig.as_object()["ssl_cert_file"] = "wrong_path"; auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e); diff --git a/tests/unit/web/impl/ServerSslContextTests.cpp b/tests/unit/web/impl/ServerSslContextTests.cpp deleted file mode 100644 index 3febd5dca..000000000 --- a/tests/unit/web/impl/ServerSslContextTests.cpp +++ /dev/null @@ -1,48 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2024, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "web/impl/ServerSslContext.hpp" - -#include - -using namespace web::impl; - -TEST(ServerSslContext, makeServerSslContext) -{ - auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, TEST_DATA_SSL_KEY_PATH); - ASSERT_TRUE(sslContext); -} - -TEST(ServerSslContext, makeServerSslContext_WrongCertPath) -{ - auto const sslContext = makeServerSslContext("wrong_path", TEST_DATA_SSL_KEY_PATH); - ASSERT_FALSE(sslContext); -} - -TEST(ServerSslContext, makeServerSslContext_WrongKeyPath) -{ - auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, "wrong_path"); - ASSERT_FALSE(sslContext); -} - -TEST(ServerSslContext, makeServerSslContext_CertKeyMismatch) -{ - auto const sslContext = makeServerSslContext(TEST_DATA_SSL_KEY_PATH, TEST_DATA_SSL_CERT_PATH); - ASSERT_FALSE(sslContext); -} diff --git a/tests/unit/web/ng/impl/ServerSslContextTests.cpp b/tests/unit/web/ng/impl/ServerSslContextTests.cpp new file mode 100644 index 000000000..6a98bb3cd --- /dev/null +++ b/tests/unit/web/ng/impl/ServerSslContextTests.cpp @@ -0,0 +1,181 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/NameGenerator.hpp" +#include "util/TmpFile.hpp" +#include "util/config/Config.hpp" +#include "web/ng/impl/ServerSslContext.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace web::ng::impl; + +struct MakeServerSslContextFromConfigTestBundle { + std::string testName; + std::optional certFile; + std::optional keyFile; + std::optional expectedError; + bool expectContext; + + boost::json::value + configJson() const + { + boost::json::object result; + if (certFile.has_value()) { + result["ssl_cert_file"] = *certFile; + } + + if (keyFile.has_value()) { + result["ssl_key_file"] = *keyFile; + } + return result; + } +}; + +struct MakeServerSslContextFromConfigTest : testing::TestWithParam {}; + +TEST_P(MakeServerSslContextFromConfigTest, makeFromConfig) +{ + auto const config = util::Config{GetParam().configJson()}; + auto const expectedServerSslContext = makeServerSslContext(config); + if (GetParam().expectedError.has_value()) { + ASSERT_FALSE(expectedServerSslContext.has_value()); + EXPECT_THAT(expectedServerSslContext.error(), testing::HasSubstr(*GetParam().expectedError)); + } else { + EXPECT_EQ(expectedServerSslContext.value().has_value(), GetParam().expectContext); + } +} + +INSTANTIATE_TEST_SUITE_P( + MakeServerSslContextFromConfigTest, + MakeServerSslContextFromConfigTest, + testing::ValuesIn( + {MakeServerSslContextFromConfigTestBundle{ + .testName = "NoCertNoKey", + .certFile = std::nullopt, + .keyFile = std::nullopt, + .expectedError = std::nullopt, + .expectContext = false + }, + MakeServerSslContextFromConfigTestBundle{ + .testName = "CertOnly", + .certFile = "some_path", + .keyFile = std::nullopt, + .expectedError = "Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together.", + .expectContext = false + }, + MakeServerSslContextFromConfigTestBundle{ + .testName = "KeyOnly", + .certFile = std::nullopt, + .keyFile = "some_path", + .expectedError = "Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together.", + .expectContext = false + }, + MakeServerSslContextFromConfigTestBundle{ + .testName = "BothKeyAndCert", + .certFile = "some_path", + .keyFile = "some_other_path", + .expectedError = "Can't read SSL certificate", + .expectContext = false + }} + ), + tests::util::NameGenerator +); + +struct MakeServerSslContextFromConfigRealFilesTest : testing::Test {}; + +TEST_F(MakeServerSslContextFromConfigRealFilesTest, WrongKeyFile) +{ + auto const certFile = tests::sslCertFile(); + boost::json::object configJson = {{"ssl_cert_file", certFile.path}, {"ssl_key_file", "some_path"}}; + + util::Config const config{configJson}; + auto const expectedServerSslContext = makeServerSslContext(config); + ASSERT_FALSE(expectedServerSslContext.has_value()); + EXPECT_THAT(expectedServerSslContext.error(), testing::HasSubstr("Can't read SSL key")); +} + +TEST_F(MakeServerSslContextFromConfigRealFilesTest, BothFilesValid) +{ + auto const certFile = tests::sslCertFile(); + auto const keyFile = tests::sslKeyFile(); + boost::json::object configJson = {{"ssl_cert_file", certFile.path}, {"ssl_key_file", keyFile.path}}; + + util::Config const config{configJson}; + auto const expectedServerSslContext = makeServerSslContext(config); + EXPECT_TRUE(expectedServerSslContext.has_value()); +} + +struct MakeServerSslContextFromDataTestBundle { + std::string testName; + std::string certData; + std::string keyData; + bool expectedSuccess; +}; + +struct MakeServerSslContextFromDataTest : testing::TestWithParam {}; + +TEST_P(MakeServerSslContextFromDataTest, makeFromData) +{ + auto const& data = GetParam(); + auto const expectedServerSslContext = makeServerSslContext(data.certData, data.keyData); + EXPECT_EQ(expectedServerSslContext.has_value(), data.expectedSuccess); +} + +INSTANTIATE_TEST_SUITE_P( + MakeServerSslContextFromDataTest, + MakeServerSslContextFromDataTest, + testing::ValuesIn( + {MakeServerSslContextFromDataTestBundle{ + .testName = "EmptyData", + .certData = "", + .keyData = "", + .expectedSuccess = false + }, + MakeServerSslContextFromDataTestBundle{ + .testName = "CertOnly", + .certData = std::string{tests::sslCert()}, + .keyData = "", + .expectedSuccess = false + }, + MakeServerSslContextFromDataTestBundle{ + .testName = "KeyOnly", + .certData = "", + .keyData = std::string{tests::sslKey()}, + .expectedSuccess = false + }, + MakeServerSslContextFromDataTestBundle{ + .testName = "BothKeyAndCert", + .certData = std::string{tests::sslCert()}, + .keyData = std::string{tests::sslKey()}, + .expectedSuccess = true + }} + ), + tests::util::NameGenerator +); From 84c7a18681927d2a205285c3a373ec88b2bfaa83 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 7 Oct 2024 13:34:50 +0100 Subject: [PATCH 29/81] Cover Request with tests --- src/web/ng/Request.cpp | 8 ++ src/web/ng/Request.hpp | 69 +++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/web/ng/RequestTests.cpp | 224 +++++++++++++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 tests/unit/web/ng/RequestTests.cpp diff --git a/src/web/ng/Request.cpp b/src/web/ng/Request.cpp index 011aab033..9b5384775 100644 --- a/src/web/ng/Request.cpp +++ b/src/web/ng/Request.cpp @@ -87,6 +87,14 @@ Request::asHttpRequest() const return httpRequest(); } +std::string_view +Request::message() const +{ + if (not isHttp()) + return std::get(data_).request; + return httpRequest().body(); +} + std::optional Request::target() const { diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 0fa507bcf..01a5639ce 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -31,8 +31,14 @@ namespace web::ng { +/** + * @brief Represents an HTTP or WebSocket request. + */ class Request { public: + /** + * @brief The headers of an HTTP request. + */ using HttpHeaders = boost::beast::http::request::header_type; private: @@ -45,30 +51,93 @@ class Request { std::variant data_; public: + /** + * @brief Construct from an HTTP request. + * + * @param request The HTTP request. + */ explicit Request(boost::beast::http::request request); + + /** + * @brief Construct from a WebSocket request. + * + * @param request The WebSocket request. + * @param headers The headers of the HTTP request initiated the WebSocket connection + */ Request(std::string request, HttpHeaders const& headers); + /** + * @brief Method of the request. + * @note WEBSOCKET is not a real method, it is used to distinguish WebSocket requests from HTTP requests. + */ enum class Method { GET, POST, WEBSOCKET, UNSUPPORTED }; + /** + * @brief Get the method of the request. + * + * @return The method of the request. + */ Method method() const; + /** + * @brief Check if the request is an HTTP request. + * + * @return true if the request is an HTTP request, false otherwise. + */ bool isHttp() const; + /** + * @brief Get the HTTP request. + * + * @return The HTTP request or std::nullopt if the request is a WebSocket request. + */ std::optional const>> asHttpRequest() const; + /** + * @brief Get the body (in case of an HTTP request) or the message (in case of a WebSocket request). + * + * @return The message of the request. + */ + std::string_view + message() const; + + /** + * @brief Get the target of the request. + * + * @return The target of the request or std::nullopt if the request is a WebSocket request. + */ std::optional target() const; + /** + * @brief Get the value of a header. + * + * @param headerName The name of the header. + * @return The value of the header or std::nullopt if the header does not exist. + */ std::optional headerValue(boost::beast::http::field headerName) const; + /** + * @brief Get the value of a header. + * + * @param headerName The name of the header. + * @return The value of the header or std::nullopt if the header does not exist. + */ std::optional headerValue(std::string const& headerName) const; private: + /** + * @brief Get the HTTP request. + * @note This function assumes that the request is an HTTP request. So if data_ is not an HTTP request, + * the behavior is undefined. + * + * @return The HTTP request. + */ HttpRequest const& httpRequest() const; }; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 9855b652b..a5cf75e70 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -132,6 +132,7 @@ target_sources( web/dosguard/IntervalSweepHandlerTests.cpp web/dosguard/WhitelistHandlerTests.cpp web/impl/AdminVerificationTests.cpp + web/ng/RequestTests.cpp web/ng/impl/ServerSslContextTests.cpp web/RPCServerHandlerTests.cpp web/ServerTests.cpp diff --git a/tests/unit/web/ng/RequestTests.cpp b/tests/unit/web/ng/RequestTests.cpp new file mode 100644 index 000000000..0e6f2ef71 --- /dev/null +++ b/tests/unit/web/ng/RequestTests.cpp @@ -0,0 +1,224 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/NameGenerator.hpp" +#include "web/ng/Request.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace web::ng; +namespace http = boost::beast::http; + +struct RequestTest : public ::testing::Test {}; + +struct RequestMethodTestBundle { + std::string testName; + Request request; + Request::Method expectedMethod; +}; + +struct RequestMethodTest : RequestTest, ::testing::WithParamInterface {}; + +TEST_P(RequestMethodTest, method) +{ + EXPECT_EQ(GetParam().request.method(), GetParam().expectedMethod); +} + +INSTANTIATE_TEST_SUITE_P( + RequestMethodTest, + RequestMethodTest, + testing::Values( + RequestMethodTestBundle{ + .testName = "HttpGet", + .request = Request{http::request{http::verb::get, "/", 11}}, + .expectedMethod = Request::Method::GET, + }, + RequestMethodTestBundle{ + .testName = "HttpPost", + .request = Request{http::request{http::verb::post, "/", 11}}, + .expectedMethod = Request::Method::POST, + }, + RequestMethodTestBundle{ + .testName = "WebSocket", + .request = Request{"websocket message", Request::HttpHeaders{}}, + .expectedMethod = Request::Method::WEBSOCKET, + }, + RequestMethodTestBundle{ + .testName = "Unsupported", + .request = Request{http::request{http::verb::acl, "/", 11}}, + .expectedMethod = Request::Method::UNSUPPORTED, + } + ), + tests::util::NameGenerator +); + +struct RequestIsHttpTestBundle { + std::string testName; + Request request; + bool expectedIsHttp; +}; + +struct RequestIsHttpTest : RequestTest, testing::WithParamInterface {}; + +TEST_P(RequestIsHttpTest, isHttp) +{ + EXPECT_EQ(GetParam().request.isHttp(), GetParam().expectedIsHttp); +} + +INSTANTIATE_TEST_SUITE_P( + RequestIsHttpTest, + RequestIsHttpTest, + testing::Values( + RequestIsHttpTestBundle{ + .testName = "HttpRequest", + .request = Request{http::request{http::verb::get, "/", 11}}, + .expectedIsHttp = true, + }, + RequestIsHttpTestBundle{ + .testName = "WebSocketRequest", + .request = Request{"websocket message", Request::HttpHeaders{}}, + .expectedIsHttp = false, + } + ), + tests::util::NameGenerator +); + +struct RequestAsHttpRequestTest : RequestTest {}; + +TEST_F(RequestAsHttpRequestTest, HttpRequest) +{ + http::request const httpRequest{http::verb::get, "/some", 11}; + Request const request{httpRequest}; + auto const maybeHttpRequest = request.asHttpRequest(); + ASSERT_TRUE(maybeHttpRequest.has_value()); + auto const& actualHttpRequest = maybeHttpRequest->get(); + EXPECT_EQ(actualHttpRequest.method(), httpRequest.method()); + EXPECT_EQ(actualHttpRequest.target(), httpRequest.target()); + EXPECT_EQ(actualHttpRequest.version(), httpRequest.version()); +} + +TEST_F(RequestAsHttpRequestTest, WebSocketRequest) +{ + Request const request{"websocket message", Request::HttpHeaders{}}; + auto const maybeHttpRequest = request.asHttpRequest(); + EXPECT_FALSE(maybeHttpRequest.has_value()); +} + +struct RequestMessageTest : RequestTest {}; + +TEST_F(RequestMessageTest, HttpRequest) +{ + std::string const body = "some body"; + http::request const httpRequest{http::verb::post, "/some", 11, body}; + Request const request{httpRequest}; + EXPECT_EQ(request.message(), httpRequest.body()); +} + +TEST_F(RequestMessageTest, WebSocketRequest) +{ + std::string const message = "websocket message"; + Request const request{message, Request::HttpHeaders{}}; + EXPECT_EQ(request.message(), message); +} + +struct RequestTargetTestBundle { + std::string testName; + Request request; + std::optional expectedTarget; +}; + +struct RequestTargetTest : RequestTest, ::testing::WithParamInterface {}; + +TEST_P(RequestTargetTest, target) +{ + auto const maybeTarget = GetParam().request.target(); + EXPECT_EQ(maybeTarget, GetParam().expectedTarget); +} + +INSTANTIATE_TEST_SUITE_P( + RequestTargetTest, + RequestTargetTest, + testing::Values( + RequestTargetTestBundle{ + .testName = "HttpRequest", + .request = Request{http::request{http::verb::get, "/some", 11}}, + .expectedTarget = "/some", + }, + RequestTargetTestBundle{ + .testName = "WebSocketRequest", + .request = Request{"websocket message", Request::HttpHeaders{}}, + .expectedTarget = std::nullopt, + } + ), + tests::util::NameGenerator +); + +struct RequestHeaderValueTest : RequestTest {}; + +TEST_F(RequestHeaderValueTest, headerValue) +{ + http::request httpRequest{http::verb::get, "/some", 11}; + http::field const headerName = http::field::user_agent; + std::string const headerValue = "clio"; + httpRequest.set(headerName, headerValue); + + Request const request{httpRequest}; + auto const maybeHeaderValue = request.headerValue(headerName); + ASSERT_TRUE(maybeHeaderValue.has_value()); + EXPECT_EQ(maybeHeaderValue.value(), headerValue); +} + +TEST_F(RequestHeaderValueTest, headerValueString) +{ + http::request httpRequest{http::verb::get, "/some", 11}; + std::string const headerName = "Custom"; + std::string const headerValue = "some value"; + httpRequest.set(headerName, headerValue); + Request const request{httpRequest}; + auto const maybeHeaderValue = request.headerValue(headerName); + ASSERT_TRUE(maybeHeaderValue.has_value()); + EXPECT_EQ(maybeHeaderValue.value(), headerValue); +} + +TEST_F(RequestHeaderValueTest, headerValueNotFound) +{ + http::request httpRequest{http::verb::get, "/some", 11}; + Request const request{httpRequest}; + auto const maybeHeaderValue = request.headerValue(http::field::user_agent); + EXPECT_FALSE(maybeHeaderValue.has_value()); +} + +TEST_F(RequestHeaderValueTest, headerValueWebsocketRequest) +{ + Request::HttpHeaders headers; + http::field const headerName = http::field::user_agent; + std::string const headerValue = "clio"; + headers.set(headerName, headerValue); + Request const request{"websocket message", headers}; + auto const maybeHeaderValue = request.headerValue(headerName); + ASSERT_TRUE(maybeHeaderValue.has_value()); + EXPECT_EQ(maybeHeaderValue.value(), headerValue); +} From 9a2c82a6fde794cf05675f045564da59912b809f Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 7 Oct 2024 16:03:13 +0100 Subject: [PATCH 30/81] Covered Response with tests --- src/web/ng/Response.cpp | 3 +- src/web/ng/Response.hpp | 35 ++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/web/ng/ResponseTests.cpp | 126 ++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 tests/unit/web/ng/ResponseTests.cpp diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp index 73de2137f..136515fe8 100644 --- a/src/web/ng/Response.cpp +++ b/src/web/ng/Response.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -87,7 +88,7 @@ Response::intoHttpResponse() && ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response"); boost::beast::http::response result{httpData_->status, httpData_->version}; - result.set(boost::beast::http::field::server, "clio-server-" + util::build::getClioVersionString()); + result.set(boost::beast::http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString())); result.set(boost::beast::http::field::content_type, asString(httpData_->contentType)); result.keep_alive(httpData_->keepAlive); result.body() = std::move(message_); diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 86c5db420..32bb71c1a 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -31,8 +31,14 @@ #include namespace web::ng { +/** + * @brief Represents an HTTP or Websocket response. + */ class Response { public: + /** + * @brief The data for an HTTP response. + */ struct HttpData { enum class ContentType { APPLICATION_JSON, TEXT_HTML }; @@ -47,12 +53,41 @@ class Response { std::optional httpData_; public: + /** + * @brief Construct a Response from string. Content type will be text/html. + * + * @param status The HTTP status. + * @param message The message to send. + * @param request The request that triggered this response. Used to determine whether the response should contain + * HTTP or WebSocket data. + */ Response(boost::beast::http::status status, std::string message, Request const& request); + + /** + * @brief Construct a Response from JSON object. Content type will be application/json. + * + * @param status The HTTP status. + * @param message The message to send. + * @param request The request that triggered this response. Used to determine whether the response should contain + * HTTP or WebSocket + */ Response(boost::beast::http::status status, boost::json::object const& message, Request const& request); + /** + * @brief Convert the Response to an HTTP response. + * @note The Response must be constructed with an HTTP request. + * + * @return The HTTP response. + */ boost::beast::http::response intoHttpResponse() &&; + /** + * @brief Get the message of the response as a const buffer. + * @note The response must be constructed with a WebSocket request. + * + * @return The message of the response as a const buffer. + */ boost::asio::const_buffer asConstBuffer() const&; }; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index a5cf75e70..8754f37c6 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -132,6 +132,7 @@ target_sources( web/dosguard/IntervalSweepHandlerTests.cpp web/dosguard/WhitelistHandlerTests.cpp web/impl/AdminVerificationTests.cpp + web/ng/ResponseTests.cpp web/ng/RequestTests.cpp web/ng/impl/ServerSslContextTests.cpp web/RPCServerHandlerTests.cpp diff --git a/tests/unit/web/ng/ResponseTests.cpp b/tests/unit/web/ng/ResponseTests.cpp new file mode 100644 index 000000000..f1a2cea09 --- /dev/null +++ b/tests/unit/web/ng/ResponseTests.cpp @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/build/Build.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace web::ng; +namespace http = boost::beast::http; + +struct ResponseDeathTest : testing::Test {}; + +TEST_F(ResponseDeathTest, intoHttpResponseWithoutHttpData) +{ + Request const request{"some messsage", Request::HttpHeaders{}}; + web::ng::Response response{boost::beast::http::status::ok, "message", request}; + EXPECT_DEATH(std::move(response).intoHttpResponse(), ""); +} + +TEST_F(ResponseDeathTest, asConstBufferWithHttpData) +{ + Request const request{http::request{http::verb::get, "/", 11}}; + web::ng::Response response{boost::beast::http::status::ok, "message", request}; + EXPECT_DEATH(response.asConstBuffer(), ""); +} + +struct ResponseTest : testing::Test { + int const httpVersion_ = 11; + http::status const responseStatus_ = http::status::ok; +}; + +TEST_F(ResponseTest, intoHttpResponse) +{ + Request const request{http::request{http::verb::post, "/", httpVersion_, "some message"}}; + std::string const responseMessage = "response message"; + + web::ng::Response response{responseStatus_, responseMessage, request}; + + auto const httpResponse = std::move(response).intoHttpResponse(); + EXPECT_EQ(httpResponse.result(), responseStatus_); + EXPECT_EQ(httpResponse.body(), responseMessage); + EXPECT_EQ(httpResponse.version(), httpVersion_); + EXPECT_EQ(httpResponse.keep_alive(), request.asHttpRequest()->get().keep_alive()); + + ASSERT_GT(httpResponse.count(http::field::content_type), 0); + EXPECT_EQ(httpResponse[http::field::content_type], "text/html"); + + ASSERT_GT(httpResponse.count(http::field::content_type), 0); + EXPECT_EQ(httpResponse[http::field::server], fmt::format("clio-server-{}", util::build::getClioVersionString())); +} + +TEST_F(ResponseTest, intoHttpResponseJson) +{ + Request const request{http::request{http::verb::post, "/", httpVersion_, "some message"}}; + boost::json::object const responseMessage{{"key", "value"}}; + + web::ng::Response response{responseStatus_, responseMessage, request}; + + auto const httpResponse = std::move(response).intoHttpResponse(); + EXPECT_EQ(httpResponse.result(), responseStatus_); + EXPECT_EQ(httpResponse.body(), boost::json::serialize(responseMessage)); + EXPECT_EQ(httpResponse.version(), httpVersion_); + EXPECT_EQ(httpResponse.keep_alive(), request.asHttpRequest()->get().keep_alive()); + + ASSERT_GT(httpResponse.count(http::field::content_type), 0); + EXPECT_EQ(httpResponse[http::field::content_type], "application/json"); + + ASSERT_GT(httpResponse.count(http::field::content_type), 0); + EXPECT_EQ(httpResponse[http::field::server], fmt::format("clio-server-{}", util::build::getClioVersionString())); +} + +TEST_F(ResponseTest, asConstBuffer) +{ + Request const request("some request", Request::HttpHeaders{}); + std::string const responseMessage = "response message"; + web::ng::Response response{responseStatus_, responseMessage, request}; + + auto const buffer = response.asConstBuffer(); + EXPECT_EQ(buffer.size(), responseMessage.size()); + + std::string const messageFromBuffer{static_cast(buffer.data()), buffer.size()}; + EXPECT_EQ(messageFromBuffer, responseMessage); +} + +TEST_F(ResponseTest, asConstBufferJson) +{ + Request const request("some request", Request::HttpHeaders{}); + boost::json::object const responseMessage{{"key", "value"}}; + web::ng::Response response{responseStatus_, responseMessage, request}; + + auto const buffer = response.asConstBuffer(); + EXPECT_EQ(buffer.size(), boost::json::serialize(responseMessage).size()); + + std::string const messageFromBuffer{static_cast(buffer.data()), buffer.size()}; + EXPECT_EQ(messageFromBuffer, boost::json::serialize(responseMessage)); +} From a17d1db7066db6d9573517a186fffc822183d854 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 8 Oct 2024 17:45:04 +0100 Subject: [PATCH 31/81] WIP: Writing tests for HttpConnection --- tests/common/CMakeLists.txt | 11 +- tests/common/util/TestHttpClient.cpp | 175 ++++++++++++ tests/common/util/TestHttpClient.hpp | 58 ++++ tests/common/util/TestHttpServer.cpp | 14 + tests/common/util/TestHttpServer.hpp | 11 + tests/common/util/TestHttpSyncClient.hpp | 270 ------------------ tests/common/util/TestWebSocketClient.cpp | 134 +++++++++ tests/common/util/TestWebSocketClient.hpp | 62 ++++ tests/unit/CMakeLists.txt | 1 + tests/unit/web/ServerTests.cpp | 21 +- .../unit/web/ng/impl/HttpConnectionTests.cpp | 74 +++++ 11 files changed, 549 insertions(+), 282 deletions(-) create mode 100644 tests/common/util/TestHttpClient.cpp create mode 100644 tests/common/util/TestHttpClient.hpp delete mode 100644 tests/common/util/TestHttpSyncClient.hpp create mode 100644 tests/common/util/TestWebSocketClient.cpp create mode 100644 tests/common/util/TestWebSocketClient.hpp create mode 100644 tests/unit/web/ng/impl/HttpConnectionTests.cpp diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index 3b9d66310..566295985 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -1,8 +1,15 @@ add_library(clio_testing_common) target_sources( - clio_testing_common PRIVATE util/StringUtils.cpp util/TestHttpServer.cpp util/TestWsServer.cpp util/TestObject.cpp - util/AssignRandomPort.cpp util/CallWithTimeout.cpp + clio_testing_common + PRIVATE util/AssignRandomPort.cpp + util/CallWithTimeout.cpp + util/StringUtils.cpp + util/TestHttpClient.cpp + util/TestHttpServer.cpp + util/TestObject.cpp + util/TestWebSocketClient.cpp + util/TestWsServer.cpp ) include(deps/gtest) diff --git a/tests/common/util/TestHttpClient.cpp b/tests/common/util/TestHttpClient.cpp new file mode 100644 index 000000000..63ef10947 --- /dev/null +++ b/tests/common/util/TestHttpClient.cpp @@ -0,0 +1,175 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/TestHttpClient.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace http = boost::beast::http; +namespace net = boost::asio; +namespace ssl = boost::asio::ssl; +using tcp = boost::asio::ip::tcp; + +namespace { + +std::string +syncRequest( + std::string const& host, + std::string const& port, + std::string const& body, + std::vector additionalHeaders, + http::verb method, + std::string target = "/" +) +{ + boost::asio::io_context ioc; + + net::ip::tcp::resolver resolver(ioc); + boost::beast::tcp_stream stream(ioc); + + auto const results = resolver.resolve(host, port); + stream.connect(results); + + http::request req{method, "/", 10}; + req.set(http::field::host, host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + for (auto& header : additionalHeaders) { + req.set(header.name, header.value); + } + + req.target(target); + req.body() = std::string(body); + req.prepare_payload(); + http::write(stream, req); + + boost::beast::flat_buffer buffer; + http::response res; + http::read(stream, buffer, res); + + boost::beast::error_code ec; + stream.socket().shutdown(tcp::socket::shutdown_both, ec); + + return res.body(); +} + +} // namespace + +WebHeader::WebHeader(http::field name, std::string value) : name(name), value(std::move(value)) +{ +} + +std::string +HttpSyncClient::post( + std::string const& host, + std::string const& port, + std::string const& body, + std::vector additionalHeaders +) +{ + return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post); +} + +std::string +HttpSyncClient::get( + std::string const& host, + std::string const& port, + std::string const& body, + std::string const& target, + std::vector additionalHeaders +) +{ + return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target); +} + +bool +HttpsSyncClient::verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */) +{ + return true; +} + +std::string +HttpsSyncClient::syncPost(std::string const& host, std::string const& port, std::string const& body) +{ + net::io_context ioc; + boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23); + ctx.set_default_verify_paths(); + ctx.set_verify_mode(ssl::verify_none); + + tcp::resolver resolver(ioc); + boost::beast::ssl_stream stream(ioc, ctx); + +// We can't fix this so have to ignore +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) +#pragma GCC diagnostic pop + { + boost::beast::error_code const ec{static_cast(::ERR_get_error()), net::error::get_ssl_category()}; + throw boost::beast::system_error{ec}; + } + + auto const results = resolver.resolve(host, port); + boost::beast::get_lowest_layer(stream).connect(results); + stream.handshake(ssl::stream_base::client); + + http::request req{http::verb::post, "/", 10}; + req.set(http::field::host, host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + req.body() = std::string(body); + req.prepare_payload(); + http::write(stream, req); + + boost::beast::flat_buffer buffer; + http::response res; + http::read(stream, buffer, res); + + boost::beast::error_code ec; + stream.shutdown(ec); + + return res.body(); +} diff --git a/tests/common/util/TestHttpClient.hpp b/tests/common/util/TestHttpClient.hpp new file mode 100644 index 000000000..bae8a4f08 --- /dev/null +++ b/tests/common/util/TestHttpClient.hpp @@ -0,0 +1,58 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include +#include + +struct WebHeader { + WebHeader(boost::beast::http::field name, std::string value); + + boost::beast::http::field name; + std::string value; +}; + +struct HttpSyncClient { + static std::string + post( + std::string const& host, + std::string const& port, + std::string const& body, + std::vector additionalHeaders = {} + ); + + static std::string + get(std::string const& host, + std::string const& port, + std::string const& body, + std::string const& target, + std::vector additionalHeaders = {}); +}; + +struct HttpsSyncClient { + static bool + verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */); + + static std::string + syncPost(std::string const& host, std::string const& port, std::string const& body); +}; diff --git a/tests/common/util/TestHttpServer.cpp b/tests/common/util/TestHttpServer.cpp index df6cf107c..61b3efc01 100644 --- a/tests/common/util/TestHttpServer.cpp +++ b/tests/common/util/TestHttpServer.cpp @@ -19,6 +19,8 @@ #include "util/TestHttpServer.hpp" +#include "util/Assert.hpp" + #include #include #include @@ -36,6 +38,7 @@ #include #include +#include #include #include @@ -114,6 +117,17 @@ TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string hos acceptor_.listen(asio::socket_base::max_listen_connections); } +std::expected +TestHttpServer::accept(boost::asio::yield_context yield) +{ + boost::beast::error_code errorCode; + tcp::socket socket(this->acceptor_.get_executor()); + acceptor_.async_accept(socket, yield[errorCode]); + if (errorCode) + return std::unexpected{errorCode}; + return socket; +} + void TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler, bool const allowToFail) { diff --git a/tests/common/util/TestHttpServer.hpp b/tests/common/util/TestHttpServer.hpp index 052581e2f..bb0574895 100644 --- a/tests/common/util/TestHttpServer.hpp +++ b/tests/common/util/TestHttpServer.hpp @@ -21,9 +21,11 @@ #include #include +#include #include #include +#include #include #include #include @@ -44,6 +46,15 @@ class TestHttpServer { */ TestHttpServer(boost::asio::io_context& context, std::string host); + /** + * @brief Accept a new connection + * + * @param yield boost::asio::yield_context to use for networking + * @return Either a socket with the new connection or an error code + */ + std::expected + accept(boost::asio::yield_context yield); + /** * @brief Start the server * diff --git a/tests/common/util/TestHttpSyncClient.hpp b/tests/common/util/TestHttpSyncClient.hpp deleted file mode 100644 index 37173c6f5..000000000 --- a/tests/common/util/TestHttpSyncClient.hpp +++ /dev/null @@ -1,270 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2023, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace http = boost::beast::http; -namespace net = boost::asio; -namespace ssl = boost::asio::ssl; -using tcp = boost::asio::ip::tcp; - -struct WebHeader { - WebHeader(http::field name, std::string value) : name(name), value(std::move(value)) - { - } - http::field name; - std::string value; -}; - -struct HttpSyncClient { - static std::string - syncPost( - std::string const& host, - std::string const& port, - std::string const& body, - std::vector additionalHeaders = {} - ) - { - return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post); - } - - static std::string - syncGet( - std::string const& host, - std::string const& port, - std::string const& body, - std::string const& target, - std::vector additionalHeaders = {} - ) - { - return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target); - } - -private: - static std::string - syncRequest( - std::string const& host, - std::string const& port, - std::string const& body, - std::vector additionalHeaders, - http::verb method, - std::string target = "/" - ) - { - boost::asio::io_context ioc; - - net::ip::tcp::resolver resolver(ioc); - boost::beast::tcp_stream stream(ioc); - - auto const results = resolver.resolve(host, port); - stream.connect(results); - - http::request req{method, "/", 10}; - req.set(http::field::host, host); - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - - for (auto& header : additionalHeaders) { - req.set(header.name, header.value); - } - - req.target(target); - req.body() = std::string(body); - req.prepare_payload(); - http::write(stream, req); - - boost::beast::flat_buffer buffer; - http::response res; - http::read(stream, buffer, res); - - boost::beast::error_code ec; - stream.socket().shutdown(tcp::socket::shutdown_both, ec); - - return res.body(); - } -}; - -class WebSocketSyncClient { - net::io_context ioc_; - tcp::resolver resolver_{ioc_}; - boost::beast::websocket::stream ws_{ioc_}; - -public: - void - connect(std::string const& host, std::string const& port, std::vector additionalHeaders = {}) - { - auto const results = resolver_.resolve(host, port); - auto const ep = net::connect(ws_.next_layer(), results); - - // Update the host_ string. This will provide the value of the - // Host HTTP header during the WebSocket handshake. - // See https://tools.ietf.org/html/rfc7230#section-5.4 - auto const hostPort = host + ':' + std::to_string(ep.port()); - - ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders - )](boost::beast::websocket::request_type& req) { - req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); - for (auto const& header : additionalHeaders) { - req.set(header.name, header.value); - } - })); - - ws_.handshake(hostPort, "/"); - } - - void - disconnect() - { - ws_.close(boost::beast::websocket::close_code::normal); - } - - std::string - syncPost(std::string const& body) - { - boost::beast::flat_buffer buffer; - - ws_.write(net::buffer(std::string(body))); - ws_.read(buffer); - - return boost::beast::buffers_to_string(buffer.data()); - } -}; - -struct HttpsSyncClient { - static bool - verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */) - { - return true; - } - - static std::string - syncPost(std::string const& host, std::string const& port, std::string const& body) - { - net::io_context ioc; - boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23); - ctx.set_default_verify_paths(); - ctx.set_verify_mode(ssl::verify_none); - - tcp::resolver resolver(ioc); - boost::beast::ssl_stream stream(ioc, ctx); - -// We can't fix this so have to ignore -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wold-style-cast" - if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) -#pragma GCC diagnostic pop - { - boost::beast::error_code const ec{static_cast(::ERR_get_error()), net::error::get_ssl_category()}; - throw boost::beast::system_error{ec}; - } - - auto const results = resolver.resolve(host, port); - boost::beast::get_lowest_layer(stream).connect(results); - stream.handshake(ssl::stream_base::client); - - http::request req{http::verb::post, "/", 10}; - req.set(http::field::host, host); - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - req.body() = std::string(body); - req.prepare_payload(); - http::write(stream, req); - - boost::beast::flat_buffer buffer; - http::response res; - http::read(stream, buffer, res); - - boost::beast::error_code ec; - stream.shutdown(ec); - - return res.body(); - } -}; - -class WebServerSslSyncClient { - net::io_context ioc_; - std::optional>> ws_; - -public: - void - connect(std::string const& host, std::string const& port) - { - boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23); - ctx.set_default_verify_paths(); - ctx.set_verify_mode(ssl::verify_none); - - tcp::resolver resolver{ioc_}; - ws_.emplace(ioc_, ctx); - - auto const results = resolver.resolve(host, port); - net::connect(ws_->next_layer().next_layer(), results.begin(), results.end()); - ws_->next_layer().handshake(ssl::stream_base::client); - - ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) { - req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); - })); - - ws_->handshake(host, "/"); - } - - void - disconnect() - { - ws_->close(boost::beast::websocket::close_code::normal); - } - - std::string - syncPost(std::string const& body) - { - boost::beast::flat_buffer buffer; - ws_->write(net::buffer(std::string(body))); - ws_->read(buffer); - - return boost::beast::buffers_to_string(buffer.data()); - } -}; diff --git a/tests/common/util/TestWebSocketClient.cpp b/tests/common/util/TestWebSocketClient.cpp new file mode 100644 index 000000000..66e5ed8b5 --- /dev/null +++ b/tests/common/util/TestWebSocketClient.cpp @@ -0,0 +1,134 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/TestWebSocketClient.hpp" + +#include "util/TestHttpClient.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace http = boost::beast::http; +namespace net = boost::asio; +namespace ssl = boost::asio::ssl; +using tcp = boost::asio::ip::tcp; + +void +WebSocketSyncClient::connect(std::string const& host, std::string const& port, std::vector additionalHeaders) +{ + auto const results = resolver_.resolve(host, port); + auto const ep = net::connect(ws_.next_layer(), results); + + // Update the host_ string. This will provide the value of the + // Host HTTP header during the WebSocket handshake. + // See https://tools.ietf.org/html/rfc7230#section-5.4 + auto const hostPort = host + ':' + std::to_string(ep.port()); + + ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders + )](boost::beast::websocket::request_type& req) { + req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); + for (auto const& header : additionalHeaders) { + req.set(header.name, header.value); + } + })); + + ws_.handshake(hostPort, "/"); +} + +void +WebSocketSyncClient::disconnect() +{ + ws_.close(boost::beast::websocket::close_code::normal); +} + +std::string +WebSocketSyncClient::syncPost(std::string const& body) +{ + boost::beast::flat_buffer buffer; + + ws_.write(net::buffer(std::string(body))); + ws_.read(buffer); + + return boost::beast::buffers_to_string(buffer.data()); +} + +void +WebServerSslSyncClient::connect(std::string const& host, std::string const& port) +{ + boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23); + ctx.set_default_verify_paths(); + ctx.set_verify_mode(ssl::verify_none); + + tcp::resolver resolver{ioc_}; + ws_.emplace(ioc_, ctx); + + auto const results = resolver.resolve(host, port); + net::connect(ws_->next_layer().next_layer(), results.begin(), results.end()); + ws_->next_layer().handshake(ssl::stream_base::client); + + ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) { + req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); + })); + + ws_->handshake(host, "/"); +} + +void +WebServerSslSyncClient::disconnect() +{ + ws_->close(boost::beast::websocket::close_code::normal); +} + +std::string +WebServerSslSyncClient::syncPost(std::string const& body) +{ + boost::beast::flat_buffer buffer; + ws_->write(net::buffer(std::string(body))); + ws_->read(buffer); + + return boost::beast::buffers_to_string(buffer.data()); +} diff --git a/tests/common/util/TestWebSocketClient.hpp b/tests/common/util/TestWebSocketClient.hpp new file mode 100644 index 000000000..346cfdcc8 --- /dev/null +++ b/tests/common/util/TestWebSocketClient.hpp @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/TestHttpClient.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +class WebSocketSyncClient { + boost::asio::io_context ioc_; + boost::asio::ip::tcp::resolver resolver_{ioc_}; + boost::beast::websocket::stream ws_{ioc_}; + +public: + void + connect(std::string const& host, std::string const& port, std::vector additionalHeaders = {}); + + void + disconnect(); + + std::string + syncPost(std::string const& body); +}; + +class WebServerSslSyncClient { + boost::asio::io_context ioc_; + std::optional>> ws_; + +public: + void + connect(std::string const& host, std::string const& port); + + void + disconnect(); + + std::string + syncPost(std::string const& body); +}; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 8754f37c6..925d976d6 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -134,6 +134,7 @@ target_sources( web/impl/AdminVerificationTests.cpp web/ng/ResponseTests.cpp web/ng/RequestTests.cpp + web/ng/impl/HttpConnectionTests.cpp web/ng/impl/ServerSslContextTests.cpp web/RPCServerHandlerTests.cpp web/ServerTests.cpp diff --git a/tests/unit/web/ServerTests.cpp b/tests/unit/web/ServerTests.cpp index 53920406d..ce5a822bd 100644 --- a/tests/unit/web/ServerTests.cpp +++ b/tests/unit/web/ServerTests.cpp @@ -20,7 +20,8 @@ #include "util/AssignRandomPort.hpp" #include "util/LoggerFixtures.hpp" #include "util/MockPrometheus.hpp" -#include "util/TestHttpSyncClient.hpp" +#include "util/TestHttpClient.hpp" +#include "util/TestWebSocketClient.hpp" #include "util/TmpFile.hpp" #include "util/config/Config.hpp" #include "util/prometheus/Label.hpp" @@ -213,7 +214,7 @@ TEST_F(WebServerTest, Http) { auto e = std::make_shared(); auto const server = makeServerSync(cfg, ctx, dosGuard, e); - auto const res = HttpSyncClient::syncPost("localhost", port, R"({"Hello":1})"); + auto const res = HttpSyncClient::post("localhost", port, R"({"Hello":1})"); EXPECT_EQ(res, R"({"Hello":1})"); } @@ -232,7 +233,7 @@ TEST_F(WebServerTest, HttpInternalError) { auto e = std::make_shared(); auto const server = makeServerSync(cfg, ctx, dosGuard, e); - auto const res = HttpSyncClient::syncPost("localhost", port, R"({})"); + auto const res = HttpSyncClient::post("localhost", port, R"({})"); EXPECT_EQ( res, R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})" @@ -315,9 +316,9 @@ TEST_F(WebServerTest, HttpRequestOverload) { auto e = std::make_shared(); auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e); - auto res = HttpSyncClient::syncPost("localhost", port, R"({})"); + auto res = HttpSyncClient::post("localhost", port, R"({})"); EXPECT_EQ(res, "{}"); - res = HttpSyncClient::syncPost("localhost", port, R"({})"); + res = HttpSyncClient::post("localhost", port, R"({})"); EXPECT_EQ( res, R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})" @@ -348,7 +349,7 @@ TEST_F(WebServerTest, HttpPayloadOverload) std::string const s100(100, 'a'); auto e = std::make_shared(); auto server = makeServerSync(cfg, ctx, dosGuardOverload, e); - auto const res = HttpSyncClient::syncPost("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100)); + auto const res = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100)); EXPECT_EQ( res, R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})" @@ -499,7 +500,7 @@ TEST_P(WebServerAdminTest, HttpAdminCheck) auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e); std::string const request = "Why hello"; uint32_t const webServerPort = serverConfig.value("server.port"); - auto const res = HttpSyncClient::syncPost("localhost", std::to_string(webServerPort), request, GetParam().headers); + auto const res = HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers); EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse)); } @@ -617,7 +618,7 @@ TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword) uint32_t const webServerPort = tests::util::generateFreePort(); Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))}; auto server = makeServerSync(serverConfig, ctx, dosGuard, e); - auto const res = HttpSyncClient::syncGet("localhost", std::to_string(webServerPort), "", "/metrics"); + auto const res = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics"); EXPECT_EQ(res, "Only admin is allowed to collect metrics"); } @@ -640,7 +641,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled) Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)}; PrometheusService::init(serverConfig); auto server = makeServerSync(serverConfig, ctx, dosGuard, e); - auto const res = HttpSyncClient::syncGet( + auto const res = HttpSyncClient::get( "localhost", std::to_string(webServerPort), "", @@ -661,7 +662,7 @@ TEST_F(WebServerPrometheusTest, validResponse) auto e = std::make_shared(); Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))}; auto server = makeServerSync(serverConfig, ctx, dosGuard, e); - auto const res = HttpSyncClient::syncGet( + auto const res = HttpSyncClient::get( "localhost", std::to_string(webServerPort), "", diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp new file mode 100644 index 000000000..060708b4f --- /dev/null +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -0,0 +1,74 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/AsioContextTestFixture.hpp" +#include "util/Taggable.hpp" +#include "util/TestHttpServer.hpp" +#include "util/config/Config.hpp" +#include "util/requests/RequestBuilder.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/impl/HttpConnection.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace web::ng::impl; + +struct HttpConnectionTests : SyncAsioContextTest { + util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}}; + TestHttpServer httpServer_{ctx, "0.0.0.0"}; + + PlainHttpConnection + acceptConnection(boost::asio::yield_context yield) + { + auto expectedSocket = httpServer_.accept(yield); + [&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }(); + auto ip = expectedSocket->remote_endpoint().address().to_string(); + return PlainHttpConnection{ + std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_ + }; + } + + // PlainHttpConnection + // connect(std::string host, std::string port, boost::asio::yield_context yield) + // { + // } +}; + +TEST_F(HttpConnectionTests, plainConnectionSendReceive) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = + util::requests::RequestBuilder{"localhost", httpServer_.port()}.addData("some data").postPlain(yield); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1}); + ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message(); + EXPECT_EQ(expectedRequest->method(), web::ng::Request::Method::POST); + EXPECT_EQ(expectedRequest->message(), "some data"); + }); +} From 7508e428f0aca37026d6c03482f91c19a81cc159 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 9 Oct 2024 17:57:30 +0100 Subject: [PATCH 32/81] Writing HttpConnection test --- src/web/ng/impl/WsConnection.hpp | 3 +- tests/common/util/TestHttpClient.cpp | 75 +++++++ tests/common/util/TestHttpClient.hpp | 41 ++++ tests/common/util/TestHttpServer.cpp | 5 +- tests/common/util/TestWebSocketClient.cpp | 89 ++++++++ tests/common/util/TestWebSocketClient.hpp | 31 +++ .../unit/web/ng/impl/HttpConnectionTests.cpp | 193 ++++++++++++++++-- 7 files changed, 422 insertions(+), 15 deletions(-) diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index f2532fb42..91fdba7d1 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -132,7 +133,7 @@ class WsConnection : public Connection { if (error) return std::unexpected{error}; - return Request{std::string{static_cast(buffer_.data().data()), buffer_.size()}, initialRequest_}; + return Request{boost::beast::buffers_to_string(buffer_.data()), initialRequest_}; } void diff --git a/tests/common/util/TestHttpClient.cpp b/tests/common/util/TestHttpClient.cpp index 63ef10947..da5c2afe9 100644 --- a/tests/common/util/TestHttpClient.cpp +++ b/tests/common/util/TestHttpClient.cpp @@ -19,9 +19,12 @@ #include "util/TestHttpClient.hpp" +#include "util/Assert.hpp" + #include #include #include +#include #include #include #include @@ -45,7 +48,10 @@ #include #include +#include +#include #include +#include #include #include @@ -173,3 +179,72 @@ HttpsSyncClient::syncPost(std::string const& host, std::string const& port, std: return res.body(); } + +HttpAsyncClient::HttpAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext} +{ +} + +std::optional +HttpAsyncClient::connect( + std::string_view host, + std::string_view port, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout +) +{ + boost::system::error_code error; + boost::asio::ip::tcp::resolver resolver{stream_.get_executor()}; + auto const resolverResults = resolver.resolve(host, port, error); + if (error) + return error; + + ASSERT(resolverResults.size() > 0, "No results from resolver"); + + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + stream_.async_connect(resolverResults.begin()->endpoint(), yield[error]); + if (error) + return error; + return std::nullopt; +} + +std::optional +HttpAsyncClient::send( + boost::beast::http::request request, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout +) +{ + request.prepare_payload(); + boost::system::error_code error; + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + http::async_write(stream_, request, yield[error]); + if (error) + return error; + return std::nullopt; +} + +std::expected, boost::system::error_code> +HttpAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) +{ + boost::system::error_code error; + http::response response; + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + http::async_read(stream_, buffer_, response, yield[error]); + if (error) + return std::unexpected{error}; + return std::move(response); +} + +void +HttpAsyncClient::gracefulShutdown() +{ + boost::system::error_code error; + stream_.socket().shutdown(tcp::socket::shutdown_both, error); +} + +void +HttpAsyncClient::disconnect() +{ + boost::system::error_code error; + stream_.socket().close(error); +} diff --git a/tests/common/util/TestHttpClient.hpp b/tests/common/util/TestHttpClient.hpp index bae8a4f08..2a5e5d4e5 100644 --- a/tests/common/util/TestHttpClient.hpp +++ b/tests/common/util/TestHttpClient.hpp @@ -19,10 +19,20 @@ #pragma once +#include +#include #include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include struct WebHeader { @@ -56,3 +66,34 @@ struct HttpsSyncClient { static std::string syncPost(std::string const& host, std::string const& port, std::string const& body); }; + +class HttpAsyncClient { + boost::beast::tcp_stream stream_; + boost::beast::flat_buffer buffer_; + +public: + HttpAsyncClient(boost::asio::io_context& ioContext); + + std::optional + connect( + std::string_view host, + std::string_view port, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout + ); + + std::optional + send( + boost::beast::http::request request, + boost::asio::yield_context yield, + std::chrono::steady_clock::duration timeout + ); + + std::expected, boost::system::error_code> + receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout); + + void + gracefulShutdown(); + void + disconnect(); +}; diff --git a/tests/common/util/TestHttpServer.cpp b/tests/common/util/TestHttpServer.cpp index 61b3efc01..391d9fc55 100644 --- a/tests/common/util/TestHttpServer.cpp +++ b/tests/common/util/TestHttpServer.cpp @@ -110,7 +110,10 @@ doSession( TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string host) : acceptor_(context) { - boost::asio::ip::tcp::endpoint const endpoint(boost::asio::ip::make_address(host), 0); + boost::asio::ip::tcp::resolver resolver{context}; + auto const results = resolver.resolve(host, "0"); + ASSERT(!results.empty(), "Failed to resolve host"); + boost::asio::ip::tcp::endpoint const& endpoint = results.begin()->endpoint(); acceptor_.open(endpoint.protocol()); acceptor_.set_option(asio::socket_base::reuse_address(true)); acceptor_.bind(endpoint); diff --git a/tests/common/util/TestWebSocketClient.cpp b/tests/common/util/TestWebSocketClient.cpp index 66e5ed8b5..4a7627f4f 100644 --- a/tests/common/util/TestWebSocketClient.cpp +++ b/tests/common/util/TestWebSocketClient.cpp @@ -19,11 +19,14 @@ #include "util/TestWebSocketClient.hpp" +#include "util/Assert.hpp" #include "util/TestHttpClient.hpp" +#include "util/WithTimeout.hpp" #include #include #include +#include #include #include #include @@ -44,9 +47,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -132,3 +137,87 @@ WebServerSslSyncClient::syncPost(std::string const& body) return boost::beast::buffers_to_string(buffer.data()); } + +WebSocketAsyncClient::WebSocketAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext} +{ +} + +std::optional +WebSocketAsyncClient::connect( + boost::asio::yield_context yield, + std::string const& host, + std::string const& port, + std::chrono::steady_clock::duration timeout, + std::vector additionalHeaders +) +{ + auto const results = boost::asio::ip::tcp::resolver{yield.get_executor()}.resolve(host, port); + ASSERT(not results.empty(), "Could not resolve {}:{}", host, port); + + boost::system::error_code error; + boost::beast::get_lowest_layer(stream_).expires_after(timeout); + stream_.next_layer().async_connect(results, yield[error]); + if (error) + return error; + + boost::beast::websocket::stream_base::timeout wsTimeout{}; + stream_.get_option(wsTimeout); + wsTimeout.handshake_timeout = timeout; + stream_.set_option(wsTimeout); + boost::beast::get_lowest_layer(stream_).expires_never(); + + stream_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders + )](boost::beast::websocket::request_type& req) { + for (auto const& header : additionalHeaders) { + req.set(header.name, header.value); + } + })); + stream_.async_handshake(fmt::format("{}:{}", host, port), "/", yield[error]); + if (error) + return error; + + return std::nullopt; +} + +std::optional +WebSocketAsyncClient::send( + boost::asio::yield_context yield, + std::string const& message, + std::chrono::steady_clock::duration timeout +) +{ + auto const error = util::withTimeout( + [this, &message](auto&& cyield) { stream_.async_write(net::buffer(message), cyield); }, yield, timeout + ); + + if (error) + return error; + return std::nullopt; +} + +std::expected +WebSocketAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) +{ + boost::beast::flat_buffer buffer{}; + auto error = + util::withTimeout([this, &buffer](auto&& cyield) { stream_.async_read(buffer, cyield); }, yield, timeout); + if (error) + return std::unexpected{error}; + return boost::beast::buffers_to_string(buffer.data()); +} + +void +WebSocketAsyncClient::gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout) +{ + boost::beast::websocket::stream_base::timeout wsTimeout{}; + stream_.get_option(wsTimeout); + wsTimeout.handshake_timeout = timeout; + stream_.set_option(wsTimeout); + stream_.async_close(boost::beast::websocket::close_code::normal, yield); +} + +void +WebSocketAsyncClient::close() +{ + boost::beast::get_lowest_layer(stream_).close(); +} diff --git a/tests/common/util/TestWebSocketClient.hpp b/tests/common/util/TestWebSocketClient.hpp index 346cfdcc8..175352297 100644 --- a/tests/common/util/TestWebSocketClient.hpp +++ b/tests/common/util/TestWebSocketClient.hpp @@ -23,9 +23,12 @@ #include #include +#include +#include #include #include +#include #include #include #include @@ -46,6 +49,34 @@ class WebSocketSyncClient { syncPost(std::string const& body); }; +class WebSocketAsyncClient { + boost::beast::websocket::stream stream_; + +public: + WebSocketAsyncClient(boost::asio::io_context& ioContext); + + std::optional + connect( + boost::asio::yield_context yield, + std::string const& host, + std::string const& port, + std::chrono::steady_clock::duration timeout, + std::vector additionalHeaders = {} + ); + + std::optional + send(boost::asio::yield_context yield, std::string const& message, std::chrono::steady_clock::duration timeout); + + std::expected + receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout); + + void + gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout); + + void + close(); +}; + class WebServerSslSyncClient { boost::asio::io_context ioc_; std::optional>> ws_; diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp index 060708b4f..4caa50970 100644 --- a/tests/unit/web/ng/impl/HttpConnectionTests.cpp +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -19,26 +19,38 @@ #include "util/AsioContextTestFixture.hpp" #include "util/Taggable.hpp" +#include "util/TestHttpClient.hpp" #include "util/TestHttpServer.hpp" #include "util/config/Config.hpp" -#include "util/requests/RequestBuilder.hpp" #include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" #include "web/ng/impl/HttpConnection.hpp" +#include #include #include #include +#include +#include +#include +#include +#include #include #include #include +#include #include using namespace web::ng::impl; +using namespace web::ng; +namespace http = boost::beast::http; struct HttpConnectionTests : SyncAsioContextTest { util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}}; - TestHttpServer httpServer_{ctx, "0.0.0.0"}; + TestHttpServer httpServer_{ctx, "localhost"}; + HttpAsyncClient httpClient_{ctx}; + http::request request_{http::verb::post, "/some_target", 11, "some data"}; PlainHttpConnection acceptConnection(boost::asio::yield_context yield) @@ -50,25 +62,180 @@ struct HttpConnectionTests : SyncAsioContextTest { std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_ }; } - - // PlainHttpConnection - // connect(std::string host, std::string port, boost::asio::yield_context yield) - // { - // } }; -TEST_F(HttpConnectionTests, plainConnectionSendReceive) +TEST_F(HttpConnectionTests, Receive) { + request_.set(boost::beast::http::field::user_agent, "test_client"); + boost::asio::spawn([this](boost::asio::yield_context yield) { - auto maybeError = - util::requests::RequestBuilder{"localhost", httpServer_.port()}.addData("some data").postPlain(yield); + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); }); runSpawn([this](boost::asio::yield_context yield) { auto connection = acceptConnection(yield); - auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1}); + auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100}); ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message(); - EXPECT_EQ(expectedRequest->method(), web::ng::Request::Method::POST); - EXPECT_EQ(expectedRequest->message(), "some data"); + ASSERT_TRUE(expectedRequest->isHttp()); + + auto const& receivedRequest = expectedRequest.value().asHttpRequest()->get(); + EXPECT_EQ(receivedRequest.method(), request_.method()); + EXPECT_EQ(receivedRequest.target(), request_.target()); + EXPECT_EQ(receivedRequest.body(), request_.body()); + EXPECT_EQ( + receivedRequest.at(boost::beast::http::field::user_agent), + request_.at(boost::beast::http::field::user_agent) + ); + }); +} + +TEST_F(HttpConnectionTests, ReceiveTimeout) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1}); + EXPECT_FALSE(expectedRequest.has_value()); + }); +} + +TEST_F(HttpConnectionTests, ReceiveClientDisconnected) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + httpClient_.disconnect(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1}); + EXPECT_FALSE(expectedRequest.has_value()); + }); +} + +TEST_F(HttpConnectionTests, Send) +{ + Request const request{request_}; + Response const response{http::status::ok, "some response data", request}; + + boost::asio::spawn([this, response = response](boost::asio::yield_context yield) mutable { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }(); + + auto const receivedResponse = expectedResponse.value(); + auto const sentResponse = std::move(response).intoHttpResponse(); + EXPECT_EQ(receivedResponse.result(), sentResponse.result()); + EXPECT_EQ(receivedResponse.body(), sentResponse.body()); + EXPECT_EQ(receivedResponse.version(), request_.version()); + EXPECT_TRUE(receivedResponse.keep_alive()); + }); + + runSpawn([this, &response](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + maybeError = connection.send(response, yield); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); +} + +TEST_F(HttpConnectionTests, SendClientDisconnected) +{ + Response const response{http::status::ok, "some response data", Request{request_}}; + boost::asio::spawn([this, response = response](boost::asio::yield_context yield) mutable { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + httpClient_.disconnect(); + }); + runSpawn([this, &response](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto maybeError = connection.send(response, yield, std::chrono::milliseconds{1}); + size_t counter{1}; + while (not maybeError.has_value() and counter < 100) { + ++counter; + maybeError = connection.send(response, yield, std::chrono::milliseconds{1}); + } + EXPECT_TRUE(maybeError.has_value()); + EXPECT_LT(counter, 100); + }); +} + +TEST_F(HttpConnectionTests, Close) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + size_t counter{0}; + while (not maybeError.has_value() and counter < 100) { + ++counter; + maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{1}); + } + EXPECT_TRUE(maybeError.has_value()); + EXPECT_LT(counter, 100); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + connection.close(yield, std::chrono::milliseconds{1}); + }); +} + +TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{1}); + EXPECT_FALSE(maybeError.has_value()) << maybeError->message(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(result.has_value()) << result.error().message(); }(); + EXPECT_FALSE(result.value()); + }); +} + +TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{1}); + EXPECT_FALSE(result.has_value()); + }); +} + +TEST_F(HttpConnectionTests, IsUpgradeRequested_Success) +{ + boost::asio::spawn([this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{1}); + EXPECT_FALSE(result.has_value()); }); } From 5026c494e447c7aa19cccc014a4e0810bfe8b873 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 11 Oct 2024 12:00:36 +0100 Subject: [PATCH 33/81] Finish HttpConnection tests --- tests/common/util/TestWebSocketClient.cpp | 3 ++- tests/common/util/TestWebSocketClient.hpp | 2 +- .../unit/web/ng/impl/HttpConnectionTests.cpp | 20 ++++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/common/util/TestWebSocketClient.cpp b/tests/common/util/TestWebSocketClient.cpp index 4a7627f4f..eb8473f66 100644 --- a/tests/common/util/TestWebSocketClient.cpp +++ b/tests/common/util/TestWebSocketClient.cpp @@ -144,9 +144,9 @@ WebSocketAsyncClient::WebSocketAsyncClient(boost::asio::io_context& ioContext) : std::optional WebSocketAsyncClient::connect( - boost::asio::yield_context yield, std::string const& host, std::string const& port, + boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout, std::vector additionalHeaders ) @@ -173,6 +173,7 @@ WebSocketAsyncClient::connect( } })); stream_.async_handshake(fmt::format("{}:{}", host, port), "/", yield[error]); + if (error) return error; diff --git a/tests/common/util/TestWebSocketClient.hpp b/tests/common/util/TestWebSocketClient.hpp index 175352297..229cbfc48 100644 --- a/tests/common/util/TestWebSocketClient.hpp +++ b/tests/common/util/TestWebSocketClient.hpp @@ -57,9 +57,9 @@ class WebSocketAsyncClient { std::optional connect( - boost::asio::yield_context yield, std::string const& host, std::string const& port, + boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout, std::vector additionalHeaders = {} ); diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp index 4caa50970..f915e710e 100644 --- a/tests/unit/web/ng/impl/HttpConnectionTests.cpp +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -21,6 +21,7 @@ #include "util/Taggable.hpp" #include "util/TestHttpClient.hpp" #include "util/TestHttpServer.hpp" +#include "util/TestWebSocketClient.hpp" #include "util/config/Config.hpp" #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +42,7 @@ #include #include +#include #include using namespace web::ng::impl; @@ -226,16 +229,23 @@ TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch) }); } -TEST_F(HttpConnectionTests, IsUpgradeRequested_Success) +TEST_F(HttpConnectionTests, Upgrade) { - boost::asio::spawn([this](boost::asio::yield_context yield) { - auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + WebSocketAsyncClient wsClient_{ctx}; + + boost::asio::spawn([this, &wsClient_](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); }); runSpawn([this](boost::asio::yield_context yield) { auto connection = acceptConnection(yield); - auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{1}); - EXPECT_FALSE(result.has_value()); + auto const expectedResult = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedResult.has_value()) << expectedResult.error().message(); }(); + [&]() { ASSERT_TRUE(expectedResult.value()); }(); + + std::optional sslContext; + auto expectedWsConnection = connection.upgrade(sslContext, tagDecoratorFactory_, yield); + [&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }(); }); } From 07b2f85abc04c8618e22b51d1d663b23c0fd3300 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 11 Oct 2024 15:53:18 +0100 Subject: [PATCH 34/81] Add tests for WsConnection --- src/web/ng/Response.cpp | 6 + src/web/ng/Response.hpp | 8 + tests/common/util/TestWebSocketClient.cpp | 3 +- tests/common/util/TestWebSocketClient.hpp | 3 +- tests/unit/CMakeLists.txt | 1 + .../unit/web/ng/impl/HttpConnectionTests.cpp | 31 ++- tests/unit/web/ng/impl/WsConnectionTests.cpp | 198 ++++++++++++++++++ 7 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 tests/unit/web/ng/impl/WsConnectionTests.cpp diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp index 136515fe8..a62318072 100644 --- a/src/web/ng/Response.cpp +++ b/src/web/ng/Response.cpp @@ -82,6 +82,12 @@ Response::Response(boost::beast::http::status status, boost::json::object const& { } +std::string const& +Response::message() const +{ + return message_; +} + boost::beast::http::response Response::intoHttpResponse() && { diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 32bb71c1a..22183836e 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -73,6 +73,14 @@ class Response { */ Response(boost::beast::http::status status, boost::json::object const& message, Request const& request); + /** + * @brief Get the message of the response. + * + * @return The message of the response. + */ + std::string const& + message() const; + /** * @brief Convert the Response to an HTTP response. * @note The Response must be constructed with an HTTP request. diff --git a/tests/common/util/TestWebSocketClient.cpp b/tests/common/util/TestWebSocketClient.cpp index eb8473f66..7ddee4318 100644 --- a/tests/common/util/TestWebSocketClient.cpp +++ b/tests/common/util/TestWebSocketClient.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include @@ -183,7 +184,7 @@ WebSocketAsyncClient::connect( std::optional WebSocketAsyncClient::send( boost::asio::yield_context yield, - std::string const& message, + std::string_view message, std::chrono::steady_clock::duration timeout ) { diff --git a/tests/common/util/TestWebSocketClient.hpp b/tests/common/util/TestWebSocketClient.hpp index 229cbfc48..c8ba064ac 100644 --- a/tests/common/util/TestWebSocketClient.hpp +++ b/tests/common/util/TestWebSocketClient.hpp @@ -31,6 +31,7 @@ #include #include #include +#include #include class WebSocketSyncClient { @@ -65,7 +66,7 @@ class WebSocketAsyncClient { ); std::optional - send(boost::asio::yield_context yield, std::string const& message, std::chrono::steady_clock::duration timeout); + send(boost::asio::yield_context yield, std::string_view message, std::chrono::steady_clock::duration timeout); std::expected receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 925d976d6..db4e53649 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -136,6 +136,7 @@ target_sources( web/ng/RequestTests.cpp web/ng/impl/HttpConnectionTests.cpp web/ng/impl/ServerSslContextTests.cpp + web/ng/impl/WsConnectionTests.cpp web/RPCServerHandlerTests.cpp web/ServerTests.cpp # New Config diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp index f915e710e..b5565dc08 100644 --- a/tests/unit/web/ng/impl/HttpConnectionTests.cpp +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -67,11 +67,24 @@ struct HttpConnectionTests : SyncAsioContextTest { } }; +TEST_F(HttpConnectionTests, wasUpgraded) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + EXPECT_FALSE(connection.wasUpgraded()); + }); +} + TEST_F(HttpConnectionTests, Receive) { request_.set(boost::beast::http::field::user_agent, "test_client"); - boost::asio::spawn([this](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); @@ -98,7 +111,7 @@ TEST_F(HttpConnectionTests, Receive) TEST_F(HttpConnectionTests, ReceiveTimeout) { - boost::asio::spawn([this](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); }); @@ -112,7 +125,7 @@ TEST_F(HttpConnectionTests, ReceiveTimeout) TEST_F(HttpConnectionTests, ReceiveClientDisconnected) { - boost::asio::spawn([this](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); httpClient_.disconnect(); @@ -130,7 +143,7 @@ TEST_F(HttpConnectionTests, Send) Request const request{request_}; Response const response{http::status::ok, "some response data", request}; - boost::asio::spawn([this, response = response](boost::asio::yield_context yield) mutable { + boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); @@ -158,7 +171,7 @@ TEST_F(HttpConnectionTests, Send) TEST_F(HttpConnectionTests, SendClientDisconnected) { Response const response{http::status::ok, "some response data", Request{request_}}; - boost::asio::spawn([this, response = response](boost::asio::yield_context yield) mutable { + boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); httpClient_.disconnect(); @@ -178,7 +191,7 @@ TEST_F(HttpConnectionTests, SendClientDisconnected) TEST_F(HttpConnectionTests, Close) { - boost::asio::spawn([this](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); @@ -199,7 +212,7 @@ TEST_F(HttpConnectionTests, Close) TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest) { - boost::asio::spawn([this](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); @@ -217,7 +230,7 @@ TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest) TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch) { - boost::asio::spawn([this](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); }); @@ -233,7 +246,7 @@ TEST_F(HttpConnectionTests, Upgrade) { WebSocketAsyncClient wsClient_{ctx}; - boost::asio::spawn([this, &wsClient_](boost::asio::yield_context yield) { + boost::asio::spawn(ctx, [this, &wsClient_](boost::asio::yield_context yield) { auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); }); diff --git a/tests/unit/web/ng/impl/WsConnectionTests.cpp b/tests/unit/web/ng/impl/WsConnectionTests.cpp new file mode 100644 index 000000000..8d274bd77 --- /dev/null +++ b/tests/unit/web/ng/impl/WsConnectionTests.cpp @@ -0,0 +1,198 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/AsioContextTestFixture.hpp" +#include "util/Taggable.hpp" +#include "util/TestHttpServer.hpp" +#include "util/TestWebSocketClient.hpp" +#include "util/config/Config.hpp" +#include "web/ng/Error.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" +#include "web/ng/impl/HttpConnection.hpp" +#include "web/ng/impl/WsConnection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace web::ng::impl; +using namespace web::ng; + +struct web_WsConnectionTests : SyncAsioContextTest { + util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}}; + TestHttpServer httpServer_{ctx, "localhost"}; + WebSocketAsyncClient wsClient_{ctx}; + Request request_{"some request", Request::HttpHeaders{}}; + + std::unique_ptr + acceptConnection(boost::asio::yield_context yield) + { + auto expectedSocket = httpServer_.accept(yield); + [&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }(); + auto ip = expectedSocket->remote_endpoint().address().to_string(); + + PlainHttpConnection httpConnection{ + std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_ + }; + + auto expectedTrue = httpConnection.isUpgradeRequested(yield); + [&]() { + ASSERT_TRUE(expectedTrue.has_value()) << expectedTrue.error().message(); + ASSERT_TRUE(expectedTrue.value()) << "Expected upgrade request"; + }(); + + std::optional sslContext; + auto expectedWsConnection = httpConnection.upgrade(sslContext, tagDecoratorFactory_, yield); + [&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }(); + auto connection = std::move(expectedWsConnection).value(); + auto wsConnectionPtr = dynamic_cast(connection.release()); + [&]() { ASSERT_NE(wsConnectionPtr, nullptr) << "Expected PlainWsConnection"; }(); + return std::unique_ptr{wsConnectionPtr}; + } +}; + +TEST_F(web_WsConnectionTests, WasUpgraded) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + }); + runSpawn([this](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + EXPECT_TRUE(wsConnection->wasUpgraded()); + }); +} + +TEST_F(web_WsConnectionTests, Send) +{ + Response const response{boost::beast::http::status::ok, "some response", request_}; + + boost::asio::spawn(ctx, [this, &response](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + auto const expectedMessage = wsClient_.receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedMessage.has_value()) << expectedMessage.error().message(); }(); + EXPECT_EQ(expectedMessage.value(), response.message()); + }); + + runSpawn([this, &response](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + }); +} + +TEST_F(web_WsConnectionTests, SendFailed) +{ + Response const response{boost::beast::http::status::ok, "some response", request_}; + + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + wsClient_.close(); + }); + + runSpawn([this, &response](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + std::optional maybeError; + size_t counter = 0; + while (not maybeError.has_value() and counter < 100) { + maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{1}); + ++counter; + } + EXPECT_TRUE(maybeError.has_value()); + EXPECT_LT(counter, 100); + }); +} + +TEST_F(web_WsConnectionTests, Receive) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100}); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }(); + EXPECT_EQ(maybeRequest->message(), request_.message()); + }); +} + +TEST_F(web_WsConnectionTests, ReceiveTimeout) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{1}); + EXPECT_FALSE(maybeRequest.has_value()); + EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::timed_out); + }); +} + +TEST_F(web_WsConnectionTests, ReceiveFailed) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + wsClient_.close(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100}); + EXPECT_FALSE(maybeRequest.has_value()); + EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::eof); + }); +} + +TEST_F(web_WsConnectionTests, Close) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + auto const maybeMessage = wsClient_.receive(yield, std::chrono::milliseconds{100}); + EXPECT_FALSE(maybeMessage.has_value()); + EXPECT_THAT(maybeMessage.error().message(), testing::HasSubstr("was gracefully closed")); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + wsConnection->close(yield, std::chrono::milliseconds{100}); + }); +} From c6223a4d40a5f402983467292344e569816d79e6 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 14 Oct 2024 17:57:39 +0100 Subject: [PATCH 35/81] Writing tests for ConnectionHandler --- src/web/Server.hpp | 12 +- src/web/ng/Connection.cpp | 10 + src/web/ng/Connection.hpp | 10 +- src/web/ng/Request.hpp | 3 + src/web/ng/Server.cpp | 5 +- src/web/ng/impl/ConnectionHandler.cpp | 45 ++-- src/web/ng/impl/ConnectionHandler.hpp | 34 +-- tests/common/web/ng/MockConnection.hpp | 62 +++++ tests/unit/CMakeLists.txt | 1 + .../web/ng/impl/ConnectionHandlerTests.cpp | 249 ++++++++++++++++++ 10 files changed, 361 insertions(+), 70 deletions(-) create mode 100644 tests/common/web/ng/MockConnection.hpp create mode 100644 tests/unit/web/ng/impl/ConnectionHandlerTests.cpp diff --git a/src/web/Server.hpp b/src/web/Server.hpp index ec4556fb6..beb27ac6d 100644 --- a/src/web/Server.hpp +++ b/src/web/Server.hpp @@ -70,10 +70,8 @@ namespace web { * @tparam HandlerType The executor to handle the requests */ template < - template - class PlainSessionType, - template - class SslSessionType, + template class PlainSessionType, + template class SslSessionType, SomeServerHandler HandlerType> class Detector : public std::enable_shared_from_this> { using std::enable_shared_from_this>::shared_from_this; @@ -192,10 +190,8 @@ class Detector : public std::enable_shared_from_this - class PlainSessionType, - template - class SslSessionType, + template class PlainSessionType, + template class SslSessionType, SomeServerHandler HandlerType> class Server : public std::enable_shared_from_this> { using std::enable_shared_from_this>::shared_from_this; diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index d9a32cec3..a997f568c 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -50,10 +50,20 @@ Connection::Connection( { } +ConnectionContext +Connection::context() const +{ + return ConnectionContext{*this}; +} + std::string const& Connection::ip() const { return ip_; } +ConnectionContext::ConnectionContext(Connection const& connection) : connection_{connection} +{ +} + } // namespace web::ng diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index e23ed232e..edd6e7eb1 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -82,16 +82,10 @@ class Connection : public util::Taggable { using ConnectionPtr = std::unique_ptr; class ConnectionContext { - std::reference_wrapper connection_; + std::reference_wrapper connection_; public: - explicit ConnectionContext(Connection& connection); - - ConnectionContext(ConnectionContext&&) = delete; - ConnectionContext(ConnectionContext const&) = default; - - bool - isAdmin() const; + explicit ConnectionContext(Connection const& connection); }; using ConnectionPtr = std::unique_ptr; diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 01a5639ce..cf1f03e2b 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -66,6 +66,9 @@ class Request { */ Request(std::string request, HttpHeaders const& headers); + bool + operator==(Request const& other) const; + /** * @brief Method of the request. * @note WEBSOCKET is not a real method, it is used to distinguish WebSocket requests from HTTP requests. diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 91be56a75..6afb636d7 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -286,13 +286,12 @@ make_Server(util::Config const& config, boost::asio::io_context& context) if (not expectedSslContext) return std::unexpected{std::move(expectedSslContext).error()}; - impl::ConnectionHandler::ProcessingStrategy processingStrategy{impl::ConnectionHandler::ProcessingStrategy::Parallel - }; + impl::ConnectionHandler::ProcessingPolicy processingStrategy{impl::ConnectionHandler::ProcessingPolicy::Parallel}; std::optional parallelRequestLimit; auto const processingStrategyStr = serverConfig.valueOr("processing_strategy", "parallel"); if (processingStrategyStr == "sequent") { - processingStrategy = impl::ConnectionHandler::ProcessingStrategy::Sequent; + processingStrategy = impl::ConnectionHandler::ProcessingPolicy::Sequential; } else if (processingStrategyStr == "parallel") { parallelRequestLimit = serverConfig.maybeValue("parallel_requests_limit"); } else { diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index ee5dba3cc..58b53cbc9 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -95,6 +95,11 @@ ConnectionHandler::StringHash::operator()(std::string const& str) const return hash_type{}(str); } +ConnectionHandler::ConnectionHandler(ProcessingPolicy processingPolicy, std::optional maxParallelRequests) + : processingPolicy_{processingPolicy}, maxParallelRequests_{maxParallelRequests} +{ +} + void ConnectionHandler::onGet(std::string const& target, MessageHandler handler) { @@ -116,41 +121,29 @@ ConnectionHandler::onWs(MessageHandler handler) void ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::yield_context yield) { - auto& connection = insertConnection(std::move(connectionPtr)); + auto& connectionRef = *connectionPtr; + auto signalConnection = onStop_.connect([&connectionRef, yield]() { connectionRef.close(yield); }); - bool shouldCloseGracefully{false}; + bool shouldCloseGracefully = false; - switch (processingStrategy_) { - case ProcessingStrategy::Sequent: - shouldCloseGracefully = sequentRequestResponseLoop(connection, yield); + switch (processingPolicy_) { + case ProcessingPolicy::Sequential: + shouldCloseGracefully = sequentRequestResponseLoop(connectionRef, yield); break; - case ProcessingStrategy::Parallel: - shouldCloseGracefully = parallelRequestResponseLoop(connection, yield); + case ProcessingPolicy::Parallel: + shouldCloseGracefully = parallelRequestResponseLoop(connectionRef, yield); break; } if (shouldCloseGracefully) - connection.close(yield); + connectionRef.close(yield); - removeConnection(connection); - // connection reference is not valid anymore -} - -Connection& -ConnectionHandler::insertConnection(ConnectionPtr connection) -{ - auto connectionsMap = connections_->lock(); - auto [it, inserted] = connectionsMap->emplace(connection->id(), std::move(connection)); - ASSERT(inserted, "Connection with id {} already exists", it->second->id()); - return *it->second; + signalConnection.disconnect(); } void -ConnectionHandler::removeConnection(Connection const& connection) +ConnectionHandler::stop() { - auto connectionsMap = connections_->lock(); - auto it = connectionsMap->find(connection.id()); - ASSERT(it != connectionsMap->end(), "Connection with id {} does not exist", connection.id()); - connectionsMap->erase(it); + onStop_(); } bool @@ -239,7 +232,7 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as &ongoingRequestsCounter, &connection, request = std::move(expectedRequest).value()](boost::asio::yield_context innerYield) mutable { - auto maybeCloseConnectionGracefully = processRequest(connection, std::move(request), innerYield); + auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield); if (maybeCloseConnectionGracefully.has_value()) { if (closeConnectionGracefully.has_value()) { // Close connection gracefully only if both are true. If at least one is false then @@ -258,7 +251,7 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as } std::optional -ConnectionHandler::processRequest(Connection& connection, Request&& request, boost::asio::yield_context yield) +ConnectionHandler::processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield) { auto response = handleRequest(connection.context(), request, yield); diff --git a/src/web/ng/impl/ConnectionHandler.hpp b/src/web/ng/impl/ConnectionHandler.hpp index 8b21a4af2..b5f8a9a06 100644 --- a/src/web/ng/impl/ConnectionHandler.hpp +++ b/src/web/ng/impl/ConnectionHandler.hpp @@ -19,7 +19,6 @@ #pragma once -#include "util/Mutex.hpp" #include "util/log/Logger.hpp" #include "web/ng/Connection.hpp" #include "web/ng/Error.hpp" @@ -28,10 +27,11 @@ #include "web/ng/Response.hpp" #include +#include +#include #include #include -#include #include #include #include @@ -41,7 +41,7 @@ namespace web::ng::impl { class ConnectionHandler { public: - enum class ProcessingStrategy { Sequent, Parallel }; + enum class ProcessingPolicy { Sequential, Parallel }; struct StringHash { using hash_type = std::hash; @@ -61,18 +61,17 @@ class ConnectionHandler { util::Logger log_{"WebServer"}; util::Logger perfLog_{"Performance"}; - ProcessingStrategy processingStrategy_; + ProcessingPolicy processingPolicy_; std::optional maxParallelRequests_; TargetToHandlerMap getHandlers_; TargetToHandlerMap postHandlers_; std::optional wsHandler_; - using ConnectionsMap = std::unordered_map; - std::unique_ptr> connections_{std::make_unique>()}; + boost::signals2::signal onStop_; public: - ConnectionHandler(ProcessingStrategy processingStrategy, std::optional maxParallelRequests_); + ConnectionHandler(ProcessingPolicy processingPolicy, std::optional maxParallelRequests); void onGet(std::string const& target, MessageHandler handler); @@ -86,25 +85,10 @@ class ConnectionHandler { void processConnection(ConnectionPtr connection, boost::asio::yield_context yield); -private: - /** - * @brief Insert a connection into the connections map. - * - * @param connection The connection to insert. - * @return A reference to the inserted connection - */ - Connection& - insertConnection(ConnectionPtr connection); - - /** - * @brief Remove a connection from the connections map. - * @note After this call, the connection reference is no longer valid. - * - * @param connection The connection to remove. - */ void - removeConnection(Connection const& connection); + stop(); +private: /** * @brief Handle an error. * @@ -129,7 +113,7 @@ class ConnectionHandler { parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield); std::optional - processRequest(Connection& connection, Request&& request, boost::asio::yield_context yield); + processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield); /** * @brief Handle a request. diff --git a/tests/common/web/ng/MockConnection.hpp b/tests/common/web/ng/MockConnection.hpp new file mode 100644 index 000000000..35caad6ae --- /dev/null +++ b/tests/common/web/ng/MockConnection.hpp @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include +#include + +#include +#include +#include + +struct MockConnectionImpl : web::ng::Connection { + using web::ng::Connection::Connection; + + MOCK_METHOD(bool, wasUpgraded, (), (const, override)); + + using SendReturnType = std::optional; + MOCK_METHOD( + SendReturnType, + send, + (web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration), + (override) + ); + + using ReceiveReturnType = std::expected; + MOCK_METHOD( + ReceiveReturnType, + receive, + (boost::asio::yield_context, std::chrono::steady_clock::duration), + (override) + ); + + MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration)); +}; + +using MockConnection = testing::NiceMock; +using MockConnectionPtr = std::unique_ptr>; + +using StrictMockConnection = testing::StrictMock; +using StrictMockConnectionPtr = std::unique_ptr>; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index db4e53649..a34e9d342 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -134,6 +134,7 @@ target_sources( web/impl/AdminVerificationTests.cpp web/ng/ResponseTests.cpp web/ng/RequestTests.cpp + web/ng/impl/ConnectionHandlerTests.cpp web/ng/impl/HttpConnectionTests.cpp web/ng/impl/ServerSslContextTests.cpp web/ng/impl/WsConnectionTests.cpp diff --git a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp new file mode 100644 index 000000000..13cf49316 --- /dev/null +++ b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp @@ -0,0 +1,249 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/AsioContextTestFixture.hpp" +#include "util/Taggable.hpp" +#include "util/UnsupportedType.hpp" +#include "util/config/Config.hpp" +#include "web/ng/Connection.hpp" +#include "web/ng/Error.hpp" +#include "web/ng/MockConnection.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" +#include "web/ng/impl/ConnectionHandler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace web::ng::impl; +using namespace web::ng; +using testing::Return; +namespace beast = boost::beast; +namespace http = boost::beast::http; +namespace websocket = boost::beast::websocket; + +struct ConnectionHandlerTest : SyncAsioContextTest { + ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy policy, std::optional maxParallelConnections) + : connectionHandler_{policy, maxParallelConnections} + { + } + + template + static std::unexpected + makeError(BoostErrorType error) + { + if constexpr (std::same_as) { + return std::unexpected{http::make_error_code(error)}; + } else if constexpr (std::same_as) { + return std::unexpected{websocket::make_error_code(error)}; + } else if constexpr (std::same_as || + std::same_as || + std::same_as || + std::same_as) { + return std::unexpected{boost::asio::error::make_error_code(error)}; + } else { + static_assert(util::Unsupported, "Wrong error type"); + } + } + + template + static std::expected + makeRequest(Args&&... args) + { + return Request{std::forward(args)...}; + } + + ConnectionHandler connectionHandler_; + + util::TagDecoratorFactory tagDecoratorFactory_{util::Config(boost::json::object{{"log_tag_style", "uint"}})}; + StrictMockConnectionPtr mockConnection_ = + std::make_unique("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_); +}; + +struct ConnectionHandlerSequentialProcessingTest : ConnectionHandlerTest { + ConnectionHandlerSequentialProcessingTest() + : ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt) + { + } +}; + +TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError) +{ + EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream))); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} +TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection) +{ + EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out))); + EXPECT_CALL(*mockConnection_, close); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_NoHandler_Send) +{ + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(Return(makeRequest("some_request", Request::HttpHeaders{}))) + .WillOnce(Return(makeError(websocket::error::closed))); + + EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), "WebSocket is not supported by this server"); + return std::nullopt; + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send) +{ + std::string const target = "/some/target"; + + std::string const requestMessage = "some message"; + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(Return(makeRequest(http::request{http::verb::get, target, 11, requestMessage}))) + .WillOnce(Return(makeError(http::error::end_of_stream))); + + EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), "Bad target"); + auto const httpResponse = std::move(response).intoHttpResponse(); + EXPECT_EQ(httpResponse.result(), http::status::bad_request); + EXPECT_EQ(httpResponse.version(), 11); + return std::nullopt; + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send) +{ + testing::StrictMock> + wsHandlerMock; + connectionHandler_.onWs(wsHandlerMock.AsStdFunction()); + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{}))) + .WillOnce(Return(makeError(websocket::error::closed))); + + EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) { + EXPECT_EQ(request.message(), requestMessage); + return Response(http::status::ok, responseMessage, request); + }); + + EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), responseMessage); + return std::nullopt; + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop) +{ + std::string const target = "/some/target"; + testing::StrictMock> + postHandlerMock; + connectionHandler_.onPost(target, postHandlerMock.AsStdFunction()); + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + + auto const returnRequest = + Return(makeRequest(http::request{http::verb::post, target, 11, requestMessage})); + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(Return(makeError(http::error::partial_message))); + + EXPECT_CALL(postHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) { + EXPECT_EQ(request.message(), requestMessage); + return Response(http::status::ok, responseMessage, request); + }); + + EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), responseMessage); + return std::nullopt; + }); + + EXPECT_CALL(*mockConnection_, close); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError) +{ + std::string const target = "/some/target"; + testing::StrictMock> + getHandlerMock; + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + + connectionHandler_.onGet(target, getHandlerMock.AsStdFunction()); + + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(Return(makeRequest(http::request{http::verb::get, target, 11, requestMessage}))); + + EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) { + EXPECT_EQ(request.message(), requestMessage); + return Response(http::status::ok, responseMessage, request); + }); + + EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), responseMessage); + return makeError(http::error::end_of_stream).error(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} From 53ccc6680309c22883af146f42a923327504c100 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 15 Oct 2024 17:59:42 +0100 Subject: [PATCH 36/81] WIP --- src/web/ng/impl/ConnectionHandler.cpp | 9 +- .../web/ng/impl/ConnectionHandlerTests.cpp | 250 ++++++++++++++++++ 2 files changed, 257 insertions(+), 2 deletions(-) diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index 58b53cbc9..84f67841c 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -27,9 +27,12 @@ #include "web/ng/Request.hpp" #include "web/ng/Response.hpp" +#include +#include #include #include #include +#include #include #include #include @@ -39,6 +42,7 @@ #include #include #include +#include #include namespace web::ng::impl { @@ -211,15 +215,16 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as while (not closeConnectionGracefully.has_value()) { auto expectedRequest = connection.receive(yield); - if (not expectedRequest) + if (not expectedRequest) { return handleError(expectedRequest.error(), connection); + } ++ongoingRequestsCounter; if (maxParallelRequests_.has_value() && ongoingRequestsCounter > *maxParallelRequests_) { connection.send( Response{ boost::beast::http::status::too_many_requests, - "Too many request for one session", + "Too many requests for one session", expectedRequest.value() }, yield diff --git a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp index 13cf49316..533b72460 100644 --- a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp +++ b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp @@ -28,8 +28,11 @@ #include "web/ng/Response.hpp" #include "web/ng/impl/ConnectionHandler.hpp" +#include +#include #include #include +#include #include #include #include @@ -42,10 +45,13 @@ #include #include +#include #include #include +#include #include #include +#include #include #include @@ -109,6 +115,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError) connectionHandler_.processConnection(std::move(mockConnection_), yield); }); } + TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection) { EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out))); @@ -247,3 +254,246 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError) connectionHandler_.processConnection(std::move(mockConnection_), yield); }); } + +TEST_F(ConnectionHandlerSequentialProcessingTest, Stop) +{ + testing::StrictMock> + wsHandlerMock; + connectionHandler_.onWs(wsHandlerMock.AsStdFunction()); + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + bool connectionClosed = false; + EXPECT_CALL(*mockConnection_, receive) + .Times(4) + .WillRepeatedly([&](auto&&, auto&&) -> std::expected { + if (connectionClosed) { + return makeError(websocket::error::closed); + } + return makeRequest(requestMessage, Request::HttpHeaders{}); + }); + + EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) { + EXPECT_EQ(request.message(), requestMessage); + return Response(http::status::ok, responseMessage, request); + }); + + size_t numCalls = 0; + EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), responseMessage); + + ++numCalls; + if (numCalls == 3) + connectionHandler_.stop(); + + return std::nullopt; + }); + + EXPECT_CALL(*mockConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest { + static size_t const maxParallelRequests = 3; + ConnectionHandlerParallelProcessingTest() + : ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Parallel, maxParallelRequests) + { + } + + static void + asyncSleep(boost::asio::yield_context yield, std::chrono::steady_clock::duration duration) + { + boost::asio::steady_timer timer{yield.get_executor()}; + std::cout << "sleep starts" << std::endl; + timer.expires_after(duration); + timer.async_wait(yield); + std::cout << "sleep ends" << std::endl; + } +}; + +TEST_F(ConnectionHandlerParallelProcessingTest, ReceiveError) +{ + EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream))); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send) +{ + testing::StrictMock> + wsHandlerMock; + connectionHandler_.onWs(wsHandlerMock.AsStdFunction()); + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{}))) + .WillOnce(Return(makeError(websocket::error::closed))); + + EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) { + EXPECT_EQ(request.message(), requestMessage); + return Response(http::status::ok, responseMessage, request); + }); + + EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), responseMessage); + return std::nullopt; + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop) +{ + testing::StrictMock> + wsHandlerMock; + connectionHandler_.onWs(wsHandlerMock.AsStdFunction()); + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + + auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); }; + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(Return(makeError(websocket::error::closed))); + + EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, auto&&, auto&&) { + EXPECT_EQ(request.message(), requestMessage); + return Response(http::status::ok, responseMessage, request); + }); + + EXPECT_CALL(*mockConnection_, send).Times(2).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), responseMessage); + return std::nullopt; + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooManyRequest) +{ + testing::StrictMock> + wsHandlerMock; + connectionHandler_.onWs(wsHandlerMock.AsStdFunction()); + + std::string const requestMessage = "some message"; + std::string const responseMessage = "some response"; + + auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); }; + testing::Sequence sequence; + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(returnRequest) + .WillOnce(Return(makeError(websocket::error::closed))); + + EXPECT_CALL(wsHandlerMock, Call) + .Times(3) + .WillRepeatedly([&](Request const& request, auto&&, boost::asio::yield_context yield) { + EXPECT_EQ(request.message(), requestMessage); + asyncSleep(yield, std::chrono::milliseconds{3}); + std::cout << "After sleep" << std::endl; + return Response(http::status::ok, responseMessage, request); + }); + + EXPECT_CALL( + *mockConnection_, + send( + testing::ResultOf( + [](Response response) { + std::cout << "1 " << response.message() << std::endl; + return response.message(); + }, + responseMessage + ), + testing::_, + testing::_ + ) + ) + .Times(3) + .WillRepeatedly(Return(std::nullopt)); + + EXPECT_CALL( + *mockConnection_, + send( + testing::ResultOf( + [](Response response) { + std::cout << "2 " << response.message() << std::endl; + return response.message(); + }, + "Too many requests for one session" + ), + testing::_, + testing::_ + ) + ) + .Times(2) + .WillRepeatedly(Return(std::nullopt)); + + // .WillRepeatedly([&](Response response, auto&&, auto&&) { + // if (handlerCallCounter < 3) { + // EXPECT_EQ(response.message(), responseMessage); + // return std::nullopt; + // } + // EXPECT_EQ(response.message(), "Too many requests for one session"); + // return std::nullopt; + // }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + +TEST_F(ConnectionHandlerParallelProcessingTest, myTest) +{ + runSpawn([](boost::asio::yield_context yield) { + boost::asio::steady_timer sync{yield.get_executor(), boost::asio::steady_timer::clock_type::duration::max()}; + + int childNumber = 0; + boost::asio::spawn(yield, [&](boost::asio::yield_context innerYield) { + ++childNumber; + + std::cout << "I'm child coroutine" << std::endl; + boost::asio::steady_timer t{innerYield.get_executor()}; + t.expires_after(std::chrono::milliseconds{20}); + t.async_wait(innerYield); + std::cout << "Child 1 done" << std::endl; + + --childNumber; + if (childNumber == 0) + sync.cancel(); + }); + + std::cout << "Parent: between" << std::endl; + + boost::asio::spawn(yield, [&](auto&& innerYield) { + ++childNumber; + + std::cout << "I'm child coroutine 2" << std::endl; + boost::asio::steady_timer t{innerYield.get_executor()}; + t.expires_after(std::chrono::milliseconds{30}); + t.async_wait(innerYield); + std::cout << "Child 2 done" << std::endl; + + --childNumber; + if (childNumber == 0) + sync.cancel(); + }); + + boost::system::error_code error; + sync.async_wait(yield[error]); + std::cout << "Parent done" << std::endl; + }); +} From eb98537ff5a317f0a2e3465c9cb5339f5e68d58a Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 16 Oct 2024 15:20:07 +0100 Subject: [PATCH 37/81] Add CoroutineGroup --- src/util/CMakeLists.txt | 1 + src/util/CoroutineGroup.cpp | 63 ++++++++++++++++++ src/util/CoroutineGroup.hpp | 65 ++++++++++++++++++ src/web/ng/impl/ConnectionHandler.cpp | 1 - tests/unit/CMakeLists.txt | 1 + tests/unit/util/CoroutineGroupTests.cpp | 88 +++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/util/CoroutineGroup.cpp create mode 100644 src/util/CoroutineGroup.hpp create mode 100644 tests/unit/util/CoroutineGroupTests.cpp diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 910542ceb..8c28fdf95 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources( clio_util PRIVATE build/Build.cpp config/Config.cpp + CoroutineGroup.cpp log/Logger.cpp prometheus/Http.cpp prometheus/Label.cpp diff --git a/src/util/CoroutineGroup.cpp b/src/util/CoroutineGroup.cpp new file mode 100644 index 000000000..71d40903a --- /dev/null +++ b/src/util/CoroutineGroup.cpp @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/CoroutineGroup.hpp" + +#include "util/Assert.hpp" + +#include +#include + +#include +#include + +namespace util { + +CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield) + : timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()} +{ +} + +void +CoroutineGroup::spawn(boost::asio::yield_context yield, std::function fn) +{ + ASSERT(not finished_, "Can't spawn a coroutine on finished group"); + + ++childrenCounter_; + boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) { + fn(yield); + --childrenCounter_; + if (childrenCounter_ == 0) { + timer_.cancel(); + finished_ = true; + } + }); +} + +void +CoroutineGroup::asyncWait(boost::asio::yield_context yield) +{ + if (finished_) + return; + + boost::system::error_code error; + timer_.async_wait(yield[error]); +} + +} // namespace util diff --git a/src/util/CoroutineGroup.hpp b/src/util/CoroutineGroup.hpp new file mode 100644 index 000000000..eb3220c1c --- /dev/null +++ b/src/util/CoroutineGroup.hpp @@ -0,0 +1,65 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include + +namespace util { + +/** + * @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and + * wait for all of them to finish. + */ +class CoroutineGroup { + boost::asio::steady_timer timer_; + int childrenCounter_{0}; + bool finished_ = false; + +public: + /** + * @brief Construct a new Coroutine Group object + * + * @param yield The yield context to use for the internal timer + */ + CoroutineGroup(boost::asio::yield_context yield); + + /** + * @brief Spawn a new coroutine in the group + * + * @param yield The yield context to use for the coroutine (it should be the same as the one used in the + * constructor) + * @param fn The function to execute + */ + void + spawn(boost::asio::yield_context yield, std::function fn); + + /** + * @brief Wait for all the coroutines in the group to finish + * + * @param yield The yield context to use for the internal timer + */ + void + asyncWait(boost::asio::yield_context yield); +}; + +} // namespace util diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index 84f67841c..55fab3f4c 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -42,7 +42,6 @@ #include #include #include -#include #include namespace web::ng::impl { diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index a34e9d342..10025fb59 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -104,6 +104,7 @@ target_sources( util/async/AnyStrandTests.cpp util/async/AsyncExecutionContextTests.cpp util/BatchingTests.cpp + util/CoroutineGroupTests.cpp util/LedgerUtilsTests.cpp # Prometheus support util/prometheus/BoolTests.cpp diff --git a/tests/unit/util/CoroutineGroupTests.cpp b/tests/unit/util/CoroutineGroupTests.cpp new file mode 100644 index 000000000..a0789b5fa --- /dev/null +++ b/tests/unit/util/CoroutineGroupTests.cpp @@ -0,0 +1,88 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/AsioContextTestFixture.hpp" +#include "util/CoroutineGroup.hpp" + +#include +#include +#include +#include + +#include + +using namespace util; + +struct CoroutineGroupTests : SyncAsioContextTest { + testing::StrictMock> callback1_; + testing::StrictMock> callback2_; + testing::StrictMock> callback3_; +}; + +TEST_F(CoroutineGroupTests, spawnWait) +{ + testing::Sequence sequence; + EXPECT_CALL(callback1_, Call).InSequence(sequence); + EXPECT_CALL(callback2_, Call).InSequence(sequence); + EXPECT_CALL(callback3_, Call).InSequence(sequence); + + runSpawn([this](boost::asio::yield_context yield) { + CoroutineGroup group{yield}; + group.spawn(yield, [&](boost::asio::yield_context yield) { + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; + timer.async_wait(yield); + callback1_.Call(); + }); + group.spawn(yield, [&](boost::asio::yield_context yield) { + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}}; + timer.async_wait(yield); + callback2_.Call(); + }); + group.asyncWait(yield); + callback3_.Call(); + }); +} + +TEST_F(CoroutineGroupTests, childCoroutinesFinishBeforeWait) +{ + testing::Sequence sequence; + EXPECT_CALL(callback2_, Call).InSequence(sequence); + EXPECT_CALL(callback1_, Call).InSequence(sequence); + EXPECT_CALL(callback3_, Call).InSequence(sequence); + + runSpawn([this](boost::asio::yield_context yield) { + CoroutineGroup group{yield}; + group.spawn(yield, [&](boost::asio::yield_context yield) { + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}}; + timer.async_wait(yield); + callback1_.Call(); + }); + group.spawn(yield, [&](boost::asio::yield_context yield) { + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; + timer.async_wait(yield); + callback2_.Call(); + }); + + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{3}}; + timer.async_wait(yield); + + group.asyncWait(yield); + callback3_.Call(); + }); +} From 0c9cb3fcb3cab3e5334d6b8c9065846684b316a1 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 16 Oct 2024 16:33:46 +0100 Subject: [PATCH 38/81] Use CoroutineGroup in ConnectionHandler --- src/util/CoroutineGroup.cpp | 17 ++--- src/util/CoroutineGroup.hpp | 10 ++- src/web/ng/impl/ConnectionHandler.cpp | 41 +++++------ tests/unit/util/CoroutineGroupTests.cpp | 52 ++++++++++++++ .../web/ng/impl/ConnectionHandlerTests.cpp | 70 +------------------ 5 files changed, 91 insertions(+), 99 deletions(-) diff --git a/src/util/CoroutineGroup.cpp b/src/util/CoroutineGroup.cpp index 71d40903a..e068bf528 100644 --- a/src/util/CoroutineGroup.cpp +++ b/src/util/CoroutineGroup.cpp @@ -19,11 +19,10 @@ #include "util/CoroutineGroup.hpp" -#include "util/Assert.hpp" - #include #include +#include #include #include @@ -37,27 +36,29 @@ CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield) void CoroutineGroup::spawn(boost::asio::yield_context yield, std::function fn) { - ASSERT(not finished_, "Can't spawn a coroutine on finished group"); - ++childrenCounter_; boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) { fn(yield); --childrenCounter_; - if (childrenCounter_ == 0) { + if (childrenCounter_ == 0) timer_.cancel(); - finished_ = true; - } }); } void CoroutineGroup::asyncWait(boost::asio::yield_context yield) { - if (finished_) + if (childrenCounter_ == 0) return; boost::system::error_code error; timer_.async_wait(yield[error]); } +size_t +CoroutineGroup::size() const +{ + return childrenCounter_; +} + } // namespace util diff --git a/src/util/CoroutineGroup.hpp b/src/util/CoroutineGroup.hpp index eb3220c1c..370381c1e 100644 --- a/src/util/CoroutineGroup.hpp +++ b/src/util/CoroutineGroup.hpp @@ -22,6 +22,7 @@ #include #include +#include #include namespace util { @@ -33,7 +34,6 @@ namespace util { class CoroutineGroup { boost::asio::steady_timer timer_; int childrenCounter_{0}; - bool finished_ = false; public: /** @@ -60,6 +60,14 @@ class CoroutineGroup { */ void asyncWait(boost::asio::yield_context yield); + + /** + * @brief Get the number of coroutines in the group + * + * @return size_t The number of coroutines in the group + */ + size_t + size() const; }; } // namespace util diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index 55fab3f4c..56c2283d8 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -20,6 +20,7 @@ #include "web/ng/impl/ConnectionHandler.hpp" #include "util/Assert.hpp" +#include "util/CoroutineGroup.hpp" #include "util/log/Logger.hpp" #include "web/ng/Connection.hpp" #include "web/ng/Error.hpp" @@ -209,49 +210,45 @@ bool ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield) { // atomic_bool is not needed here because everything happening on coroutine's strand - std::optional closeConnectionGracefully; - size_t ongoingRequestsCounter{0}; + bool stop = false; + bool closeConnectionGracefully = true; + util::CoroutineGroup tasksGroup{yield}; - while (not closeConnectionGracefully.has_value()) { + while (not stop) { auto expectedRequest = connection.receive(yield); if (not expectedRequest) { - return handleError(expectedRequest.error(), connection); + auto const closeGracefully = handleError(expectedRequest.error(), connection); + stop = true; + closeConnectionGracefully &= closeGracefully; + break; } - ++ongoingRequestsCounter; - if (maxParallelRequests_.has_value() && ongoingRequestsCounter > *maxParallelRequests_) { + if (maxParallelRequests_.has_value() && tasksGroup.size() >= *maxParallelRequests_) { connection.send( Response{ boost::beast::http::status::too_many_requests, - "Too many requests for one session", + "Too many requests for one connection", expectedRequest.value() }, yield ); } else { - boost::asio::spawn( + tasksGroup.spawn( yield, // spawn on the same strand - [this, - &closeConnectionGracefully, - &ongoingRequestsCounter, - &connection, - request = std::move(expectedRequest).value()](boost::asio::yield_context innerYield) mutable { + [this, &stop, &closeConnectionGracefully, &connection, request = std::move(expectedRequest).value()]( + boost::asio::yield_context innerYield + ) mutable { auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield); if (maybeCloseConnectionGracefully.has_value()) { - if (closeConnectionGracefully.has_value()) { - // Close connection gracefully only if both are true. If at least one is false then - // connection is already closed. - closeConnectionGracefully = *closeConnectionGracefully && *maybeCloseConnectionGracefully; - } else { - closeConnectionGracefully = maybeCloseConnectionGracefully; - } + stop = true; + closeConnectionGracefully &= maybeCloseConnectionGracefully.value(); } - --ongoingRequestsCounter; } ); } } - return *closeConnectionGracefully; + tasksGroup.asyncWait(yield); + return closeConnectionGracefully; } std::optional diff --git a/tests/unit/util/CoroutineGroupTests.cpp b/tests/unit/util/CoroutineGroupTests.cpp index a0789b5fa..55105a8ab 100644 --- a/tests/unit/util/CoroutineGroupTests.cpp +++ b/tests/unit/util/CoroutineGroupTests.cpp @@ -44,21 +44,61 @@ TEST_F(CoroutineGroupTests, spawnWait) runSpawn([this](boost::asio::yield_context yield) { CoroutineGroup group{yield}; + group.spawn(yield, [&](boost::asio::yield_context yield) { boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; timer.async_wait(yield); callback1_.Call(); }); + EXPECT_EQ(group.size(), 1); + group.spawn(yield, [&](boost::asio::yield_context yield) { boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}}; timer.async_wait(yield); callback2_.Call(); }); + EXPECT_EQ(group.size(), 2); + group.asyncWait(yield); + EXPECT_EQ(group.size(), 0); + callback3_.Call(); }); } +TEST_F(CoroutineGroupTests, spawnWaitSpawnWait) +{ + testing::Sequence sequence; + EXPECT_CALL(callback1_, Call).InSequence(sequence); + EXPECT_CALL(callback2_, Call).InSequence(sequence); + EXPECT_CALL(callback3_, Call).InSequence(sequence); + + runSpawn([this](boost::asio::yield_context yield) { + CoroutineGroup group{yield}; + + group.spawn(yield, [&](boost::asio::yield_context yield) { + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; + timer.async_wait(yield); + callback1_.Call(); + }); + EXPECT_EQ(group.size(), 1); + + group.asyncWait(yield); + EXPECT_EQ(group.size(), 0); + + group.spawn(yield, [&](boost::asio::yield_context yield) { + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; + timer.async_wait(yield); + callback2_.Call(); + }); + EXPECT_EQ(group.size(), 1); + + group.asyncWait(yield); + EXPECT_EQ(group.size(), 0); + + callback3_.Call(); + }); +} TEST_F(CoroutineGroupTests, childCoroutinesFinishBeforeWait) { testing::Sequence sequence; @@ -86,3 +126,15 @@ TEST_F(CoroutineGroupTests, childCoroutinesFinishBeforeWait) callback3_.Call(); }); } + +TEST_F(CoroutineGroupTests, emptyGroup) +{ + testing::Sequence sequence; + EXPECT_CALL(callback1_, Call).InSequence(sequence); + + runSpawn([this](boost::asio::yield_context yield) { + CoroutineGroup group{yield}; + group.asyncWait(yield); + callback1_.Call(); + }); +} diff --git a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp index 533b72460..9b500129a 100644 --- a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp +++ b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp @@ -48,10 +48,8 @@ #include #include #include -#include #include #include -#include #include #include @@ -307,10 +305,8 @@ struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest { asyncSleep(boost::asio::yield_context yield, std::chrono::steady_clock::duration duration) { boost::asio::steady_timer timer{yield.get_executor()}; - std::cout << "sleep starts" << std::endl; timer.expires_after(duration); timer.async_wait(yield); - std::cout << "sleep ends" << std::endl; } }; @@ -404,20 +400,13 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany .WillRepeatedly([&](Request const& request, auto&&, boost::asio::yield_context yield) { EXPECT_EQ(request.message(), requestMessage); asyncSleep(yield, std::chrono::milliseconds{3}); - std::cout << "After sleep" << std::endl; return Response(http::status::ok, responseMessage, request); }); EXPECT_CALL( *mockConnection_, send( - testing::ResultOf( - [](Response response) { - std::cout << "1 " << response.message() << std::endl; - return response.message(); - }, - responseMessage - ), + testing::ResultOf([](Response response) { return response.message(); }, responseMessage), testing::_, testing::_ ) @@ -429,11 +418,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany *mockConnection_, send( testing::ResultOf( - [](Response response) { - std::cout << "2 " << response.message() << std::endl; - return response.message(); - }, - "Too many requests for one session" + [](Response response) { return response.message(); }, "Too many requests for one connection" ), testing::_, testing::_ @@ -442,58 +427,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany .Times(2) .WillRepeatedly(Return(std::nullopt)); - // .WillRepeatedly([&](Response response, auto&&, auto&&) { - // if (handlerCallCounter < 3) { - // EXPECT_EQ(response.message(), responseMessage); - // return std::nullopt; - // } - // EXPECT_EQ(response.message(), "Too many requests for one session"); - // return std::nullopt; - // }); - runSpawn([this](boost::asio::yield_context yield) { connectionHandler_.processConnection(std::move(mockConnection_), yield); }); } - -TEST_F(ConnectionHandlerParallelProcessingTest, myTest) -{ - runSpawn([](boost::asio::yield_context yield) { - boost::asio::steady_timer sync{yield.get_executor(), boost::asio::steady_timer::clock_type::duration::max()}; - - int childNumber = 0; - boost::asio::spawn(yield, [&](boost::asio::yield_context innerYield) { - ++childNumber; - - std::cout << "I'm child coroutine" << std::endl; - boost::asio::steady_timer t{innerYield.get_executor()}; - t.expires_after(std::chrono::milliseconds{20}); - t.async_wait(innerYield); - std::cout << "Child 1 done" << std::endl; - - --childNumber; - if (childNumber == 0) - sync.cancel(); - }); - - std::cout << "Parent: between" << std::endl; - - boost::asio::spawn(yield, [&](auto&& innerYield) { - ++childNumber; - - std::cout << "I'm child coroutine 2" << std::endl; - boost::asio::steady_timer t{innerYield.get_executor()}; - t.expires_after(std::chrono::milliseconds{30}); - t.async_wait(innerYield); - std::cout << "Child 2 done" << std::endl; - - --childNumber; - if (childNumber == 0) - sync.cancel(); - }); - - boost::system::error_code error; - sync.async_wait(yield[error]); - std::cout << "Parent done" << std::endl; - }); -} From 5fd082d4452ba2f62da624b4aed54811c0147e11 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 16 Oct 2024 16:56:09 +0100 Subject: [PATCH 39/81] Move tasks limit to CoroutineGroup --- src/util/CoroutineGroup.cpp | 18 ++++++++-- src/util/CoroutineGroup.hpp | 19 +++++++++-- src/web/ng/impl/ConnectionHandler.cpp | 30 ++++++++--------- tests/unit/util/CoroutineGroupTests.cpp | 45 ++++++++++++++++++++----- 4 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/util/CoroutineGroup.cpp b/src/util/CoroutineGroup.cpp index e068bf528..271486078 100644 --- a/src/util/CoroutineGroup.cpp +++ b/src/util/CoroutineGroup.cpp @@ -19,23 +19,34 @@ #include "util/CoroutineGroup.hpp" +#include "util/Assert.hpp" + #include #include #include #include +#include #include namespace util { -CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield) - : timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()} +CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional maxChildren) + : timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren} { } -void +CoroutineGroup::~CoroutineGroup() +{ + ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish"); +} + +bool CoroutineGroup::spawn(boost::asio::yield_context yield, std::function fn) { + if (maxChildren_.has_value() && childrenCounter_ >= *maxChildren_) + return false; + ++childrenCounter_; boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) { fn(yield); @@ -43,6 +54,7 @@ CoroutineGroup::spawn(boost::asio::yield_context yield, std::function #include +#include namespace util { @@ -33,6 +34,7 @@ namespace util { */ class CoroutineGroup { boost::asio::steady_timer timer_; + std::optional maxChildren_; int childrenCounter_{0}; public: @@ -40,8 +42,17 @@ class CoroutineGroup { * @brief Construct a new Coroutine Group object * * @param yield The yield context to use for the internal timer + * @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there + * is no limit */ - CoroutineGroup(boost::asio::yield_context yield); + CoroutineGroup(boost::asio::yield_context yield, std::optional maxChildren = std::nullopt); + + /** + * @brief Destroy the Coroutine Group object + * + * @note asyncWait() must be called before the object is destroyed + */ + ~CoroutineGroup(); /** * @brief Spawn a new coroutine in the group @@ -49,13 +60,17 @@ class CoroutineGroup { * @param yield The yield context to use for the coroutine (it should be the same as the one used in the * constructor) * @param fn The function to execute + * @return true If the coroutine was spawned successfully. false if the maximum number of coroutines has been + * reached */ - void + bool spawn(boost::asio::yield_context yield, std::function fn); /** * @brief Wait for all the coroutines in the group to finish * + * @note This method must be called before the object is destroyed + * * @param yield The yield context to use for the internal timer */ void diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index 56c2283d8..f87107fcd 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -212,7 +212,7 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as // atomic_bool is not needed here because everything happening on coroutine's strand bool stop = false; bool closeConnectionGracefully = true; - util::CoroutineGroup tasksGroup{yield}; + util::CoroutineGroup tasksGroup{yield, maxParallelRequests_}; while (not stop) { auto expectedRequest = connection.receive(yield); @@ -223,7 +223,20 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as break; } - if (maxParallelRequests_.has_value() && tasksGroup.size() >= *maxParallelRequests_) { + bool const spawnSuccess = tasksGroup.spawn( + yield, // spawn on the same strand + [this, &stop, &closeConnectionGracefully, &connection, request = std::move(expectedRequest).value()]( + boost::asio::yield_context innerYield + ) mutable { + auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield); + if (maybeCloseConnectionGracefully.has_value()) { + stop = true; + closeConnectionGracefully &= maybeCloseConnectionGracefully.value(); + } + } + ); + + if (not spawnSuccess) { connection.send( Response{ boost::beast::http::status::too_many_requests, @@ -232,19 +245,6 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as }, yield ); - } else { - tasksGroup.spawn( - yield, // spawn on the same strand - [this, &stop, &closeConnectionGracefully, &connection, request = std::move(expectedRequest).value()]( - boost::asio::yield_context innerYield - ) mutable { - auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield); - if (maybeCloseConnectionGracefully.has_value()) { - stop = true; - closeConnectionGracefully &= maybeCloseConnectionGracefully.value(); - } - } - ); } } tasksGroup.asyncWait(yield); diff --git a/tests/unit/util/CoroutineGroupTests.cpp b/tests/unit/util/CoroutineGroupTests.cpp index 55105a8ab..ba9c6e38b 100644 --- a/tests/unit/util/CoroutineGroupTests.cpp +++ b/tests/unit/util/CoroutineGroupTests.cpp @@ -35,7 +35,7 @@ struct CoroutineGroupTests : SyncAsioContextTest { testing::StrictMock> callback3_; }; -TEST_F(CoroutineGroupTests, spawnWait) +TEST_F(CoroutineGroupTests, SpawnWait) { testing::Sequence sequence; EXPECT_CALL(callback1_, Call).InSequence(sequence); @@ -43,7 +43,7 @@ TEST_F(CoroutineGroupTests, spawnWait) EXPECT_CALL(callback3_, Call).InSequence(sequence); runSpawn([this](boost::asio::yield_context yield) { - CoroutineGroup group{yield}; + CoroutineGroup group{yield, 2}; group.spawn(yield, [&](boost::asio::yield_context yield) { boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; @@ -66,7 +66,7 @@ TEST_F(CoroutineGroupTests, spawnWait) }); } -TEST_F(CoroutineGroupTests, spawnWaitSpawnWait) +TEST_F(CoroutineGroupTests, SpawnWaitSpawnWait) { testing::Sequence sequence; EXPECT_CALL(callback1_, Call).InSequence(sequence); @@ -74,7 +74,7 @@ TEST_F(CoroutineGroupTests, spawnWaitSpawnWait) EXPECT_CALL(callback3_, Call).InSequence(sequence); runSpawn([this](boost::asio::yield_context yield) { - CoroutineGroup group{yield}; + CoroutineGroup group{yield, 2}; group.spawn(yield, [&](boost::asio::yield_context yield) { boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}}; @@ -99,7 +99,8 @@ TEST_F(CoroutineGroupTests, spawnWaitSpawnWait) callback3_.Call(); }); } -TEST_F(CoroutineGroupTests, childCoroutinesFinishBeforeWait) + +TEST_F(CoroutineGroupTests, ChildCoroutinesFinishBeforeWait) { testing::Sequence sequence; EXPECT_CALL(callback2_, Call).InSequence(sequence); @@ -107,7 +108,7 @@ TEST_F(CoroutineGroupTests, childCoroutinesFinishBeforeWait) EXPECT_CALL(callback3_, Call).InSequence(sequence); runSpawn([this](boost::asio::yield_context yield) { - CoroutineGroup group{yield}; + CoroutineGroup group{yield, 2}; group.spawn(yield, [&](boost::asio::yield_context yield) { boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}}; timer.async_wait(yield); @@ -127,10 +128,9 @@ TEST_F(CoroutineGroupTests, childCoroutinesFinishBeforeWait) }); } -TEST_F(CoroutineGroupTests, emptyGroup) +TEST_F(CoroutineGroupTests, EmptyGroup) { - testing::Sequence sequence; - EXPECT_CALL(callback1_, Call).InSequence(sequence); + EXPECT_CALL(callback1_, Call); runSpawn([this](boost::asio::yield_context yield) { CoroutineGroup group{yield}; @@ -138,3 +138,30 @@ TEST_F(CoroutineGroupTests, emptyGroup) callback1_.Call(); }); } + +TEST_F(CoroutineGroupTests, TooManyCoroutines) +{ + EXPECT_CALL(callback1_, Call); + EXPECT_CALL(callback2_, Call); + EXPECT_CALL(callback3_, Call); + + runSpawn([this](boost::asio::yield_context yield) { + CoroutineGroup group{yield, 1}; + + EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context innerYield) { + boost::asio::steady_timer timer{innerYield.get_executor(), std::chrono::milliseconds{1}}; + timer.async_wait(innerYield); + callback1_.Call(); + })); + + EXPECT_FALSE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); })); + + boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}}; + timer.async_wait(yield); + + EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); })); + + group.asyncWait(yield); + callback3_.Call(); + }); +} From 61947419abcf42164bb80011e43ec207c66e9cb2 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 17 Oct 2024 14:52:42 +0100 Subject: [PATCH 40/81] Add tests for make_Server --- docs/examples/config/example-config.json | 6 +- src/web/ng/Server.cpp | 14 ++- tests/unit/CMakeLists.txt | 1 + tests/unit/web/ng/ServerTests.cpp | 116 +++++++++++++++++++++++ 4 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 tests/unit/web/ng/ServerTests.cpp diff --git a/docs/examples/config/example-config.json b/docs/examples/config/example-config.json index 441bf0e8b..bf839ab01 100644 --- a/docs/examples/config/example-config.json +++ b/docs/examples/config/example-config.json @@ -71,9 +71,9 @@ // If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests // It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time "local_admin": false, - "processing_strategy": "parallel", // Could be "sequent" or "parallel". - // For sequent strategy request from one client connection will be processed one by one and the next one will not be read before - // the previous one is processed. For parallel strategy Clio will take all requests and process them in parallel and + "processing_policy": "parallel", // Could be "sequent" or "parallel". + // For sequent policy request from one client connection will be processed one by one and the next one will not be read before + // the previous one is processed. For parallel policy Clio will take all requests and process them in parallel and // send a reply for each request whenever it is ready. "parallel_requests_limit": 10 // Optional parameter, used only if "processing_strategy" is "parallel". It limits the number of requests for one client connection processed in parallel. Infinite if not specified. diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 6afb636d7..be6192fd6 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -63,7 +63,11 @@ makeEndpoint(util::Config const& serverConfig) if (not ip.has_value()) return std::unexpected{"Missing 'ip` in server config."}; - auto const address = boost::asio::ip::make_address(*ip); + boost::system::error_code error; + auto const address = boost::asio::ip::make_address(*ip, error); + if (error) + return std::unexpected{fmt::format("Error parsing provided IP: {}", error.message())}; + auto const port = serverConfig.maybeValue("port"); if (not port.has_value()) return std::unexpected{"Missing 'port` in server config."}; @@ -286,12 +290,12 @@ make_Server(util::Config const& config, boost::asio::io_context& context) if (not expectedSslContext) return std::unexpected{std::move(expectedSslContext).error()}; - impl::ConnectionHandler::ProcessingPolicy processingStrategy{impl::ConnectionHandler::ProcessingPolicy::Parallel}; + impl::ConnectionHandler::ProcessingPolicy processingPolicy{impl::ConnectionHandler::ProcessingPolicy::Parallel}; std::optional parallelRequestLimit; - auto const processingStrategyStr = serverConfig.valueOr("processing_strategy", "parallel"); + auto const processingStrategyStr = serverConfig.valueOr("processing_policy", "parallel"); if (processingStrategyStr == "sequent") { - processingStrategy = impl::ConnectionHandler::ProcessingPolicy::Sequential; + processingPolicy = impl::ConnectionHandler::ProcessingPolicy::Sequential; } else if (processingStrategyStr == "parallel") { parallelRequestLimit = serverConfig.maybeValue("parallel_requests_limit"); } else { @@ -302,7 +306,7 @@ make_Server(util::Config const& config, boost::asio::io_context& context) context, std::move(endpoint).value(), std::move(expectedSslContext).value(), - impl::ConnectionHandler{processingStrategy, parallelRequestLimit}, + impl::ConnectionHandler{processingPolicy, parallelRequestLimit}, util::TagDecoratorFactory(config) }; } diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 10025fb59..09b8fd703 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -135,6 +135,7 @@ target_sources( web/impl/AdminVerificationTests.cpp web/ng/ResponseTests.cpp web/ng/RequestTests.cpp + web/ng/ServerTests.cpp web/ng/impl/ConnectionHandlerTests.cpp web/ng/impl/HttpConnectionTests.cpp web/ng/impl/ServerSslContextTests.cpp diff --git a/tests/unit/web/ng/ServerTests.cpp b/tests/unit/web/ng/ServerTests.cpp new file mode 100644 index 000000000..0257cbfbc --- /dev/null +++ b/tests/unit/web/ng/ServerTests.cpp @@ -0,0 +1,116 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/AsioContextTestFixture.hpp" +#include "util/LoggerFixtures.hpp" +#include "util/NameGenerator.hpp" +#include "util/config/Config.hpp" +#include "web/ng/Server.hpp" + +#include +#include +#include +#include +#include + +#include + +using namespace web::ng; + +struct MakeServerTestBundle { + std::string testName; + std::string configJson; + bool expectSuccess; +}; + +struct MakeServerTest : NoLoggerFixture, testing::WithParamInterface { + boost::asio::io_context ioContext_; +}; + +TEST_P(MakeServerTest, Make) +{ + util::Config const config{boost::json::parse(GetParam().configJson)}; + auto const expectedServer = make_Server(config, ioContext_); + EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess); +} + +INSTANTIATE_TEST_CASE_P( + MakeServerTests, + MakeServerTest, + testing::Values( + MakeServerTestBundle{ + "BadEndpoint", + R"json( + { + "server": {"ip": "wrong", "port": 12345} + } + )json", + false + }, + MakeServerTestBundle{ + "PortMissing", + R"json( + { + "server": {"ip": "127.0.0.1"} + } + )json", + false + }, + MakeServerTestBundle{ + "BadSslConfig", + R"json( + { + "server": {"ip": "127.0.0.1", "port": 12345}, + "ssl_cert_file": "somг_file" + } + )json", + false + }, + MakeServerTestBundle{ + "BadProcessingPolicy", + R"json( + { + "server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "wrong"} + } + )json", + false + }, + MakeServerTestBundle{ + "CorrectConfig_ParallelPolicy", + R"json( + { + "server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "parallel"} + } + )json", + true + }, + MakeServerTestBundle{ + "CorrectConfig_SequentPolicy", + R"json( + { + "server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "sequent"} + } + )json", + true + } + ), + tests::util::NameGenerator +); + +struct ServerTest : SyncAsioContextTest {}; From f6b2a135ad8f2cad36fa0f5fc87329271c03f62d Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 18 Oct 2024 16:04:39 +0100 Subject: [PATCH 41/81] Cover Server with tests. Fix some bugs --- src/web/ng/Server.cpp | 9 +- src/web/ng/impl/WsConnection.hpp | 5 +- tests/unit/web/ng/ServerTests.cpp | 160 +++++++++++++++++- .../unit/web/ng/impl/HttpConnectionTests.cpp | 32 +++- tests/unit/web/ng/impl/WsConnectionTests.cpp | 54 +++++- 5 files changed, 255 insertions(+), 5 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index be6192fd6..23ba04dcc 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -155,7 +155,14 @@ makeConnection( ); } - if (connection->isUpgradeRequested(yield)) { + auto const expectedIsUpgrade = connection->isUpgradeRequested(yield); + if (not expectedIsUpgrade.has_value()) { + return std::unexpected{ + fmt::format("Error checking whether upgrade requested: {}", expectedIsUpgrade.error().message()) + }; + } + + if (*expectedIsUpgrade) { return connection->upgrade(sslContext, tagDecoratorFactory, yield) .or_else([](Error error) -> std::expected { return std::unexpected{fmt::format("Error upgrading connection: {}", error.what())}; diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index 91fdba7d1..34e9e42c7 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -133,7 +133,10 @@ class WsConnection : public Connection { if (error) return std::unexpected{error}; - return Request{boost::beast::buffers_to_string(buffer_.data()), initialRequest_}; + auto request = boost::beast::buffers_to_string(buffer_.data()); + buffer_.consume(buffer_.size()); + + return Request{std::move(request), initialRequest_}; } void diff --git a/tests/unit/web/ng/ServerTests.cpp b/tests/unit/web/ng/ServerTests.cpp index 0257cbfbc..87120ff63 100644 --- a/tests/unit/web/ng/ServerTests.cpp +++ b/tests/unit/web/ng/ServerTests.cpp @@ -18,21 +18,38 @@ //============================================================================== #include "util/AsioContextTestFixture.hpp" +#include "util/AssignRandomPort.hpp" #include "util/LoggerFixtures.hpp" #include "util/NameGenerator.hpp" +#include "util/TestHttpClient.hpp" +#include "util/TestWebSocketClient.hpp" #include "util/config/Config.hpp" +#include "web/ng/Connection.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" #include "web/ng/Server.hpp" #include +#include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include +#include #include using namespace web::ng; +namespace http = boost::beast::http; + struct MakeServerTestBundle { std::string testName; std::string configJson; @@ -113,4 +130,145 @@ INSTANTIATE_TEST_CASE_P( tests::util::NameGenerator ); -struct ServerTest : SyncAsioContextTest {}; +struct ServerTest : SyncAsioContextTest { + ServerTest() + { + [&]() { ASSERT_TRUE(server_.has_value()); }(); + server_->onGet("/", getHandler_.AsStdFunction()); + server_->onPost("/", postHandler_.AsStdFunction()); + server_->onWs(wsHandler_.AsStdFunction()); + } + + uint32_t const serverPort_ = tests::util::generateFreePort(); + + util::Config const config_{ + boost::json::object{{"server", boost::json::object{{"ip", "127.0.0.1"}, {"port", serverPort_}}}} + }; + + std::expected server_ = make_Server(config_, ctx); + + std::string requestMessage_ = "some request"; + std::string const headerName_ = "Some-header"; + std::string const headerValue_ = "some value"; + + testing::StrictMock> + getHandler_; + testing::StrictMock> + postHandler_; + testing::StrictMock> + wsHandler_; +}; + +struct ServerTestBundle { + std::string testName; + http::verb method; + + Request::Method + expectedMethod() const + { + switch (method) { + case http::verb::get: + return Request::Method::GET; + case http::verb::post: + return Request::Method::POST; + default: + return Request::Method::UNSUPPORTED; + } + } +}; + +struct ServerHttpTest : ServerTest, testing::WithParamInterface {}; + +TEST_P(ServerHttpTest, RequestResponse) +{ + HttpAsyncClient client{ctx}; + + http::request request{GetParam().method, "/", 11, requestMessage_}; + request.set(headerName_, headerValue_); + + Response const response{http::status::ok, "some response", Request{request}}; + + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto maybeError = + client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + maybeError = client.send(request, yield, std::chrono::milliseconds{100}); + EXPECT_FALSE(maybeError.has_value()) << maybeError->message(); + + auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }(); + EXPECT_EQ(expectedResponse->result(), http::status::ok); + EXPECT_EQ(expectedResponse->body(), response.message()); + } + + client.gracefulShutdown(); + ctx.stop(); + }); + + auto& handler = GetParam().method == http::verb::get ? getHandler_ : postHandler_; + + EXPECT_CALL(handler, Call) + .Times(3) + .WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) { + EXPECT_TRUE(receivedRequest.isHttp()); + EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod()); + EXPECT_EQ(receivedRequest.message(), request.body()); + EXPECT_EQ(receivedRequest.target(), request.target()); + EXPECT_EQ(receivedRequest.headerValue(headerName_), request.at(headerName_)); + + return response; + }); + + server_->run(); + + runContext(); +} + +INSTANTIATE_TEST_SUITE_P( + ServerHttpTests, + ServerHttpTest, + testing::Values(ServerTestBundle{"GET", http::verb::get}, ServerTestBundle{"POST", http::verb::post}), + tests::util::NameGenerator +); + +TEST_F(ServerTest, WsRequestResponse) +{ + WebSocketAsyncClient client{ctx}; + + Response const response{http::status::ok, "some response", Request{requestMessage_, Request::HttpHeaders{}}}; + + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto maybeError = + client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + maybeError = client.send(yield, requestMessage_, std::chrono::milliseconds{100}); + EXPECT_FALSE(maybeError.has_value()) << maybeError->message(); + + auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }(); + EXPECT_EQ(expectedResponse.value(), response.message()); + } + + client.gracefulClose(yield, std::chrono::milliseconds{100}); + ctx.stop(); + }); + + EXPECT_CALL(wsHandler_, Call) + .Times(3) + .WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) { + EXPECT_FALSE(receivedRequest.isHttp()); + EXPECT_EQ(receivedRequest.method(), Request::Method::WEBSOCKET); + EXPECT_EQ(receivedRequest.message(), requestMessage_); + EXPECT_EQ(receivedRequest.target(), std::nullopt); + + return response; + }); + + server_->run(); + + runContext(); +} diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp index b5565dc08..330dae387 100644 --- a/tests/unit/web/ng/impl/HttpConnectionTests.cpp +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include using namespace web::ng::impl; @@ -162,9 +163,38 @@ TEST_F(HttpConnectionTests, Send) auto connection = acceptConnection(yield); auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); +} - maybeError = connection.send(response, yield); +TEST_F(HttpConnectionTests, SendMultipleTimes) +{ + Request const request{request_}; + Response const response{http::status::ok, "some response data", request}; + + boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }(); + + auto const receivedResponse = expectedResponse.value(); + auto const sentResponse = Response{response}.intoHttpResponse(); + EXPECT_EQ(receivedResponse.result(), sentResponse.result()); + EXPECT_EQ(receivedResponse.body(), sentResponse.body()); + EXPECT_EQ(receivedResponse.version(), request_.version()); + EXPECT_TRUE(receivedResponse.keep_alive()); + } + }); + + runSpawn([this, &response](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + } }); } diff --git a/tests/unit/web/ng/impl/WsConnectionTests.cpp b/tests/unit/web/ng/impl/WsConnectionTests.cpp index 8d274bd77..3ea2782fb 100644 --- a/tests/unit/web/ng/impl/WsConnectionTests.cpp +++ b/tests/unit/web/ng/impl/WsConnectionTests.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include using namespace web::ng::impl; @@ -111,6 +112,31 @@ TEST_F(web_WsConnectionTests, Send) }); } +TEST_F(web_WsConnectionTests, MultipleSend) +{ + Response const response{boost::beast::http::status::ok, "some response", request_}; + + boost::asio::spawn(ctx, [this, &response](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + auto const expectedMessage = wsClient_.receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(expectedMessage.has_value()) << expectedMessage.error().message(); }(); + EXPECT_EQ(expectedMessage.value(), response.message()); + } + }); + + runSpawn([this, &response](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + } + }); +} + TEST_F(web_WsConnectionTests, SendFailed) { Response const response{boost::beast::http::status::ok, "some response", request_}; @@ -139,17 +165,43 @@ TEST_F(web_WsConnectionTests, Receive) boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); - wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100}); + + maybeError = wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100}); + EXPECT_FALSE(maybeError.has_value()) << maybeError->message(); }); runSpawn([this](boost::asio::yield_context yield) { auto wsConnection = acceptConnection(yield); + auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100}); [&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }(); EXPECT_EQ(maybeRequest->message(), request_.message()); }); } +TEST_F(web_WsConnectionTests, MultipleReceive) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }(); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + maybeError = wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100}); + EXPECT_FALSE(maybeError.has_value()) << maybeError->message(); + } + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto wsConnection = acceptConnection(yield); + + for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) { + auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }(); + EXPECT_EQ(maybeRequest->message(), request_.message()); + } + }); +} + TEST_F(web_WsConnectionTests, ReceiveTimeout) { boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { From 35a917ff265a9dc5d02ee7f3c67fb848b67cb8c5 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 21 Oct 2024 15:10:58 +0100 Subject: [PATCH 42/81] Add documentation --- src/web/ng/Connection.cpp | 14 +------ src/web/ng/Connection.hpp | 73 ++++++++++++++++++++++++++++++----- src/web/ng/Error.hpp | 3 ++ src/web/ng/MessageHandler.hpp | 3 ++ src/web/ng/Request.hpp | 3 -- src/web/ng/Server.hpp | 56 +++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 25 deletions(-) diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index a997f568c..4bfb4bc22 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -23,30 +23,18 @@ #include -#include #include #include #include namespace web::ng { -namespace { - -size_t -generateId() -{ - static std::atomic_size_t id{0}; - return id++; -} - -} // namespace - Connection::Connection( std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory ) - : util::Taggable(tagDecoratorFactory), id_{generateId()}, ip_{std::move(ip)}, buffer_{std::move(buffer)} + : util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}, buffer_{std::move(buffer)} { } diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index edd6e7eb1..45b20052e 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -37,22 +37,51 @@ namespace web::ng { +/** + * @brief A forward declaration of ConnectionContext. + */ class ConnectionContext; +/** + *@brief A class representing a connection to a client. + */ class Connection : public util::Taggable { protected: - size_t id_; std::string ip_; // client ip boost::beast::flat_buffer buffer_; public: + /** + * @brief The default timeout for send, receive, and close operations. + */ static constexpr std::chrono::steady_clock::duration DEFAULT_TIMEOUT = std::chrono::seconds{30}; + /** + * @brief Construct a new Connection object + * + * @param ip The client ip. + * @param buffer The buffer to use for reading and writing. + * @param tagDecoratorFactory The factory for creating tag decorators. + */ Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory); + /** + * @brief Whether the connection was upgraded. Upgraded connections are websocket connections. + * + * @return true if the connection was upgraded. + */ virtual bool wasUpgraded() const = 0; + /** + * @brief Send a response to the client. + * + * @param response The response to send. + * @param yield The yield context. + * @param timeout The timeout for the operation. + * @return An error if the operation failed or nullopt if it succeeded. + */ + virtual std::optional send( Response response, @@ -60,34 +89,60 @@ class Connection : public util::Taggable { std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT ) = 0; + /** + * @brief Receive a request from the client. + * + * @param yield The yield context. + * @param timeout The timeout for the operation. + * @return The request if it was received or an error if the operation failed. + */ virtual std::expected receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0; + /** + * @brief Gracefully close the connection. + * + * @param yield The yield context. + * @param timeout The timeout for the operation. + */ virtual void close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0; - void - subscribeToDisconnect(); - + /** + * @brief Get the connection context. + * + * @return The connection context. + */ ConnectionContext context() const; - size_t - id() const; - + /** + * @brief Get the ip of the client. + * + * @return The ip of the client. + */ std::string const& ip() const; }; +/** + * @brief A pointer to a connection. + */ using ConnectionPtr = std::unique_ptr; +/** + * @brief A class representing the context of a connection. + */ class ConnectionContext { std::reference_wrapper connection_; public: + /** + * @brief Construct a new ConnectionContext object. + * + * @param connection The connection. + */ explicit ConnectionContext(Connection const& connection); }; -using ConnectionPtr = std::unique_ptr; - } // namespace web::ng diff --git a/src/web/ng/Error.hpp b/src/web/ng/Error.hpp index bca1d708b..93f356464 100644 --- a/src/web/ng/Error.hpp +++ b/src/web/ng/Error.hpp @@ -23,6 +23,9 @@ namespace web::ng { +/** + * @brief Error of any async operation. + */ using Error = boost::system::error_code; } // namespace web::ng diff --git a/src/web/ng/MessageHandler.hpp b/src/web/ng/MessageHandler.hpp index 7ecba4d44..f518238f8 100644 --- a/src/web/ng/MessageHandler.hpp +++ b/src/web/ng/MessageHandler.hpp @@ -29,6 +29,9 @@ namespace web::ng { +/** + * @brief Handler for messages. + */ using MessageHandler = std::function; } // namespace web::ng diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index cf1f03e2b..01a5639ce 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -66,9 +66,6 @@ class Request { */ Request(std::string request, HttpHeaders const& headers); - bool - operator==(Request const& other) const; - /** * @brief Method of the request. * @note WEBSOCKET is not a real method, it is used to distinguish WebSocket requests from HTTP requests. diff --git a/src/web/ng/Server.hpp b/src/web/ng/Server.hpp index 6b19bf85d..dd68ad0b9 100644 --- a/src/web/ng/Server.hpp +++ b/src/web/ng/Server.hpp @@ -38,6 +38,9 @@ namespace web::ng { +/** + * @brief Web server class. + */ class Server { util::Logger log_{"WebServer"}; util::Logger perfLog_{"Performance"}; @@ -54,6 +57,15 @@ class Server { bool running_{false}; public: + /** + * @brief Construct a new Server object. + * + * @param ctx The boost::asio::io_context to use. + * @param endpoint The endpoint to listen on. + * @param sslContext The SSL context to use (optional). + * @param connectionHandler The connection handler. + * @param tagDecoratorFactory The tag decorator factory. + */ Server( boost::asio::io_context& ctx, boost::asio::ip::tcp::endpoint endpoint, @@ -62,21 +74,57 @@ class Server { util::TagDecoratorFactory tagDecoratorFactory ); + /** + * @brief Copy constructor is deleted. The Server couldn't be copied. + */ Server(Server const&) = delete; + + /** + * @brief Move constructor is defaulted. + */ Server(Server&&) = default; + /** + * @brief Set handler for GET requests. + * @note This method can't be called after run() is called. + * + * @param target The target of the request. + * @param handler The handler to set. + */ void onGet(std::string const& target, MessageHandler handler); + /** + * @brief Set handler for POST requests. + * @note This method can't be called after run() is called. + * + * @param target The target of the request. + * @param handler The handler to set. + */ void onPost(std::string const& target, MessageHandler handler); + /** + * @brief Set handler for WebSocket requests. + * @note This method can't be called after run() is called. + * + * @param handler The handler to set. + */ void onWs(MessageHandler handler); + /** + * @brief Run the server. + * + * @return std::nullopt if the server started successfully, otherwise an error message. + */ std::optional run(); + /** + * @brief Stop the server. + ** @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections. + */ void stop(); @@ -85,6 +133,14 @@ class Server { handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield); }; +/** + * @brief Create a new Server. + * + * @param config The configuration. + * @param context The boost::asio::io_context to use. + * + * @return The Server or an error message. + */ std::expected make_Server(util::Config const& config, boost::asio::io_context& context); From e6547186814632013f402b84466b68f6302048e2 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 21 Oct 2024 15:21:27 +0100 Subject: [PATCH 43/81] Add more documentation --- src/web/ng/Response.hpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 22183836e..3e0f95a2c 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -40,12 +40,15 @@ class Response { * @brief The data for an HTTP response. */ struct HttpData { + /** + * @brief The content type of the response. + */ enum class ContentType { APPLICATION_JSON, TEXT_HTML }; - boost::beast::http::status status; - ContentType contentType; - bool keepAlive; - unsigned int version; + boost::beast::http::status status; ///< The HTTP status. + ContentType contentType; ///< The content type. + bool keepAlive; ///< Whether the connection should be kept alive. + unsigned int version; ///< The HTTP version. }; private: From 0b2abe6d28bd289678fb31e624a656e610167678 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 21 Oct 2024 15:35:26 +0100 Subject: [PATCH 44/81] Removed and_then and or_else ;-( --- src/web/ng/Server.cpp | 10 ++++++---- src/web/ng/impl/HttpConnection.hpp | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 23ba04dcc..581a5f770 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -163,10 +163,12 @@ makeConnection( } if (*expectedIsUpgrade) { - return connection->upgrade(sslContext, tagDecoratorFactory, yield) - .or_else([](Error error) -> std::expected { - return std::unexpected{fmt::format("Error upgrading connection: {}", error.what())}; - }); + auto expectedUpgradedConnection = connection->upgrade(sslContext, tagDecoratorFactory, yield); + if (expectedUpgradedConnection.has_value()) + return std::move(expectedUpgradedConnection).value(); + + return std::unexpected{fmt::format("Error upgrading connection: {}", expectedUpgradedConnection.error().what()) + }; } return connection; diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index a15d070c9..da0104494 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -129,9 +129,11 @@ class HttpConnection : public UpgradableConnection { request_.reset(); return std::move(result); } - return fetch(yield, timeout).and_then([](auto httpRequest) -> std::expected { - return Request{std::move(httpRequest)}; - }); + auto expectedRequest = fetch(yield, timeout); + if (expectedRequest.has_value()) + return Request{std::move(expectedRequest).value()}; + + return std::unexpected{std::move(expectedRequest).error()}; } void From 633707a50693d4d00596ad6110fce65a61bf5f08 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 21 Oct 2024 16:21:37 +0100 Subject: [PATCH 45/81] Remove redundant std::move's --- src/web/ng/Server.cpp | 3 +-- src/web/ng/impl/HttpConnection.hpp | 4 ++-- src/web/ng/impl/WsConnection.cpp | 4 ++-- src/web/ng/impl/WsConnection.hpp | 2 +- tests/common/util/TestHttpClient.cpp | 2 +- tests/unit/web/ng/impl/ConnectionHandlerTests.cpp | 8 ++++++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/web/ng/Server.cpp b/src/web/ng/Server.cpp index 581a5f770..07045de1f 100644 --- a/src/web/ng/Server.cpp +++ b/src/web/ng/Server.cpp @@ -24,7 +24,6 @@ #include "util/config/Config.hpp" #include "util/log/Logger.hpp" #include "web/ng/Connection.hpp" -#include "web/ng/Error.hpp" #include "web/ng/MessageHandler.hpp" #include "web/ng/impl/HttpConnection.hpp" #include "web/ng/impl/ServerSslContext.hpp" @@ -87,7 +86,7 @@ makeAcceptor(boost::asio::io_context& context, boost::asio::ip::tcp::endpoint co } catch (boost::system::system_error const& error) { return std::unexpected{fmt::format("Error creating TCP acceptor: {}", error.what())}; } - return std::move(acceptor); + return acceptor; } std::expected diff --git a/src/web/ng/impl/HttpConnection.hpp b/src/web/ng/impl/HttpConnection.hpp index da0104494..3a598c38b 100644 --- a/src/web/ng/impl/HttpConnection.hpp +++ b/src/web/ng/impl/HttpConnection.hpp @@ -127,7 +127,7 @@ class HttpConnection : public UpgradableConnection { if (request_.has_value()) { Request result{std::move(request_).value()}; request_.reset(); - return std::move(result); + return result; } auto expectedRequest = fetch(yield, timeout); if (expectedRequest.has_value()) @@ -208,7 +208,7 @@ class HttpConnection : public UpgradableConnection { boost::beast::http::async_read(stream_, buffer_, request, yield[error]); if (error) return std::unexpected{error}; - return std::move(request); + return request; } }; diff --git a/src/web/ng/impl/WsConnection.cpp b/src/web/ng/impl/WsConnection.cpp index 4fa3df281..5ea6a9763 100644 --- a/src/web/ng/impl/WsConnection.cpp +++ b/src/web/ng/impl/WsConnection.cpp @@ -51,7 +51,7 @@ make_PlainWsConnection( auto maybeError = connection->performHandshake(yield); if (maybeError.has_value()) return std::unexpected{maybeError.value()}; - return std::move(connection); + return connection; } std::expected, Error> @@ -71,7 +71,7 @@ make_SslWsConnection( auto maybeError = connection->performHandshake(yield); if (maybeError.has_value()) return std::unexpected{maybeError.value()}; - return std::move(connection); + return connection; } } // namespace web::ng::impl diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index 34e9e42c7..956f15388 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -122,7 +122,7 @@ class WsConnection : public Connection { [this, &response](auto&& yield) { stream_.async_write(response.asConstBuffer(), yield); }, yield, timeout ); if (error) - return std::move(error); + return error; return std::nullopt; } diff --git a/tests/common/util/TestHttpClient.cpp b/tests/common/util/TestHttpClient.cpp index da5c2afe9..45445c05a 100644 --- a/tests/common/util/TestHttpClient.cpp +++ b/tests/common/util/TestHttpClient.cpp @@ -232,7 +232,7 @@ HttpAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_c http::async_read(stream_, buffer_, response, yield[error]); if (error) return std::unexpected{error}; - return std::move(response); + return response; } void diff --git a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp index 9b500129a..8306cddf6 100644 --- a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp +++ b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp @@ -295,9 +295,13 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop) } struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest { - static size_t const maxParallelRequests = 3; + static size_t constexpr maxParallelRequests = 3; + ConnectionHandlerParallelProcessingTest() - : ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Parallel, maxParallelRequests) + : ConnectionHandlerTest( + ConnectionHandler::ProcessingPolicy::Parallel, + ConnectionHandlerParallelProcessingTest::maxParallelRequests + ) { } From 81a4fff89a4f4b07f071f806dfac4e70b7db7a52 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 22 Oct 2024 13:18:18 +0100 Subject: [PATCH 46/81] Remove test data from artifacts --- .github/workflows/build.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8342fd030..4471bf5f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,13 +149,6 @@ jobs: name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }} path: build/clio_*tests - - name: Upload test data - if: ${{ !matrix.code_coverage }} - uses: actions/upload-artifact@v4 - with: - name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }} - path: build/tests/unit/test_data - - name: Save cache uses: ./.github/actions/save_cache with: @@ -219,11 +212,6 @@ jobs: with: name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }} - - uses: actions/download-artifact@v4 - with: - name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }} - path: tests/unit/test_data - - name: Run clio_tests run: | chmod +x ./clio_tests From bc17152c0c7a9ef81a981eaf1198170362a60769 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 22 Oct 2024 14:34:51 +0100 Subject: [PATCH 47/81] Improving test coverage --- tests/unit/web/ng/ServerTests.cpp | 64 ++++++++++++++++++- .../web/ng/impl/ConnectionHandlerTests.cpp | 16 +++++ .../unit/web/ng/impl/HttpConnectionTests.cpp | 2 + 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/tests/unit/web/ng/ServerTests.cpp b/tests/unit/web/ng/ServerTests.cpp index 87120ff63..80345b89d 100644 --- a/tests/unit/web/ng/ServerTests.cpp +++ b/tests/unit/web/ng/ServerTests.cpp @@ -21,6 +21,7 @@ #include "util/AssignRandomPort.hpp" #include "util/LoggerFixtures.hpp" #include "util/NameGenerator.hpp" +#include "util/Taggable.hpp" #include "util/TestHttpClient.hpp" #include "util/TestWebSocketClient.hpp" #include "util/config/Config.hpp" @@ -30,6 +31,8 @@ #include "web/ng/Server.hpp" #include +#include +#include #include #include #include @@ -45,6 +48,7 @@ #include #include #include +#include using namespace web::ng; @@ -71,6 +75,15 @@ INSTANTIATE_TEST_CASE_P( MakeServerTests, MakeServerTest, testing::Values( + MakeServerTestBundle{ + "NoIp", + R"json( + { + "server": {"port": 12345} + } + )json", + false + }, MakeServerTestBundle{ "BadEndpoint", R"json( @@ -159,7 +172,18 @@ struct ServerTest : SyncAsioContextTest { wsHandler_; }; -struct ServerTestBundle { +TEST_F(ServerTest, BadEndpoint) +{ + boost::asio::ip::tcp::endpoint endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0}; + impl::ConnectionHandler connectionHandler{impl::ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt}; + util::TagDecoratorFactory tagDecoratorFactory{util::Config{boost::json::value{}}}; + Server server{ctx, endpoint, std::nullopt, std::move(connectionHandler), tagDecoratorFactory}; + auto maybeError = server.run(); + ASSERT_TRUE(maybeError.has_value()); + EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor")); +} + +struct ServerHttpTestBundle { std::string testName; http::verb method; @@ -177,7 +201,23 @@ struct ServerTestBundle { } }; -struct ServerHttpTest : ServerTest, testing::WithParamInterface {}; +struct ServerHttpTest : ServerTest, testing::WithParamInterface {}; + +TEST_F(ServerHttpTest, ClientDisconnects) +{ + HttpAsyncClient client{ctx}; + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto maybeError = + client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + client.disconnect(); + ctx.stop(); + }); + + server_->run(); + runContext(); +} TEST_P(ServerHttpTest, RequestResponse) { @@ -229,10 +269,28 @@ TEST_P(ServerHttpTest, RequestResponse) INSTANTIATE_TEST_SUITE_P( ServerHttpTests, ServerHttpTest, - testing::Values(ServerTestBundle{"GET", http::verb::get}, ServerTestBundle{"POST", http::verb::post}), + testing::Values(ServerHttpTestBundle{"GET", http::verb::get}, ServerHttpTestBundle{"POST", http::verb::post}), tests::util::NameGenerator ); +TEST_F(ServerTest, WsClientDisconnects) +{ + WebSocketAsyncClient client{ctx}; + + boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) { + auto maybeError = + client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + + client.close(); + ctx.stop(); + }); + + server_->run(); + + runContext(); +} + TEST_F(ServerTest, WsRequestResponse) { WebSocketAsyncClient client{ctx}; diff --git a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp index 8306cddf6..c12c8d4a4 100644 --- a/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp +++ b/tests/unit/web/ng/impl/ConnectionHandlerTests.cpp @@ -162,6 +162,22 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send) }); } +TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadMethod_Send) +{ + EXPECT_CALL(*mockConnection_, receive) + .WillOnce(Return(makeRequest(http::request{http::verb::acl, "/", 11}))) + .WillOnce(Return(makeError(http::error::end_of_stream))); + + EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) { + EXPECT_EQ(response.message(), "Unsupported http method"); + return std::nullopt; + }); + + runSpawn([this](boost::asio::yield_context yield) { + connectionHandler_.processConnection(std::move(mockConnection_), yield); + }); +} + TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send) { testing::StrictMock> diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp index 330dae387..4a31c3702 100644 --- a/tests/unit/web/ng/impl/HttpConnectionTests.cpp +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -95,6 +95,8 @@ TEST_F(HttpConnectionTests, Receive) runSpawn([this](boost::asio::yield_context yield) { auto connection = acceptConnection(yield); + EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip(); + auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100}); ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message(); ASSERT_TRUE(expectedRequest->isHttp()); From 8394b3b4850306242fc6024e8cad2544148790da Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 23 Oct 2024 14:19:55 +0100 Subject: [PATCH 48/81] Fix review issues --- src/web/ng/Request.cpp | 10 ++++---- src/web/ng/Request.hpp | 2 +- src/web/ng/Response.cpp | 28 +++++++++++++---------- src/web/ng/Response.hpp | 2 +- src/web/ng/impl/Concepts.hpp | 4 ++-- src/web/ng/impl/ConnectionHandler.cpp | 6 ++--- tests/common/util/TestHttpClient.cpp | 2 +- tests/common/util/TestWebSocketClient.cpp | 2 +- tests/unit/web/ng/RequestTests.cpp | 8 +++---- tests/unit/web/ng/ServerTests.cpp | 8 +++---- 10 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/web/ng/Request.cpp b/src/web/ng/Request.cpp index 9b5384775..1a60736ba 100644 --- a/src/web/ng/Request.cpp +++ b/src/web/ng/Request.cpp @@ -39,7 +39,7 @@ template std::optional getHeaderValue(HeadersType const& headers, HeaderNameType const& headerName) { - auto it = headers.find(headerName); + auto const it = headers.find(headerName); if (it == headers.end()) return std::nullopt; return it->value(); @@ -60,15 +60,15 @@ Request::Method Request::method() const { if (not isHttp()) - return Method::WEBSOCKET; + return Method::Websocket; switch (httpRequest().method()) { case boost::beast::http::verb::get: - return Method::GET; + return Method::Get; case boost::beast::http::verb::post: - return Method::POST; + return Method::Post; default: - return Method::UNSUPPORTED; + return Method::Unsupported; } } diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 01a5639ce..32de4e3cb 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -70,7 +70,7 @@ class Request { * @brief Method of the request. * @note WEBSOCKET is not a real method, it is used to distinguish WebSocket requests from HTTP requests. */ - enum class Method { GET, POST, WEBSOCKET, UNSUPPORTED }; + enum class Method { Get, Post, Websocket, Unsupported }; /** * @brief Get the method of the request. diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp index a62318072..da7d2d164 100644 --- a/src/web/ng/Response.cpp +++ b/src/web/ng/Response.cpp @@ -35,8 +35,10 @@ #include #include #include +#include #include +namespace http = boost::beast::http; namespace web::ng { namespace { @@ -45,9 +47,9 @@ std::string_view asString(Response::HttpData::ContentType type) { switch (type) { - case Response::HttpData::ContentType::TEXT_HTML: + case Response::HttpData::ContentType::TextHtml: return "text/html"; - case Response::HttpData::ContentType::APPLICATION_JSON: + case Response::HttpData::ContentType::ApplicationJson: return "application/json"; } ASSERT(false, "Unknown content type"); @@ -56,14 +58,16 @@ asString(Response::HttpData::ContentType type) template std::optional -makeHttpData(boost::beast::http::status status, Request const& request) +makeHttpData(http::status status, Request const& request) { if (request.isHttp()) { auto const& httpRequest = request.asHttpRequest()->get(); + auto constexpr contentType = std::is_same_v, std::string> + ? Response::HttpData::ContentType::TextHtml + : Response::HttpData::ContentType::ApplicationJson; return Response::HttpData{ .status = status, - .contentType = std::is_same_v ? Response::HttpData::ContentType::TEXT_HTML - : Response::HttpData::ContentType::APPLICATION_JSON, + .contentType = contentType, .keepAlive = httpRequest.keep_alive(), .version = httpRequest.version() }; @@ -72,12 +76,12 @@ makeHttpData(boost::beast::http::status status, Request const& request) } } // namespace -Response::Response(boost::beast::http::status status, std::string message, Request const& request) +Response::Response(http::status status, std::string message, Request const& request) : message_(std::move(message)), httpData_{makeHttpData(status, request)} { } -Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request) +Response::Response(http::status status, boost::json::object const& message, Request const& request) : message_(boost::json::serialize(message)), httpData_{makeHttpData(status, request)} { } @@ -88,14 +92,14 @@ Response::message() const return message_; } -boost::beast::http::response +http::response Response::intoHttpResponse() && { ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response"); - boost::beast::http::response result{httpData_->status, httpData_->version}; - result.set(boost::beast::http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString())); - result.set(boost::beast::http::field::content_type, asString(httpData_->contentType)); + http::response result{httpData_->status, httpData_->version}; + result.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString())); + result.set(http::field::content_type, asString(httpData_->contentType)); result.keep_alive(httpData_->keepAlive); result.body() = std::move(message_); result.prepare_payload(); @@ -105,7 +109,7 @@ Response::intoHttpResponse() && boost::asio::const_buffer Response::asConstBuffer() const& { - ASSERT(not httpData_.has_value(), "Loosing existing http data"); + ASSERT(not httpData_.has_value(), "Losing existing http data"); return boost::asio::buffer(message_.data(), message_.size()); } diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 3e0f95a2c..33348e5a6 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -43,7 +43,7 @@ class Response { /** * @brief The content type of the response. */ - enum class ContentType { APPLICATION_JSON, TEXT_HTML }; + enum class ContentType { ApplicationJson, TextHtml }; boost::beast::http::status status; ///< The HTTP status. ContentType contentType; ///< The content type. diff --git a/src/web/ng/impl/Concepts.hpp b/src/web/ng/impl/Concepts.hpp index 801430dd0..7c0985d4a 100644 --- a/src/web/ng/impl/Concepts.hpp +++ b/src/web/ng/impl/Concepts.hpp @@ -27,9 +27,9 @@ namespace web::ng::impl { template -concept IsTcpStream = std::is_same_v; +concept IsTcpStream = std::is_same_v, boost::beast::tcp_stream>; template -concept IsSslTcpStream = std::is_same_v>; +concept IsSslTcpStream = std::is_same_v, boost::asio::ssl::stream>; } // namespace web::ng::impl diff --git a/src/web/ng/impl/ConnectionHandler.cpp b/src/web/ng/impl/ConnectionHandler.cpp index f87107fcd..20b321583 100644 --- a/src/web/ng/impl/ConnectionHandler.cpp +++ b/src/web/ng/impl/ConnectionHandler.cpp @@ -271,11 +271,11 @@ ConnectionHandler::handleRequest( ) { switch (request.method()) { - case Request::Method::GET: + case Request::Method::Get: return handleHttpRequest(connectionContext, getHandlers_, request, yield); - case Request::Method::POST: + case Request::Method::Post: return handleHttpRequest(connectionContext, postHandlers_, request, yield); - case Request::Method::WEBSOCKET: + case Request::Method::Websocket: return handleWsRequest(connectionContext, wsHandler_, request, yield); default: return Response{boost::beast::http::status::bad_request, "Unsupported http method", request}; diff --git a/tests/common/util/TestHttpClient.cpp b/tests/common/util/TestHttpClient.cpp index 45445c05a..fbe975cef 100644 --- a/tests/common/util/TestHttpClient.cpp +++ b/tests/common/util/TestHttpClient.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2023, the clio developers. + Copyright (c) 2024, the clio developers. Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/tests/common/util/TestWebSocketClient.cpp b/tests/common/util/TestWebSocketClient.cpp index 7ddee4318..f34cffbeb 100644 --- a/tests/common/util/TestWebSocketClient.cpp +++ b/tests/common/util/TestWebSocketClient.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2023, the clio developers. + Copyright (c) 2024, the clio developers. Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/tests/unit/web/ng/RequestTests.cpp b/tests/unit/web/ng/RequestTests.cpp index 0e6f2ef71..3478061fd 100644 --- a/tests/unit/web/ng/RequestTests.cpp +++ b/tests/unit/web/ng/RequestTests.cpp @@ -54,22 +54,22 @@ INSTANTIATE_TEST_SUITE_P( RequestMethodTestBundle{ .testName = "HttpGet", .request = Request{http::request{http::verb::get, "/", 11}}, - .expectedMethod = Request::Method::GET, + .expectedMethod = Request::Method::Get, }, RequestMethodTestBundle{ .testName = "HttpPost", .request = Request{http::request{http::verb::post, "/", 11}}, - .expectedMethod = Request::Method::POST, + .expectedMethod = Request::Method::Post, }, RequestMethodTestBundle{ .testName = "WebSocket", .request = Request{"websocket message", Request::HttpHeaders{}}, - .expectedMethod = Request::Method::WEBSOCKET, + .expectedMethod = Request::Method::Websocket, }, RequestMethodTestBundle{ .testName = "Unsupported", .request = Request{http::request{http::verb::acl, "/", 11}}, - .expectedMethod = Request::Method::UNSUPPORTED, + .expectedMethod = Request::Method::Unsupported, } ), tests::util::NameGenerator diff --git a/tests/unit/web/ng/ServerTests.cpp b/tests/unit/web/ng/ServerTests.cpp index 80345b89d..459f11cab 100644 --- a/tests/unit/web/ng/ServerTests.cpp +++ b/tests/unit/web/ng/ServerTests.cpp @@ -192,11 +192,11 @@ struct ServerHttpTestBundle { { switch (method) { case http::verb::get: - return Request::Method::GET; + return Request::Method::Get; case http::verb::post: - return Request::Method::POST; + return Request::Method::Post; default: - return Request::Method::UNSUPPORTED; + return Request::Method::Unsupported; } } }; @@ -319,7 +319,7 @@ TEST_F(ServerTest, WsRequestResponse) .Times(3) .WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) { EXPECT_FALSE(receivedRequest.isHttp()); - EXPECT_EQ(receivedRequest.method(), Request::Method::WEBSOCKET); + EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket); EXPECT_EQ(receivedRequest.message(), requestMessage_); EXPECT_EQ(receivedRequest.target(), std::nullopt); From 333ed86dbaec1d52b25f30b2e109174545c5ce63 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 23 Oct 2024 14:27:32 +0100 Subject: [PATCH 49/81] Make doxygen happy --- src/web/ng/Response.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp index da7d2d164..70c915065 100644 --- a/src/web/ng/Response.cpp +++ b/src/web/ng/Response.cpp @@ -76,12 +76,12 @@ makeHttpData(http::status status, Request const& request) } } // namespace -Response::Response(http::status status, std::string message, Request const& request) +Response::Response(boost::beast::http::status status, std::string message, Request const& request) : message_(std::move(message)), httpData_{makeHttpData(status, request)} { } -Response::Response(http::status status, boost::json::object const& message, Request const& request) +Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request) : message_(boost::json::serialize(message)), httpData_{makeHttpData(status, request)} { } From a7a40d4d984ff420b975ff61bd4984563653d042 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 23 Oct 2024 14:33:11 +0100 Subject: [PATCH 50/81] Fix naming in comment --- src/web/ng/Request.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/ng/Request.hpp b/src/web/ng/Request.hpp index 32de4e3cb..060181566 100644 --- a/src/web/ng/Request.hpp +++ b/src/web/ng/Request.hpp @@ -68,7 +68,7 @@ class Request { /** * @brief Method of the request. - * @note WEBSOCKET is not a real method, it is used to distinguish WebSocket requests from HTTP requests. + * @note Websocket is not a real method, it is used to distinguish WebSocket requests from HTTP requests. */ enum class Method { Get, Post, Websocket, Unsupported }; From 49503f50a27838efc657e1549608e14cd5dbbc09 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 23 Oct 2024 16:05:55 +0100 Subject: [PATCH 51/81] Add --ng-web-server flag --- src/app/CliArgs.cpp | 4 +++- src/app/CliArgs.hpp | 7 +++---- tests/unit/app/CliArgsTests.cpp | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/app/CliArgs.cpp b/src/app/CliArgs.cpp index 4780f2f69..b3a57a4d1 100644 --- a/src/app/CliArgs.cpp +++ b/src/app/CliArgs.cpp @@ -44,6 +44,7 @@ CliArgs::parse(int argc, char const* argv[]) ("help,h", "print help message and exit") ("version,v", "print version and exit") ("conf,c", po::value()->default_value(defaultConfigPath), "configuration file") + ("ng-web-server,w", "Use ng-web-server") ; // clang-format on po::positional_options_description positional; @@ -64,7 +65,8 @@ CliArgs::parse(int argc, char const* argv[]) } auto configPath = parsed["conf"].as(); - return Action{Action::Run{std::move(configPath)}}; + return Action{Action::Run{.configPath = std::move(configPath), .useNgWebServer = parsed.count("ng-web-server") != 0} + }; } } // namespace app diff --git a/src/app/CliArgs.hpp b/src/app/CliArgs.hpp index bc7dd738c..77dd8eb76 100644 --- a/src/app/CliArgs.hpp +++ b/src/app/CliArgs.hpp @@ -43,14 +43,13 @@ class CliArgs { public: /** @brief Run action. */ struct Run { - /** @brief Configuration file path. */ - std::string configPath; + std::string configPath; ///< Configuration file path. + bool useNgWebServer; ///< Whether to use a ng web server }; /** @brief Exit action. */ struct Exit { - /** @brief Exit code. */ - int exitCode; + int exitCode; ///< Exit code. }; /** diff --git a/tests/unit/app/CliArgsTests.cpp b/tests/unit/app/CliArgsTests.cpp index cf3b1dc31..69de39c67 100644 --- a/tests/unit/app/CliArgsTests.cpp +++ b/tests/unit/app/CliArgsTests.cpp @@ -41,11 +41,27 @@ TEST_F(CliArgsTests, Parse_NoArgs) int const returnCode = 123; EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) { EXPECT_EQ(run.configPath, CliArgs::defaultConfigPath); + EXPECT_FALSE(run.useNgWebServer); return returnCode; }); EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode); } +TEST_F(CliArgsTests, Parse_NgWebServer) +{ + for (auto& argv : {std::array{"clio_server", "-w"}, std::array{"clio_server", "--ng-web-server"}}) { + auto const action = CliArgs::parse(argv.size(), const_cast(argv.data())); + + int const returnCode = 123; + EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) { + EXPECT_EQ(run.configPath, CliArgs::defaultConfigPath); + EXPECT_TRUE(run.useNgWebServer); + return returnCode; + }); + EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode); + } +} + TEST_F(CliArgsTests, Parse_VersionHelp) { for (auto& argv : From 4de29daef462060fbae21464fe1f96e5fed8f224 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 24 Oct 2024 14:53:06 +0100 Subject: [PATCH 52/81] Add Prometheus handler with new web server --- src/app/ClioApplication.cpp | 105 ++++++++++++++++++++++++++++ src/app/ClioApplication.hpp | 8 +++ src/main/Main.cpp | 4 ++ src/web/ng/Connection.cpp | 6 ++ src/web/ng/Connection.hpp | 8 +++ src/web/ng/Response.cpp | 95 ++++++++++++++----------- src/web/ng/Response.hpp | 35 ++++------ src/web/ng/impl/WsConnection.hpp | 2 +- tests/unit/web/ng/ResponseTests.cpp | 8 +-- 9 files changed, 204 insertions(+), 67 deletions(-) diff --git a/src/app/ClioApplication.cpp b/src/app/ClioApplication.cpp index 95c4b33a0..e81cae6b9 100644 --- a/src/app/ClioApplication.cpp +++ b/src/app/ClioApplication.cpp @@ -29,22 +29,30 @@ #include "rpc/RPCEngine.hpp" #include "rpc/WorkQueue.hpp" #include "rpc/common/impl/HandlerProvider.hpp" +#include "util/Assert.hpp" #include "util/build/Build.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" +#include "util/prometheus/Http.hpp" #include "util/prometheus/Prometheus.hpp" #include "web/RPCServerHandler.hpp" #include "web/Server.hpp" #include "web/dosguard/DOSGuard.hpp" #include "web/dosguard/IntervalSweepHandler.hpp" #include "web/dosguard/WhitelistHandler.hpp" +#include "web/ng/Connection.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" +#include "web/ng/Server.hpp" #include +#include #include #include #include #include +#include #include namespace app { @@ -139,4 +147,101 @@ ClioApplication::run() return EXIT_SUCCESS; } +int +ClioApplication::runWithNgWebServer() +{ + auto const threads = config_.valueOr("io_threads", 2); + if (threads <= 0) { + LOG(util::LogService::fatal()) << "io_threads is less than 1"; + return EXIT_FAILURE; + } + LOG(util::LogService::info()) << "Number of io threads = " << threads; + + // IO context to handle all incoming requests, as well as other things. + // This is not the only io context in the application. + boost::asio::io_context ioc{threads}; + + // Rate limiter, to prevent abuse + auto whitelistHandler = web::dosguard::WhitelistHandler{config_}; + auto dosGuard = web::dosguard::DOSGuard{config_, whitelistHandler}; + auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard}; + + // Interface to the database + auto backend = data::make_Backend(config_); + + // Manages clients subscribed to streams + auto subscriptions = feed::SubscriptionManager::make_SubscriptionManager(config_, backend); + + // Tracks which ledgers have been validated by the network + auto ledgers = etl::NetworkValidatedLedgers::make_ValidatedLedgers(); + + // Handles the connection to one or more rippled nodes. + // ETL uses the balancer to extract data. + // The server uses the balancer to forward RPCs to a rippled node. + // The balancer itself publishes to streams (transactions_proposed and accounts_proposed) + auto balancer = etl::LoadBalancer::make_LoadBalancer(config_, ioc, backend, subscriptions, ledgers); + + // ETL is responsible for writing and publishing to streams. In read-only mode, ETL only publishes + auto etl = etl::ETLService::make_ETLService(config_, ioc, backend, subscriptions, balancer, ledgers); + + auto workQueue = rpc::WorkQueue::make_WorkQueue(config_); + auto counters = rpc::Counters::make_Counters(workQueue); + auto const amendmentCenter = std::make_shared(backend); + auto const handlerProvider = std::make_shared( + config_, backend, subscriptions, balancer, etl, amendmentCenter, counters + ); + + using RPCEngineType = rpc::RPCEngine; + auto const rpcEngine = + RPCEngineType::make_RPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider); + + auto handler = + std::make_shared>(config_, backend, rpcEngine, etl); + + auto expectedAdminVerifier = web::impl::make_AdminVerificationStrategy(config_); + if (not expectedAdminVerifier.has_value()) { + LOG(util::LogService::error()) << "Error admin verifier: " << expectedAdminVerifier.error(); + return EXIT_FAILURE; + } + auto adminVerifier = std::move(expectedAdminVerifier).value(); + + auto httpServer = web::ng::make_Server(config_, ioc); + + if (not httpServer.has_value()) { + LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error(); + return EXIT_FAILURE; + } + + httpServer->onGet( + "/metrics", + [adminVerifier]( + web::ng::Request const& request, web::ng::ConnectionContext context, boost::asio::yield_context + ) -> web::ng::Response { + auto const maybeHttpRequest = request.asHttpRequest(); + ASSERT(maybeHttpRequest.has_value(), "Got not a http request in Get"); + auto const& httpRequest = maybeHttpRequest->get(); + + // FIXME(#1702): Using veb server thread to handle prometheus request. Better to post on work queue. + auto maybeResponse = util::prometheus::handlePrometheusRequest( + httpRequest, adminVerifier->isAdmin(httpRequest, context.ip()) + ); + ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus"); + return web::ng::Response{std::move(maybeResponse).value(), request}; + } + ); + + auto const maybeError = httpServer->run(); + if (maybeError.has_value()) { + LOG(util::LogService::error()) << "Error starting web server: " << *maybeError; + return EXIT_FAILURE; + } + + // Blocks until stopped. + // When stopped, shared_ptrs fall out of scope + // Calls destructors on all resources, and destructs in order + start(ioc, threads); + + return EXIT_SUCCESS; +} + } // namespace app diff --git a/src/app/ClioApplication.hpp b/src/app/ClioApplication.hpp index 6bb31ea7a..328050644 100644 --- a/src/app/ClioApplication.hpp +++ b/src/app/ClioApplication.hpp @@ -46,6 +46,14 @@ class ClioApplication { */ int run(); + + /** + * @brief Run the application with the ng web server + * + * @return exit code + */ + int + runWithNgWebServer(); }; } // namespace app diff --git a/src/main/Main.cpp b/src/main/Main.cpp index f637402df..aecbcfdf0 100644 --- a/src/main/Main.cpp +++ b/src/main/Main.cpp @@ -44,6 +44,10 @@ try { } util::LogService::init(config); app::ClioApplication clio{config}; + + if (run.useNgWebServer) + return clio.runWithNgWebServer(); + return clio.run(); } ); diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index 4bfb4bc22..82c7ee6f1 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -54,4 +54,10 @@ ConnectionContext::ConnectionContext(Connection const& connection) : connection_ { } +std::string const& +ConnectionContext::ip() const +{ + return connection_.get().ip(); +} + } // namespace web::ng diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 45b20052e..f89bb6622 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -143,6 +143,14 @@ class ConnectionContext { * @param connection The connection. */ explicit ConnectionContext(Connection const& connection); + + /** + * @brief Get the ip of the client. + * + * @return The ip of the client. + */ + std::string const& + ip() const; }; } // namespace web::ng diff --git a/src/web/ng/Response.cpp b/src/web/ng/Response.cpp index 70c915065..ab2a8b240 100644 --- a/src/web/ng/Response.cpp +++ b/src/web/ng/Response.cpp @@ -20,6 +20,7 @@ #include "web/ng/Response.hpp" #include "util/Assert.hpp" +#include "util/OverloadSet.hpp" #include "util/build/Build.hpp" #include "web/ng/Request.hpp" @@ -34,83 +35,97 @@ #include #include -#include #include #include +#include namespace http = boost::beast::http; namespace web::ng { namespace { -std::string_view -asString(Response::HttpData::ContentType type) +template +consteval bool +isString() { - switch (type) { - case Response::HttpData::ContentType::TextHtml: - return "text/html"; - case Response::HttpData::ContentType::ApplicationJson: - return "application/json"; - } - ASSERT(false, "Unknown content type"); - std::unreachable(); + return std::is_same_v; +} + +http::response +prepareResponse(http::response response, http::request const& request) +{ + response.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString())); + response.keep_alive(request.keep_alive()); + response.prepare_payload(); + return response; } template -std::optional -makeHttpData(http::status status, Request const& request) +std::variant, std::string> +makeData(http::status status, MessageType message, Request const& request) { - if (request.isHttp()) { - auto const& httpRequest = request.asHttpRequest()->get(); - auto constexpr contentType = std::is_same_v, std::string> - ? Response::HttpData::ContentType::TextHtml - : Response::HttpData::ContentType::ApplicationJson; - return Response::HttpData{ - .status = status, - .contentType = contentType, - .keepAlive = httpRequest.keep_alive(), - .version = httpRequest.version() - }; + std::string body; + if constexpr (isString()) { + body = std::move(message); + } else { + body = boost::json::serialize(message); } - return std::nullopt; + + if (not request.isHttp()) + return std::move(body); + + auto const& httpRequest = request.asHttpRequest()->get(); + std::string const contentType = isString() ? "text/html" : "application/json"; + + http::response result{status, httpRequest.version(), std::move(body)}; + result.set(http::field::content_type, contentType); + return prepareResponse(std::move(result), httpRequest); } + } // namespace Response::Response(boost::beast::http::status status, std::string message, Request const& request) - : message_(std::move(message)), httpData_{makeHttpData(status, request)} + : data_{makeData(status, std::move(message), request)} { } Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request) - : message_(boost::json::serialize(message)), httpData_{makeHttpData(status, request)} + : data_{makeData(status, message, request)} +{ +} + +Response::Response(boost::beast::http::response response, Request const& request) { + ASSERT(request.isHttp(), "Request must be HTTP to construct response from HTTP response"); + data_ = prepareResponse(std::move(response), request.asHttpRequest()->get()); } std::string const& Response::message() const { - return message_; + return std::visit( + util::OverloadSet{ + [](http::response const& response) -> std::string const& { return response.body(); }, + [](std::string const& message) -> std::string const& { return message; }, + }, + data_ + ); } http::response Response::intoHttpResponse() && { - ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response"); - - http::response result{httpData_->status, httpData_->version}; - result.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString())); - result.set(http::field::content_type, asString(httpData_->contentType)); - result.keep_alive(httpData_->keepAlive); - result.body() = std::move(message_); - result.prepare_payload(); - return result; + ASSERT(std::holds_alternative>(data_), "Response must contain HTTP data"); + + return std::move(std::get>(data_)); } boost::asio::const_buffer -Response::asConstBuffer() const& +Response::intoWsResponse() const& { - ASSERT(not httpData_.has_value(), "Losing existing http data"); - return boost::asio::buffer(message_.data(), message_.size()); + ASSERT(std::holds_alternative(data_), "Response must contain WebSocket data"); + auto const& message = std::get(data_); + return boost::asio::buffer(message.data(), message.size()); } } // namespace web::ng diff --git a/src/web/ng/Response.hpp b/src/web/ng/Response.hpp index 33348e5a6..5ab195237 100644 --- a/src/web/ng/Response.hpp +++ b/src/web/ng/Response.hpp @@ -27,8 +27,8 @@ #include #include -#include #include +#include namespace web::ng { /** @@ -36,30 +36,13 @@ namespace web::ng { */ class Response { public: - /** - * @brief The data for an HTTP response. - */ - struct HttpData { - /** - * @brief The content type of the response. - */ - enum class ContentType { ApplicationJson, TextHtml }; - - boost::beast::http::status status; ///< The HTTP status. - ContentType contentType; ///< The content type. - bool keepAlive; ///< Whether the connection should be kept alive. - unsigned int version; ///< The HTTP version. - }; - -private: - std::string message_; - std::optional httpData_; + std::variant, std::string> data_; public: /** * @brief Construct a Response from string. Content type will be text/html. * - * @param status The HTTP status. + * @param status The HTTP status. It will be ignored if request is WebSocket. * @param message The message to send. * @param request The request that triggered this response. Used to determine whether the response should contain * HTTP or WebSocket data. @@ -69,13 +52,21 @@ class Response { /** * @brief Construct a Response from JSON object. Content type will be application/json. * - * @param status The HTTP status. + * @param status The HTTP status. It will be ignored if request is WebSocket. * @param message The message to send. * @param request The request that triggered this response. Used to determine whether the response should contain * HTTP or WebSocket */ Response(boost::beast::http::status status, boost::json::object const& message, Request const& request); + /** + * @brief Construct a Response from HTTP response. + * + * @param response The HTTP response. + * @param request The request that triggered this response. It must be an HTTP request. + */ + Response(boost::beast::http::response response, Request const& request); + /** * @brief Get the message of the response. * @@ -100,7 +91,7 @@ class Response { * @return The message of the response as a const buffer. */ boost::asio::const_buffer - asConstBuffer() const&; + intoWsResponse() const&; }; } // namespace web::ng diff --git a/src/web/ng/impl/WsConnection.hpp b/src/web/ng/impl/WsConnection.hpp index 956f15388..6b32e2144 100644 --- a/src/web/ng/impl/WsConnection.hpp +++ b/src/web/ng/impl/WsConnection.hpp @@ -119,7 +119,7 @@ class WsConnection : public Connection { ) override { auto error = util::withTimeout( - [this, &response](auto&& yield) { stream_.async_write(response.asConstBuffer(), yield); }, yield, timeout + [this, &response](auto&& yield) { stream_.async_write(response.intoWsResponse(), yield); }, yield, timeout ); if (error) return error; diff --git a/tests/unit/web/ng/ResponseTests.cpp b/tests/unit/web/ng/ResponseTests.cpp index f1a2cea09..c118f1583 100644 --- a/tests/unit/web/ng/ResponseTests.cpp +++ b/tests/unit/web/ng/ResponseTests.cpp @@ -47,11 +47,11 @@ TEST_F(ResponseDeathTest, intoHttpResponseWithoutHttpData) EXPECT_DEATH(std::move(response).intoHttpResponse(), ""); } -TEST_F(ResponseDeathTest, asConstBufferWithHttpData) +TEST_F(ResponseDeathTest, intoWsResponseWithHttpData) { Request const request{http::request{http::verb::get, "/", 11}}; web::ng::Response response{boost::beast::http::status::ok, "message", request}; - EXPECT_DEATH(response.asConstBuffer(), ""); + EXPECT_DEATH(response.intoWsResponse(), ""); } struct ResponseTest : testing::Test { @@ -105,7 +105,7 @@ TEST_F(ResponseTest, asConstBuffer) std::string const responseMessage = "response message"; web::ng::Response response{responseStatus_, responseMessage, request}; - auto const buffer = response.asConstBuffer(); + auto const buffer = response.intoWsResponse(); EXPECT_EQ(buffer.size(), responseMessage.size()); std::string const messageFromBuffer{static_cast(buffer.data()), buffer.size()}; @@ -118,7 +118,7 @@ TEST_F(ResponseTest, asConstBufferJson) boost::json::object const responseMessage{{"key", "value"}}; web::ng::Response response{responseStatus_, responseMessage, request}; - auto const buffer = response.asConstBuffer(); + auto const buffer = response.intoWsResponse(); EXPECT_EQ(buffer.size(), boost::json::serialize(responseMessage).size()); std::string const messageFromBuffer{static_cast(buffer.data()), buffer.size()}; From 1844f66122a253581cde8c0e1c4b680196a03d2c Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 24 Oct 2024 15:15:38 +0100 Subject: [PATCH 53/81] Merge run methods --- src/app/ClioApplication.cpp | 133 ++++++++++++------------------------ src/app/ClioApplication.hpp | 10 +-- src/main/Main.cpp | 5 +- 3 files changed, 45 insertions(+), 103 deletions(-) diff --git a/src/app/ClioApplication.cpp b/src/app/ClioApplication.cpp index e81cae6b9..312666187 100644 --- a/src/app/ClioApplication.cpp +++ b/src/app/ClioApplication.cpp @@ -87,7 +87,7 @@ ClioApplication::ClioApplication(util::Config const& config) : config_(config), } int -ClioApplication::run() +ClioApplication::run(bool const useNgWebServer) { auto const threads = config_.valueOr("io_threads", 2); if (threads <= 0) { @@ -137,104 +137,55 @@ ClioApplication::run() // Init the web server auto handler = std::make_shared>(config_, backend, rpcEngine, etl); - auto const httpServer = web::make_HttpServer(config_, ioc, dosGuard, handler); - - // Blocks until stopped. - // When stopped, shared_ptrs fall out of scope - // Calls destructors on all resources, and destructs in order - start(ioc, threads); - - return EXIT_SUCCESS; -} - -int -ClioApplication::runWithNgWebServer() -{ - auto const threads = config_.valueOr("io_threads", 2); - if (threads <= 0) { - LOG(util::LogService::fatal()) << "io_threads is less than 1"; - return EXIT_FAILURE; - } - LOG(util::LogService::info()) << "Number of io threads = " << threads; - - // IO context to handle all incoming requests, as well as other things. - // This is not the only io context in the application. - boost::asio::io_context ioc{threads}; - - // Rate limiter, to prevent abuse - auto whitelistHandler = web::dosguard::WhitelistHandler{config_}; - auto dosGuard = web::dosguard::DOSGuard{config_, whitelistHandler}; - auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard}; - - // Interface to the database - auto backend = data::make_Backend(config_); - - // Manages clients subscribed to streams - auto subscriptions = feed::SubscriptionManager::make_SubscriptionManager(config_, backend); - - // Tracks which ledgers have been validated by the network - auto ledgers = etl::NetworkValidatedLedgers::make_ValidatedLedgers(); - // Handles the connection to one or more rippled nodes. - // ETL uses the balancer to extract data. - // The server uses the balancer to forward RPCs to a rippled node. - // The balancer itself publishes to streams (transactions_proposed and accounts_proposed) - auto balancer = etl::LoadBalancer::make_LoadBalancer(config_, ioc, backend, subscriptions, ledgers); - - // ETL is responsible for writing and publishing to streams. In read-only mode, ETL only publishes - auto etl = etl::ETLService::make_ETLService(config_, ioc, backend, subscriptions, balancer, ledgers); - - auto workQueue = rpc::WorkQueue::make_WorkQueue(config_); - auto counters = rpc::Counters::make_Counters(workQueue); - auto const amendmentCenter = std::make_shared(backend); - auto const handlerProvider = std::make_shared( - config_, backend, subscriptions, balancer, etl, amendmentCenter, counters - ); + if (useNgWebServer) { + auto expectedAdminVerifier = web::impl::make_AdminVerificationStrategy(config_); + if (not expectedAdminVerifier.has_value()) { + LOG(util::LogService::error()) << "Error admin verifier: " << expectedAdminVerifier.error(); + return EXIT_FAILURE; + } + auto adminVerifier = std::move(expectedAdminVerifier).value(); - using RPCEngineType = rpc::RPCEngine; - auto const rpcEngine = - RPCEngineType::make_RPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider); + auto httpServer = web::ng::make_Server(config_, ioc); - auto handler = - std::make_shared>(config_, backend, rpcEngine, etl); + if (not httpServer.has_value()) { + LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error(); + return EXIT_FAILURE; + } - auto expectedAdminVerifier = web::impl::make_AdminVerificationStrategy(config_); - if (not expectedAdminVerifier.has_value()) { - LOG(util::LogService::error()) << "Error admin verifier: " << expectedAdminVerifier.error(); - return EXIT_FAILURE; - } - auto adminVerifier = std::move(expectedAdminVerifier).value(); + httpServer->onGet( + "/metrics", + [adminVerifier]( + web::ng::Request const& request, web::ng::ConnectionContext context, boost::asio::yield_context + ) -> web::ng::Response { + auto const maybeHttpRequest = request.asHttpRequest(); + ASSERT(maybeHttpRequest.has_value(), "Got not a http request in Get"); + auto const& httpRequest = maybeHttpRequest->get(); + + // FIXME(#1702): Using veb server thread to handle prometheus request. Better to post on work queue. + auto maybeResponse = util::prometheus::handlePrometheusRequest( + httpRequest, adminVerifier->isAdmin(httpRequest, context.ip()) + ); + ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus"); + return web::ng::Response{std::move(maybeResponse).value(), request}; + } + ); + + auto const maybeError = httpServer->run(); + if (maybeError.has_value()) { + LOG(util::LogService::error()) << "Error starting web server: " << *maybeError; + return EXIT_FAILURE; + } - auto httpServer = web::ng::make_Server(config_, ioc); + // Blocks until stopped. + // When stopped, shared_ptrs fall out of scope + // Calls destructors on all resources, and destructs in order + start(ioc, threads); - if (not httpServer.has_value()) { - LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error(); - return EXIT_FAILURE; + return EXIT_SUCCESS; } - httpServer->onGet( - "/metrics", - [adminVerifier]( - web::ng::Request const& request, web::ng::ConnectionContext context, boost::asio::yield_context - ) -> web::ng::Response { - auto const maybeHttpRequest = request.asHttpRequest(); - ASSERT(maybeHttpRequest.has_value(), "Got not a http request in Get"); - auto const& httpRequest = maybeHttpRequest->get(); - - // FIXME(#1702): Using veb server thread to handle prometheus request. Better to post on work queue. - auto maybeResponse = util::prometheus::handlePrometheusRequest( - httpRequest, adminVerifier->isAdmin(httpRequest, context.ip()) - ); - ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus"); - return web::ng::Response{std::move(maybeResponse).value(), request}; - } - ); - - auto const maybeError = httpServer->run(); - if (maybeError.has_value()) { - LOG(util::LogService::error()) << "Error starting web server: " << *maybeError; - return EXIT_FAILURE; - } + auto const httpServer = web::make_HttpServer(config_, ioc, dosGuard, handler); // Blocks until stopped. // When stopped, shared_ptrs fall out of scope diff --git a/src/app/ClioApplication.hpp b/src/app/ClioApplication.hpp index 328050644..30fbaf8cc 100644 --- a/src/app/ClioApplication.hpp +++ b/src/app/ClioApplication.hpp @@ -42,18 +42,12 @@ class ClioApplication { /** * @brief Run the application * - * @return exit code - */ - int - run(); - - /** - * @brief Run the application with the ng web server + * @param useNgWebServer Whether to use the new web server * * @return exit code */ int - runWithNgWebServer(); + run(bool useNgWebServer); }; } // namespace app diff --git a/src/main/Main.cpp b/src/main/Main.cpp index aecbcfdf0..ffd4dbd96 100644 --- a/src/main/Main.cpp +++ b/src/main/Main.cpp @@ -45,10 +45,7 @@ try { util::LogService::init(config); app::ClioApplication clio{config}; - if (run.useNgWebServer) - return clio.runWithNgWebServer(); - - return clio.run(); + return clio.run(run.useNgWebServer); } ); } catch (std::exception const& e) { From a4769efc633094493d577298a6dfad098afbbb8d Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 25 Oct 2024 15:52:54 +0100 Subject: [PATCH 54/81] Add ng ErrorHandling --- src/web/CMakeLists.txt | 1 + src/web/impl/ErrorHandling.hpp | 1 + src/web/ng/impl/ErrorHandling.cpp | 185 ++++++++++++++++++++++++++++++ src/web/ng/impl/ErrorHandling.hpp | 64 +++++++++++ 4 files changed, 251 insertions(+) create mode 100644 src/web/ng/impl/ErrorHandling.cpp create mode 100644 src/web/ng/impl/ErrorHandling.hpp diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index b5b0ae9f0..414cd1755 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources( dosguard/WhitelistHandler.cpp impl/AdminVerificationStrategy.cpp ng/Connection.cpp + ng/impl/ErrorHandling.cpp ng/impl/ConnectionHandler.cpp ng/impl/ServerSslContext.cpp ng/impl/WsConnection.cpp diff --git a/src/web/impl/ErrorHandling.hpp b/src/web/impl/ErrorHandling.hpp index 464e9bdc9..a431a8588 100644 --- a/src/web/impl/ErrorHandling.hpp +++ b/src/web/impl/ErrorHandling.hpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include diff --git a/src/web/ng/impl/ErrorHandling.cpp b/src/web/ng/impl/ErrorHandling.cpp new file mode 100644 index 000000000..7dc3b90d2 --- /dev/null +++ b/src/web/ng/impl/ErrorHandling.cpp @@ -0,0 +1,185 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "web/ng/impl/ErrorHandling.hpp" + +#include "rpc/Errors.hpp" +#include "rpc/JS.hpp" +#include "util/Assert.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace web::ng::impl { + +namespace { + +boost::json::object +composeError(auto const& error, Request const& rawRequest, std::optional const& request) +{ + auto e = rpc::makeError(error); + + if (request) { + auto const appendFieldIfExist = [&](auto const& field) { + if (request->contains(field) and not request->at(field).is_null()) + e[field] = request->at(field); + }; + + appendFieldIfExist(JS(id)); + + if (not rawRequest.isHttp()) + appendFieldIfExist(JS(api_version)); + + e[JS(request)] = request.value(); + } + + if (not rawRequest.isHttp()) { + return e; + } + return {{JS(result), e}}; +} + +} // namespace + +ErrorHelper::ErrorHelper(Request const& rawRequest, std::optional request) + : rawRequest_{rawRequest}, request_{std::move(request)} +{ +} + +Response +ErrorHelper::makeError(rpc::Status const& err) const +{ + if (not rawRequest_.get().isHttp()) { + return Response{ + boost::beast::http::status::bad_request, + boost::json::serialize(composeError(err, rawRequest_, request_)), + rawRequest_ + }; + } + + // Note: a collection of crutches to match rippled output follows + if (auto const clioCode = std::get_if(&err.code)) { + switch (*clioCode) { + case rpc::ClioError::rpcINVALID_API_VERSION: + return Response{ + boost::beast::http::status::bad_request, + std::string{rpc::getErrorInfo(*clioCode).error}, + rawRequest_ + }; + case rpc::ClioError::rpcCOMMAND_IS_MISSING: + return Response{boost::beast::http::status::bad_request, "Null method", rawRequest_}; + case rpc::ClioError::rpcCOMMAND_IS_EMPTY: + return Response{boost::beast::http::status::bad_request, "method is empty", rawRequest_}; + case rpc::ClioError::rpcCOMMAND_NOT_STRING: + return Response{boost::beast::http::status::bad_request, "method is not string", rawRequest_}; + case rpc::ClioError::rpcPARAMS_UNPARSEABLE: + return Response{boost::beast::http::status::bad_request, "params unparseable", rawRequest_}; + + // others are not applicable but we want a compilation error next time we add one + case rpc::ClioError::rpcUNKNOWN_OPTION: + case rpc::ClioError::rpcMALFORMED_CURRENCY: + case rpc::ClioError::rpcMALFORMED_REQUEST: + case rpc::ClioError::rpcMALFORMED_OWNER: + case rpc::ClioError::rpcMALFORMED_ADDRESS: + case rpc::ClioError::rpcINVALID_HOT_WALLET: + case rpc::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION: + case rpc::ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID: + case rpc::ClioError::etlCONNECTION_ERROR: + case rpc::ClioError::etlREQUEST_ERROR: + case rpc::ClioError::etlREQUEST_TIMEOUT: + case rpc::ClioError::etlINVALID_RESPONSE: + ASSERT(false, "Unknown rpc error code {}", static_cast(*clioCode)); // this should never happen + break; + } + } + + return Response{ + boost::beast::http::status::bad_request, + boost::json::serialize(composeError(err, rawRequest_, request_)), + rawRequest_ + }; +} + +Response +ErrorHelper::makeInternalError() const +{ + return Response{ + boost::beast::http::status::internal_server_error, + boost::json::serialize(composeError(rpc::RippledError::rpcINTERNAL, rawRequest_, request_)), + rawRequest_ + }; +} + +Response +ErrorHelper::makeNotReadyError() const +{ + return Response{ + boost::beast::http::status::ok, + boost::json::serialize(composeError(rpc::RippledError::rpcNOT_READY, rawRequest_, request_)), + rawRequest_ + }; +} + +Response +ErrorHelper::makeTooBusyError() const +{ + if (not rawRequest_.get().isHttp()) { + return Response{ + boost::beast::http::status::too_many_requests, + boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)), + rawRequest_ + }; + } + + return Response{ + boost::beast::http::status::service_unavailable, + boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)), + rawRequest_ + }; +} + +Response +ErrorHelper::makeJsonParsingError() const +{ + if (not rawRequest_.get().isHttp()) { + return Response{ + boost::beast::http::status::bad_request, + boost::json::serialize(rpc::makeError(rpc::RippledError::rpcBAD_SYNTAX)), + rawRequest_ + }; + } + + return Response{ + boost::beast::http::status::bad_request, fmt::format("Unable to parse JSON from the request"), rawRequest_ + }; +} + +} // namespace web::ng::impl diff --git a/src/web/ng/impl/ErrorHandling.hpp b/src/web/ng/impl/ErrorHandling.hpp new file mode 100644 index 000000000..a18c0f70e --- /dev/null +++ b/src/web/ng/impl/ErrorHandling.hpp @@ -0,0 +1,64 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "rpc/Errors.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace web::ng::impl { + +/** + * @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible. + */ +class ErrorHelper { + std::reference_wrapper rawRequest_; + std::optional request_; + +public: + ErrorHelper(Request const& rawRequest, std::optional request = std::nullopt); + + Response + makeError(rpc::Status const& err) const; + + Response + makeInternalError() const; + + Response + makeNotReadyError() const; + + Response + makeTooBusyError() const; + + Response + makeJsonParsingError() const; +}; + +} // namespace web::ng::impl From 71afb5308744ae5aea99728c763488fcf3f1f22c Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 25 Oct 2024 17:27:55 +0100 Subject: [PATCH 55/81] Add ErrorHandling tests --- src/web/ng/impl/ErrorHandling.cpp | 38 ++- tests/unit/CMakeLists.txt | 1 + tests/unit/web/impl/ErrorHandlingTests.cpp | 293 +++++++++++++++++++++ 3 files changed, 309 insertions(+), 23 deletions(-) create mode 100644 tests/unit/web/impl/ErrorHandlingTests.cpp diff --git a/src/web/ng/impl/ErrorHandling.cpp b/src/web/ng/impl/ErrorHandling.cpp index 7dc3b90d2..c6bd010db 100644 --- a/src/web/ng/impl/ErrorHandling.cpp +++ b/src/web/ng/impl/ErrorHandling.cpp @@ -17,8 +17,6 @@ */ //============================================================================== -#pragma once - #include "web/ng/impl/ErrorHandling.hpp" #include "rpc/Errors.hpp" @@ -38,6 +36,8 @@ #include #include +namespace http = boost::beast::http; + namespace web::ng::impl { namespace { @@ -79,9 +79,7 @@ ErrorHelper::makeError(rpc::Status const& err) const { if (not rawRequest_.get().isHttp()) { return Response{ - boost::beast::http::status::bad_request, - boost::json::serialize(composeError(err, rawRequest_, request_)), - rawRequest_ + http::status::bad_request, boost::json::serialize(composeError(err, rawRequest_, request_)), rawRequest_ }; } @@ -90,18 +88,16 @@ ErrorHelper::makeError(rpc::Status const& err) const switch (*clioCode) { case rpc::ClioError::rpcINVALID_API_VERSION: return Response{ - boost::beast::http::status::bad_request, - std::string{rpc::getErrorInfo(*clioCode).error}, - rawRequest_ + http::status::bad_request, std::string{rpc::getErrorInfo(*clioCode).error}, rawRequest_ }; case rpc::ClioError::rpcCOMMAND_IS_MISSING: - return Response{boost::beast::http::status::bad_request, "Null method", rawRequest_}; + return Response{http::status::bad_request, "Null method", rawRequest_}; case rpc::ClioError::rpcCOMMAND_IS_EMPTY: - return Response{boost::beast::http::status::bad_request, "method is empty", rawRequest_}; + return Response{http::status::bad_request, "method is empty", rawRequest_}; case rpc::ClioError::rpcCOMMAND_NOT_STRING: - return Response{boost::beast::http::status::bad_request, "method is not string", rawRequest_}; + return Response{http::status::bad_request, "method is not string", rawRequest_}; case rpc::ClioError::rpcPARAMS_UNPARSEABLE: - return Response{boost::beast::http::status::bad_request, "params unparseable", rawRequest_}; + return Response{http::status::bad_request, "params unparseable", rawRequest_}; // others are not applicable but we want a compilation error next time we add one case rpc::ClioError::rpcUNKNOWN_OPTION: @@ -122,9 +118,7 @@ ErrorHelper::makeError(rpc::Status const& err) const } return Response{ - boost::beast::http::status::bad_request, - boost::json::serialize(composeError(err, rawRequest_, request_)), - rawRequest_ + http::status::bad_request, boost::json::serialize(composeError(err, rawRequest_, request_)), rawRequest_ }; } @@ -132,7 +126,7 @@ Response ErrorHelper::makeInternalError() const { return Response{ - boost::beast::http::status::internal_server_error, + http::status::internal_server_error, boost::json::serialize(composeError(rpc::RippledError::rpcINTERNAL, rawRequest_, request_)), rawRequest_ }; @@ -142,7 +136,7 @@ Response ErrorHelper::makeNotReadyError() const { return Response{ - boost::beast::http::status::ok, + http::status::ok, boost::json::serialize(composeError(rpc::RippledError::rpcNOT_READY, rawRequest_, request_)), rawRequest_ }; @@ -153,14 +147,14 @@ ErrorHelper::makeTooBusyError() const { if (not rawRequest_.get().isHttp()) { return Response{ - boost::beast::http::status::too_many_requests, + http::status::too_many_requests, boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)), rawRequest_ }; } return Response{ - boost::beast::http::status::service_unavailable, + http::status::service_unavailable, boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)), rawRequest_ }; @@ -171,15 +165,13 @@ ErrorHelper::makeJsonParsingError() const { if (not rawRequest_.get().isHttp()) { return Response{ - boost::beast::http::status::bad_request, + http::status::bad_request, boost::json::serialize(rpc::makeError(rpc::RippledError::rpcBAD_SYNTAX)), rawRequest_ }; } - return Response{ - boost::beast::http::status::bad_request, fmt::format("Unable to parse JSON from the request"), rawRequest_ - }; + return Response{http::status::bad_request, fmt::format("Unable to parse JSON from the request"), rawRequest_}; } } // namespace web::ng::impl diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 09b8fd703..cf0820969 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -133,6 +133,7 @@ target_sources( web/dosguard/IntervalSweepHandlerTests.cpp web/dosguard/WhitelistHandlerTests.cpp web/impl/AdminVerificationTests.cpp + web/impl/ErrorHandlingTests.cpp web/ng/ResponseTests.cpp web/ng/RequestTests.cpp web/ng/ServerTests.cpp diff --git a/tests/unit/web/impl/ErrorHandlingTests.cpp b/tests/unit/web/impl/ErrorHandlingTests.cpp new file mode 100644 index 000000000..f4e0bd211 --- /dev/null +++ b/tests/unit/web/impl/ErrorHandlingTests.cpp @@ -0,0 +1,293 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "rpc/Errors.hpp" +#include "util/LoggerFixtures.hpp" +#include "util/NameGenerator.hpp" +#include "util/Taggable.hpp" +#include "util/config/Config.hpp" +#include "web/impl/ErrorHandling.hpp" +#include "web/interface/ConnectionBase.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace web::impl; +using namespace web; + +struct ErrorHandlingTests : NoLoggerFixture { + struct ConnectionBaseMock : ConnectionBase { + using ConnectionBase::ConnectionBase; + + MOCK_METHOD(void, send, (std::string&&, boost::beast::http::status), (override)); + MOCK_METHOD(void, send, (std::shared_ptr), (override)); + }; + + util::TagDecoratorFactory tagFactory_{util::Config{}}; + std::string const clientIp_ = "some ip"; + std::shared_ptr> connection_ = + std::make_shared>(tagFactory_, clientIp_); +}; + +struct ErrorHandlingComposeErrorTestBundle { + std::string testName; + bool connectionUpgraded; + std::optional request; + boost::json::object expectedResult; +}; + +struct ErrorHandlingComposeErrorTest : ErrorHandlingTests, + testing::WithParamInterface {}; + +TEST_P(ErrorHandlingComposeErrorTest, composeError) +{ + connection_->upgraded = GetParam().connectionUpgraded; + ErrorHelper errorHelper{connection_, GetParam().request}; + auto const result = errorHelper.composeError(rpc::RippledError::rpcNOT_READY); + EXPECT_EQ(boost::json::serialize(result), boost::json::serialize(GetParam().expectedResult)); +} + +INSTANTIATE_TEST_CASE_P( + ErrorHandlingComposeErrorTestGroup, + ErrorHandlingComposeErrorTest, + testing::ValuesIn( + {ErrorHandlingComposeErrorTestBundle{ + "NoRequest_UpgradedConnection", + true, + std::nullopt, + {{"error", "notReady"}, + {"error_code", 13}, + {"error_message", "Not ready to handle this request."}, + {"status", "error"}, + {"type", "response"}} + }, + ErrorHandlingComposeErrorTestBundle{ + "NoRequest_NotUpgradedConnection", + false, + std::nullopt, + {{"result", + {{"error", "notReady"}, + {"error_code", 13}, + {"error_message", "Not ready to handle this request."}, + {"status", "error"}, + {"type", "response"}}}} + }, + ErrorHandlingComposeErrorTestBundle{ + "Request_UpgradedConnection", + true, + boost::json::object{{"id", 1}, {"api_version", 2}}, + {{"error", "notReady"}, + {"error_code", 13}, + {"error_message", "Not ready to handle this request."}, + {"status", "error"}, + {"type", "response"}, + {"id", 1}, + {"api_version", 2}, + {"request", {{"id", 1}, {"api_version", 2}}}} + }, + ErrorHandlingComposeErrorTestBundle{ + "Request_NotUpgradedConnection", + false, + boost::json::object{{"id", 1}, {"api_version", 2}}, + {{"result", + {{"error", "notReady"}, + {"error_code", 13}, + {"error_message", "Not ready to handle this request."}, + {"status", "error"}, + {"type", "response"}, + {"id", 1}, + {"request", {{"id", 1}, {"api_version", 2}}}}}} + }} + ), + tests::util::NameGenerator +); + +struct ErrorHandlingSendErrorTestBundle { + std::string testName; + bool connectionUpgraded; + rpc::Status status; + std::string expectedMessage; + boost::beast::http::status expectedStatus; +}; + +struct ErrorHandlingSendErrorTest : ErrorHandlingTests, + testing::WithParamInterface {}; + +TEST_P(ErrorHandlingSendErrorTest, sendError) +{ + connection_->upgraded = GetParam().connectionUpgraded; + ErrorHelper errorHelper{connection_}; + + EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus)); + errorHelper.sendError(GetParam().status); +} + +INSTANTIATE_TEST_CASE_P( + ErrorHandlingSendErrorTestGroup, + ErrorHandlingSendErrorTest, + testing::ValuesIn({ + ErrorHandlingSendErrorTestBundle{ + "UpgradedConnection", + true, + rpc::Status{rpc::RippledError::rpcTOO_BUSY}, + R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})", + boost::beast::http::status::ok + }, + ErrorHandlingSendErrorTestBundle{ + "NotUpgradedConnection_InvalidApiVersion", + false, + rpc::Status{rpc::ClioError::rpcINVALID_API_VERSION}, + "invalid_API_version", + boost::beast::http::status::bad_request + }, + ErrorHandlingSendErrorTestBundle{ + "NotUpgradedConnection_CommandIsMissing", + false, + rpc::Status{rpc::ClioError::rpcCOMMAND_IS_MISSING}, + "Null method", + boost::beast::http::status::bad_request + }, + ErrorHandlingSendErrorTestBundle{ + "NotUpgradedConnection_CommandIsEmpty", + false, + rpc::Status{rpc::ClioError::rpcCOMMAND_IS_EMPTY}, + "method is empty", + boost::beast::http::status::bad_request + }, + ErrorHandlingSendErrorTestBundle{ + "NotUpgradedConnection_CommandNotString", + false, + rpc::Status{rpc::ClioError::rpcCOMMAND_NOT_STRING}, + "method is not string", + boost::beast::http::status::bad_request + }, + ErrorHandlingSendErrorTestBundle{ + "NotUpgradedConnection_ParamsUnparseable", + false, + rpc::Status{rpc::ClioError::rpcPARAMS_UNPARSEABLE}, + "params unparseable", + boost::beast::http::status::bad_request + }, + ErrorHandlingSendErrorTestBundle{ + "NotUpgradedConnection_RippledError", + false, + rpc::Status{rpc::RippledError::rpcTOO_BUSY}, + R"({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})", + boost::beast::http::status::bad_request + }, + }), + tests::util::NameGenerator +); + +TEST_F(ErrorHandlingTests, sendInternalError) +{ + ErrorHelper errorHelper{connection_}; + + EXPECT_CALL( + *connection_, + send( + std::string{ + R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})" + }, + boost::beast::http::status::internal_server_error + ) + ); + errorHelper.sendInternalError(); +} + +TEST_F(ErrorHandlingTests, sendNotReadyError) +{ + ErrorHelper errorHelper{connection_}; + EXPECT_CALL( + *connection_, + send( + std::string{ + R"({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})" + }, + boost::beast::http::status::ok + ) + ); + errorHelper.sendNotReadyError(); +} + +TEST_F(ErrorHandlingTests, sendTooBusyError_UpgradedConnection) +{ + connection_->upgraded = true; + ErrorHelper errorHelper{connection_}; + EXPECT_CALL( + *connection_, + send( + std::string{ + R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})" + }, + boost::beast::http::status::ok + ) + ); + errorHelper.sendTooBusyError(); +} + +TEST_F(ErrorHandlingTests, sendTooBusyError_NotUpgradedConnection) +{ + connection_->upgraded = false; + ErrorHelper errorHelper{connection_}; + EXPECT_CALL( + *connection_, + send( + std::string{ + R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})" + }, + boost::beast::http::status::service_unavailable + ) + ); + errorHelper.sendTooBusyError(); +} + +TEST_F(ErrorHandlingTests, sendJsonParsingError_UpgradedConnection) +{ + connection_->upgraded = true; + ErrorHelper errorHelper{connection_}; + EXPECT_CALL( + *connection_, + send( + std::string{ + R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})" + }, + boost::beast::http::status::ok + ) + ); + errorHelper.sendJsonParsingError(); +} + +TEST_F(ErrorHandlingTests, sendJsonParsingError_NotUpgradedConnection) +{ + connection_->upgraded = false; + ErrorHelper errorHelper{connection_}; + EXPECT_CALL( + *connection_, + send(std::string{"Unable to parse JSON from the request"}, boost::beast::http::status::bad_request) + ); + errorHelper.sendJsonParsingError(); +} From e217401bcd796b26399ca6a0c5a18bceae0408a8 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 28 Oct 2024 16:19:18 +0000 Subject: [PATCH 56/81] Add tests for ng::ErrorHandling --- tests/unit/CMakeLists.txt | 1 + tests/unit/web/ng/impl/ErrorHandlingTests.cpp | 283 ++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 tests/unit/web/ng/impl/ErrorHandlingTests.cpp diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index cf0820969..f2c10275c 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -138,6 +138,7 @@ target_sources( web/ng/RequestTests.cpp web/ng/ServerTests.cpp web/ng/impl/ConnectionHandlerTests.cpp + web/ng/impl/ErrorHandlingTests.cpp web/ng/impl/HttpConnectionTests.cpp web/ng/impl/ServerSslContextTests.cpp web/ng/impl/WsConnectionTests.cpp diff --git a/tests/unit/web/ng/impl/ErrorHandlingTests.cpp b/tests/unit/web/ng/impl/ErrorHandlingTests.cpp new file mode 100644 index 000000000..bb8703ee7 --- /dev/null +++ b/tests/unit/web/ng/impl/ErrorHandlingTests.cpp @@ -0,0 +1,283 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "rpc/Errors.hpp" +#include "util/LoggerFixtures.hpp" +#include "util/NameGenerator.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/impl/ErrorHandling.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace web::ng::impl; +using namespace web::ng; + +namespace http = boost::beast::http; + +struct ng_ErrorHandlingTests : NoLoggerFixture { + static Request + makeRequest(bool isHttp, std::optional body = std::nullopt) + { + if (isHttp) + return Request{http::request{http::verb::post, "/", 11, body.value_or("")}}; + return Request{body.value_or(""), Request::HttpHeaders{}}; + } +}; + +struct ng_ErrorHandlingMakeErrorTestBundle { + std::string testName; + bool isHttp; + rpc::Status status; + std::string expectedMessage; + boost::beast::http::status expectedStatus; +}; + +struct ng_ErrorHandlingMakeErrorTest : ng_ErrorHandlingTests, + testing::WithParamInterface {}; + +TEST_P(ng_ErrorHandlingMakeErrorTest, MakeError) +{ + auto const request = makeRequest(GetParam().isHttp); + ErrorHelper errorHelper{request}; + + auto response = errorHelper.makeError(GetParam().status); + EXPECT_EQ(response.message(), GetParam().expectedMessage); + if (GetParam().isHttp) + EXPECT_EQ(std::move(response).intoHttpResponse().result(), GetParam().expectedStatus); +} + +INSTANTIATE_TEST_CASE_P( + ng_ErrorHandlingMakeErrorTestGroup, + ng_ErrorHandlingMakeErrorTest, + testing::ValuesIn({ + ng_ErrorHandlingMakeErrorTestBundle{ + "WsRequest", + false, + rpc::Status{rpc::RippledError::rpcTOO_BUSY}, + R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})", + boost::beast::http::status::ok + }, + ng_ErrorHandlingMakeErrorTestBundle{ + "HttpRequest_InvalidApiVersion", + true, + rpc::Status{rpc::ClioError::rpcINVALID_API_VERSION}, + "invalid_API_version", + boost::beast::http::status::bad_request + }, + ng_ErrorHandlingMakeErrorTestBundle{ + "HttpRequest_CommandIsMissing", + true, + rpc::Status{rpc::ClioError::rpcCOMMAND_IS_MISSING}, + "Null method", + boost::beast::http::status::bad_request + }, + ng_ErrorHandlingMakeErrorTestBundle{ + "HttpRequest_CommandIsEmpty", + true, + rpc::Status{rpc::ClioError::rpcCOMMAND_IS_EMPTY}, + "method is empty", + boost::beast::http::status::bad_request + }, + ng_ErrorHandlingMakeErrorTestBundle{ + "HttpRequest_CommandNotString", + true, + rpc::Status{rpc::ClioError::rpcCOMMAND_NOT_STRING}, + "method is not string", + boost::beast::http::status::bad_request + }, + ng_ErrorHandlingMakeErrorTestBundle{ + "HttpRequest_ParamsUnparseable", + true, + rpc::Status{rpc::ClioError::rpcPARAMS_UNPARSEABLE}, + "params unparseable", + boost::beast::http::status::bad_request + }, + ng_ErrorHandlingMakeErrorTestBundle{ + "HttpRequest_RippledError", + true, + rpc::Status{rpc::RippledError::rpcTOO_BUSY}, + R"({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})", + boost::beast::http::status::bad_request + }, + }), + tests::util::NameGenerator +); + +struct ng_ErrorHandlingMakeInternalErrorTestBundle { + std::string testName; + bool isHttp; + std::optional request; + boost::json::object expectedResult; +}; + +struct ng_ErrorHandlingMakeInternalErrorTest + : ng_ErrorHandlingTests, + testing::WithParamInterface {}; + +TEST_P(ng_ErrorHandlingMakeInternalErrorTest, ComposeError) +{ + auto const request = makeRequest(GetParam().isHttp, GetParam().request); + std::optional const requestJson = GetParam().request.has_value() + ? std::make_optional(boost::json::parse(*GetParam().request).as_object()) + : std::nullopt; + ErrorHelper errorHelper{request, requestJson}; + + auto response = errorHelper.makeInternalError(); + + EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult)); + if (GetParam().isHttp) + EXPECT_EQ(std::move(response).intoHttpResponse().result(), boost::beast::http::status::internal_server_error); +} + +INSTANTIATE_TEST_CASE_P( + ng_ErrorHandlingComposeErrorTestGroup, + ng_ErrorHandlingMakeInternalErrorTest, + testing::ValuesIn( + {ng_ErrorHandlingMakeInternalErrorTestBundle{ + "NoRequest_WebsocketConnection", + false, + std::nullopt, + {{"error", "internal"}, + {"error_code", 73}, + {"error_message", "Internal error."}, + {"status", "error"}, + {"type", "response"}} + }, + ng_ErrorHandlingMakeInternalErrorTestBundle{ + "NoRequest_HttpConnection", + true, + std::nullopt, + {{"result", + {{"error", "internal"}, + {"error_code", 73}, + {"error_message", "Internal error."}, + {"status", "error"}, + {"type", "response"}}}} + }, + ng_ErrorHandlingMakeInternalErrorTestBundle{ + "Request_WebsocketConnection", + false, + std::string{R"({"id": 1, "api_version": 2})"}, + {{"error", "internal"}, + {"error_code", 73}, + {"error_message", "Internal error."}, + {"status", "error"}, + {"type", "response"}, + {"id", 1}, + {"api_version", 2}, + {"request", {{"id", 1}, {"api_version", 2}}}} + }, + ng_ErrorHandlingMakeInternalErrorTestBundle{ + "Request_WebsocketConnection_NoId", + false, + std::string{R"({"api_version": 2})"}, + {{"error", "internal"}, + {"error_code", 73}, + {"error_message", "Internal error."}, + {"status", "error"}, + {"type", "response"}, + {"api_version", 2}, + {"request", {{"api_version", 2}}}} + }, + ng_ErrorHandlingMakeInternalErrorTestBundle{ + "Request_HttpConnection", + true, + std::string{R"({"id": 1, "api_version": 2})"}, + {{"result", + {{"error", "internal"}, + {"error_code", 73}, + {"error_message", "Internal error."}, + {"status", "error"}, + {"type", "response"}, + {"id", 1}, + {"request", {{"id", 1}, {"api_version", 2}}}}}} + }} + ), + tests::util::NameGenerator +); + +TEST_F(ng_ErrorHandlingTests, MakeNotReadyError) +{ + auto const request = makeRequest(true); + auto response = ErrorHelper{request}.makeNotReadyError(); + EXPECT_EQ( + response.message(), + std::string{ + R"({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})" + } + ); + EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::ok); +} + +TEST_F(ng_ErrorHandlingTests, MakeTooBusyError_WebsocketRequest) +{ + auto const request = makeRequest(false); + auto const response = ErrorHelper{request}.makeTooBusyError(); + EXPECT_EQ( + response.message(), + std::string{ + R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})" + } + ); +} + +TEST_F(ng_ErrorHandlingTests, sendTooBusyError_HttpConnection) +{ + auto const request = makeRequest(true); + auto response = ErrorHelper{request}.makeTooBusyError(); + EXPECT_EQ( + response.message(), + std::string{ + R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})" + } + ); + EXPECT_EQ(std::move(response).intoHttpResponse().result(), boost::beast::http::status::service_unavailable); +} + +TEST_F(ng_ErrorHandlingTests, makeJsonParsingError_WebsocketConnection) +{ + auto const request = makeRequest(false); + auto const response = ErrorHelper{request}.makeJsonParsingError(); + EXPECT_EQ( + response.message(), + std::string{ + R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})" + } + ); +} + +TEST_F(ng_ErrorHandlingTests, makeJsonParsingError_HttpConnection) +{ + auto const request = makeRequest(true); + auto response = ErrorHelper{request}.makeJsonParsingError(); + EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"}); + EXPECT_EQ(std::move(response).intoHttpResponse().result(), boost::beast::http::status::bad_request); +} From c4ec64f69d7a62eba44093b5bb44a98d96c936b3 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 28 Oct 2024 18:01:43 +0000 Subject: [PATCH 57/81] WIP: Refactoring ng::RPCServerHandler --- src/web/ng/Connection.hpp | 6 + src/web/ng/RPCServerHandler.hpp | 310 ++++++++++++++++++++++++++++++ src/web/ng/impl/ErrorHandling.hpp | 10 +- 3 files changed, 321 insertions(+), 5 deletions(-) create mode 100644 src/web/ng/RPCServerHandler.hpp diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index f89bb6622..1bdc24f40 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -151,6 +151,12 @@ class ConnectionContext { */ std::string const& ip() const; + + util::BaseTagDecorator const& + tag() const; + + bool + wasUpgraded() const; }; } // namespace web::ng diff --git a/src/web/ng/RPCServerHandler.hpp b/src/web/ng/RPCServerHandler.hpp new file mode 100644 index 000000000..0ad9eba40 --- /dev/null +++ b/src/web/ng/RPCServerHandler.hpp @@ -0,0 +1,310 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "data/BackendInterface.hpp" +#include "rpc/Errors.hpp" +#include "rpc/Factories.hpp" +#include "rpc/JS.hpp" +#include "rpc/RPCHelpers.hpp" +#include "rpc/common/impl/APIVersionParser.hpp" +#include "util/Assert.hpp" +#include "util/JsonUtils.hpp" +#include "util/Profiler.hpp" +#include "util/Taggable.hpp" +#include "util/config/Config.hpp" +#include "util/log/Logger.hpp" +#include "web/interface/ConnectionBase.hpp" +#include "web/ng/Connection.hpp" +#include "web/ng/Request.hpp" +#include "web/ng/Response.hpp" +#include "web/ng/impl/ErrorHandling.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace web::ng { + +/** + * @brief The server handler for RPC requests called by web server. + * + * Note: see @ref web::SomeServerHandler concept + */ +template +class RPCServerHandler { + std::shared_ptr const backend_; + std::shared_ptr const rpcEngine_; + std::shared_ptr const etl_; + util::TagDecoratorFactory const tagFactory_; + rpc::impl::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed + + util::Logger log_{"RPC"}; + util::Logger perfLog_{"Performance"}; + +public: + /** + * @brief Create a new server handler. + * + * @param config Clio config to use + * @param backend The backend to use + * @param rpcEngine The RPC engine to use + * @param etl The ETL to use + */ + RPCServerHandler( + util::Config const& config, + std::shared_ptr const& backend, + std::shared_ptr const& rpcEngine, + std::shared_ptr const& etl + ) + : backend_(backend) + , rpcEngine_(rpcEngine) + , etl_(etl) + , tagFactory_(config) + , apiVersionParser_(config.sectionOr("api_version", {})) + { + } + + /** + * @brief The callback when server receives a request. + * + * @param request The request + * @param connection The connection + */ + Response + operator()( + Request const& request, + ConnectionContext connectionContext, + bool const isAdmin, + boost::asio::yield_context yield + ) + { + std::optional response; + bool const postSuccessful = rpcEngine_->post( + [this, &request, &response, connectionContext, isAdmin](boost::asio::yield_context yield) mutable { + try { + auto parsedRequest = boost::json::parse(request.message()).as_object(); + LOG(perfLog_.debug()) << connectionContext.tag() << "Adding to work queue"; + + if (not connectionContext.wasUpgraded() and shouldReplaceParams(parsedRequest)) + parsedRequest[JS(params)] = boost::json::array({boost::json::object{}}); + + response = handleRequest(yield, request, std::move(parsedRequest), connectionContext, isAdmin); + } catch (boost::system::system_error const& ex) { + // system_error thrown when json parsing failed + rpcEngine_->notifyBadSyntax(); + response = impl::ErrorHelper{request}.makeJsonParsingError(); + LOG(log_.warn()) << "Error parsing JSON: " << ex.what() << ". For request: " << request; + } catch (std::invalid_argument const& ex) { + // thrown when json parses something that is not an object at top level + rpcEngine_->notifyBadSyntax(); + LOG(log_.warn()) << "Invalid argument error: " << ex.what() << ". For request: " << request; + response = impl::ErrorHelper{request}.makeJsonParsingError(); + } catch (std::exception const& ex) { + LOG(perfLog_.error()) << connectionContext.tag() << "Caught exception: " << ex.what(); + rpcEngine_->notifyInternalError(); + response = impl::ErrorHelper{request}.makeInternalError(); + } + }, + connectionContext.ip() + ); + + if (not postSuccessful) { + rpcEngine_->notifyTooBusy(); + return impl::ErrorHelper{request}.makeTooBusyError(); + } + ASSERT(response.has_value(), "Woke up coroutine without setting response"); + return std::move(response).value(); + } + +private: + Response + handleRequest( + boost::asio::yield_context yield, + Request const& rawRequest, + boost::json::object&& request, + ConnectionContext connectionContext, + bool const isAdmin + ) + { + LOG(log_.info()) << connectionContext.tag() << (connectionContext.wasUpgraded() ? "ws" : "http") + << " received request from work queue: " << util::removeSecret(request) + << " ip = " << connectionContext.ip(); + + try { + auto const range = backend_->fetchLedgerRange(); + if (!range) { + // for error that happened before the handler, we don't attach any warnings + rpcEngine_->notifyNotReady(); + return impl::ErrorHelper{rawRequest, std::move(request)}.makeNotReadyError(); + } + + auto const context = [&] { + if (connectionContext.wasUpgraded()) { + return rpc::make_WsContext( + yield, + request, + connectionContext, + tagFactory_.with(connectionContext.tag()), + *range, + connectionContext.ip(), + std::cref(apiVersionParser_), + isAdmin + ); + } + return rpc::make_HttpContext( + yield, + request, + tagFactory_.with(connectionContext.tag()), + *range, + connectionContext.ip(), + std::cref(apiVersionParser_), + isAdmin + ); + }(); + + if (!context) { + auto const err = context.error(); + LOG(perfLog_.warn()) << connectionContext.tag() << "Could not create Web context: " << err; + LOG(log_.warn()) << connectionContext.tag() << "Could not create Web context: " << err; + + // we count all those as BadSyntax - as the WS path would. + // Although over HTTP these will yield a 400 status with a plain text response (for most). + rpcEngine_->notifyBadSyntax(); + return impl::ErrorHelper(rawRequest, std::move(request)).makeError(err); + } + + auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); }); + + auto us = std::chrono::duration(timeDiff); + rpc::logDuration(*context, us); + + boost::json::object response; + + if (auto const status = std::get_if(&result.response)) { + // note: error statuses are counted/notified in buildResponse itself + response = impl::ErrorHelper(rawRequest, request).composeError(*status); + auto const responseStr = boost::json::serialize(response); + + LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr; + LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr; + } else { + // This can still technically be an error. Clio counts forwarded requests as successful. + rpcEngine_->notifyComplete(context->method, us); + + auto& json = std::get(result.response); + auto const isForwarded = + json.contains("forwarded") && json.at("forwarded").is_bool() && json.at("forwarded").as_bool(); + + if (isForwarded) + json.erase("forwarded"); + + // if the result is forwarded - just use it as is + // if forwarded request has error, for http, error should be in "result"; for ws, error should + // be at top + if (isForwarded && (json.contains(JS(result)) || connectionContext.wasUpgraded())) { + for (auto const& [k, v] : json) + response.insert_or_assign(k, v); + } else { + response[JS(result)] = json; + } + + if (isForwarded) + response["forwarded"] = true; + + // for ws there is an additional field "status" in the response, + // otherwise the "status" is in the "result" field + if (connectionContext.wasUpgraded()) { + auto const appendFieldIfExist = [&](auto const& field) { + if (request.contains(field) and not request.at(field).is_null()) + response[field] = request.at(field); + }; + + appendFieldIfExist(JS(id)); + appendFieldIfExist(JS(api_version)); + + if (!response.contains(JS(error))) + response[JS(status)] = JS(success); + + response[JS(type)] = JS(response); + } else { + if (response.contains(JS(result)) && !response[JS(result)].as_object().contains(JS(error))) + response[JS(result)].as_object()[JS(status)] = JS(success); + } + } + + boost::json::array warnings = std::move(result.warnings); + warnings.emplace_back(rpc::makeWarning(rpc::warnRPC_CLIO)); + + if (etl_->lastCloseAgeSeconds() >= 60) + warnings.emplace_back(rpc::makeWarning(rpc::warnRPC_OUTDATED)); + + response["warnings"] = warnings; + return Response{boost::beast::http::status::ok, boost::json::serialize(response), rawRequest}; + } catch (std::exception const& ex) { + // note: while we are catching this in buildResponse too, this is here to make sure + // that any other code that may throw is outside of buildResponse is also worked around. + LOG(perfLog_.error()) << connectionContext.tag() << "Caught exception: " << ex.what(); + LOG(log_.error()) << connectionContext.tag() << "Caught exception: " << ex.what(); + + rpcEngine_->notifyInternalError(); + return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError(); + } + } + + bool + shouldReplaceParams(boost::json::object const& req) const + { + auto const hasParams = req.contains(JS(params)); + auto const paramsIsArray = hasParams and req.at(JS(params)).is_array(); + auto const paramsIsEmptyString = + hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty(); + auto const paramsIsEmptyObject = + hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty(); + auto const paramsIsNull = hasParams and req.at(JS(params)).is_null(); + auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty(); + auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty(); + auto const firstArgIsNull = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null(); + auto const firstArgIsEmptyString = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_string() and + req.at(JS(params)).as_array().at(0).as_string().empty(); + + // Note: all this compatibility dance is to match `rippled` as close as possible + return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or arrayIsEmpty or + firstArgIsEmptyString or firstArgIsNull; + } +}; + +} // namespace web::ng diff --git a/src/web/ng/impl/ErrorHandling.hpp b/src/web/ng/impl/ErrorHandling.hpp index a18c0f70e..f400a6fe4 100644 --- a/src/web/ng/impl/ErrorHandling.hpp +++ b/src/web/ng/impl/ErrorHandling.hpp @@ -45,19 +45,19 @@ class ErrorHelper { public: ErrorHelper(Request const& rawRequest, std::optional request = std::nullopt); - Response + [[nodiscard]] Response makeError(rpc::Status const& err) const; - Response + [[nodiscard]] Response makeInternalError() const; - Response + [[nodiscard]] Response makeNotReadyError() const; - Response + [[nodiscard]] Response makeTooBusyError() const; - Response + [[nodiscard]] Response makeJsonParsingError() const; }; From af5a4332b1e42b2bd2da4a89230cc99675245498 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 29 Oct 2024 13:43:53 +0000 Subject: [PATCH 58/81] Add foreign coroutine support for CoroutineGroup --- src/util/CoroutineGroup.cpp | 30 +++++++++++++++++++++---- src/util/CoroutineGroup.hpp | 22 ++++++++++++++++++ tests/unit/util/CoroutineGroupTests.cpp | 28 +++++++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/util/CoroutineGroup.cpp b/src/util/CoroutineGroup.cpp index 271486078..8515f57c5 100644 --- a/src/util/CoroutineGroup.cpp +++ b/src/util/CoroutineGroup.cpp @@ -44,19 +44,27 @@ CoroutineGroup::~CoroutineGroup() bool CoroutineGroup::spawn(boost::asio::yield_context yield, std::function fn) { - if (maxChildren_.has_value() && childrenCounter_ >= *maxChildren_) + if (isFull()) return false; ++childrenCounter_; boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) { fn(yield); - --childrenCounter_; - if (childrenCounter_ == 0) - timer_.cancel(); + onCoroutineCompleted(); }); return true; } +std::optional> +CoroutineGroup::registerForeign() +{ + if (isFull()) + return std::nullopt; + + ++childrenCounter_; + return [this]() { onCoroutineCompleted(); }; +} + void CoroutineGroup::asyncWait(boost::asio::yield_context yield) { @@ -73,4 +81,18 @@ CoroutineGroup::size() const return childrenCounter_; } +bool +CoroutineGroup::isFull() const +{ + return maxChildren_.has_value() && childrenCounter_ >= *maxChildren_; +} + +void +CoroutineGroup::onCoroutineCompleted() +{ + --childrenCounter_; + if (childrenCounter_ == 0) + timer_.cancel(); +} + } // namespace util diff --git a/src/util/CoroutineGroup.hpp b/src/util/CoroutineGroup.hpp index 7fc5aa077..f107d28b3 100644 --- a/src/util/CoroutineGroup.hpp +++ b/src/util/CoroutineGroup.hpp @@ -66,6 +66,16 @@ class CoroutineGroup { bool spawn(boost::asio::yield_context yield, std::function fn); + /** + * @brief Register a foreign coroutine this group should wait for. + * @note A foreign coroutine is still counted as a child one, i.e. calling this method increases the size of the + * group. + * + * @return A callback to call on foreign coroutine completes or std::nullopt if the group is already full. + */ + std::optional> + registerForeign(); + /** * @brief Wait for all the coroutines in the group to finish * @@ -83,6 +93,18 @@ class CoroutineGroup { */ size_t size() const; + + /** + * @brief Check if the group is full + * + * @return true If the group is full false otherwise + */ + bool + isFull() const; + +private: + void + onCoroutineCompleted(); }; } // namespace util diff --git a/tests/unit/util/CoroutineGroupTests.cpp b/tests/unit/util/CoroutineGroupTests.cpp index ba9c6e38b..4d88ef167 100644 --- a/tests/unit/util/CoroutineGroupTests.cpp +++ b/tests/unit/util/CoroutineGroupTests.cpp @@ -155,13 +155,41 @@ TEST_F(CoroutineGroupTests, TooManyCoroutines) })); EXPECT_FALSE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); })); + EXPECT_TRUE(group.isFull()); boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}}; timer.async_wait(yield); + EXPECT_FALSE(group.isFull()); EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); })); group.asyncWait(yield); callback3_.Call(); }); } + +TEST_F(CoroutineGroupTests, SpawnForeign) +{ + testing::Sequence const sequence; + EXPECT_CALL(callback1_, Call).InSequence(sequence); + EXPECT_CALL(callback2_, Call).InSequence(sequence); + + runSpawn([this](boost::asio::yield_context yield) { + CoroutineGroup group{yield, 1}; + + auto const onForeignComplete = group.registerForeign(); + [&]() { ASSERT_TRUE(onForeignComplete.has_value()); }(); + + [&]() { ASSERT_FALSE(group.registerForeign().has_value()); }(); + + boost::asio::spawn(ctx, [this, &onForeignComplete](boost::asio::yield_context innerYield) { + boost::asio::steady_timer timer{innerYield.get_executor(), std::chrono::milliseconds{2}}; + timer.async_wait(innerYield); + callback1_.Call(); + onForeignComplete->operator()(); + }); + + group.asyncWait(yield); + callback2_.Call(); + }); +} From 70ad7c8a82fd226dd4520001a95998f8b2ed2857 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 29 Oct 2024 16:11:08 +0000 Subject: [PATCH 59/81] Fix typo --- src/rpc/Factories.cpp | 2 +- tests/unit/web/RPCServerHandlerTests.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/Factories.cpp b/src/rpc/Factories.cpp index adbb30e51..11deec76b 100644 --- a/src/rpc/Factories.cpp +++ b/src/rpc/Factories.cpp @@ -94,7 +94,7 @@ make_HttpContext( auto const command = boost::json::value_to(request.at("method")); if (command == "subscribe" || command == "unsubscribe") - return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed or websocket."}}; + return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed for websocket."}}; if (!request.at("params").is_array()) return Error{{ClioError::rpcPARAMS_UNPARSEABLE, "Missing params array."}}; diff --git a/tests/unit/web/RPCServerHandlerTests.cpp b/tests/unit/web/RPCServerHandlerTests.cpp index 89afe08c2..2a7d53db3 100644 --- a/tests/unit/web/RPCServerHandlerTests.cpp +++ b/tests/unit/web/RPCServerHandlerTests.cpp @@ -524,7 +524,7 @@ TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe) "result": { "error": "badSyntax", "error_code": 1, - "error_message": "Subscribe and unsubscribe are only allowed or websocket.", + "error_message": "Subscribe and unsubscribe are only allowed for websocket.", "status": "error", "type": "response", "request": { From 3431baeb9f7fb640e65343379df1a20b6eedb920 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 29 Oct 2024 16:11:39 +0000 Subject: [PATCH 60/81] Add isAdmin to Connection --- src/web/ng/Connection.cpp | 6 +++ src/web/ng/Connection.hpp | 22 +++++++++++ .../unit/web/ng/impl/HttpConnectionTests.cpp | 38 ++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/web/ng/Connection.cpp b/src/web/ng/Connection.cpp index 82c7ee6f1..7990584e4 100644 --- a/src/web/ng/Connection.cpp +++ b/src/web/ng/Connection.cpp @@ -50,6 +50,12 @@ Connection::ip() const return ip_; } +bool +Connection::isAdmin() const +{ + return isAdmin_.value_or(false); +} + ConnectionContext::ConnectionContext(Connection const& connection) : connection_{connection} { } diff --git a/src/web/ng/Connection.hpp b/src/web/ng/Connection.hpp index 1bdc24f40..7346b1fae 100644 --- a/src/web/ng/Connection.hpp +++ b/src/web/ng/Connection.hpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,7 @@ class Connection : public util::Taggable { protected: std::string ip_; // client ip boost::beast::flat_buffer buffer_; + std::optional isAdmin_; public: /** @@ -123,6 +125,26 @@ class Connection : public util::Taggable { */ std::string const& ip() const; + + /** + * @brieg Get whether the client is an admin. + * + * @return true if the client is an admin. + */ + bool + isAdmin() const; + + /** + * @brief Set the isAdmin field. + * @note This function is lazy, it will update isAdmin only if it is not set yet. + */ + template + void + setIsAdmin(T&& setter) + { + if (not isAdmin_.has_value()) + isAdmin_ = setter(); + } }; /** diff --git a/tests/unit/web/ng/impl/HttpConnectionTests.cpp b/tests/unit/web/ng/impl/HttpConnectionTests.cpp index 4a31c3702..c6c504c1f 100644 --- a/tests/unit/web/ng/impl/HttpConnectionTests.cpp +++ b/tests/unit/web/ng/impl/HttpConnectionTests.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -95,7 +96,6 @@ TEST_F(HttpConnectionTests, Receive) runSpawn([this](boost::asio::yield_context yield) { auto connection = acceptConnection(yield); - EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip(); auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100}); ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message(); @@ -294,3 +294,39 @@ TEST_F(HttpConnectionTests, Upgrade) [&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }(); }); } + +TEST_F(HttpConnectionTests, Ip) +{ + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) mutable { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); + + runSpawn([this](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip(); + }); +} + +TEST_F(HttpConnectionTests, isAdminSetAdmin) +{ + testing::StrictMock> adminSetter; + EXPECT_CALL(adminSetter, Call).WillOnce(testing::Return(true)); + + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) mutable { + auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100}); + [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }(); + }); + + runSpawn([&](boost::asio::yield_context yield) { + auto connection = acceptConnection(yield); + EXPECT_FALSE(connection.isAdmin()); + + connection.setIsAdmin(adminSetter.AsStdFunction()); + EXPECT_TRUE(connection.isAdmin()); + + // Setter shouldn't not be called here because isAdmin is already set + connection.setIsAdmin(adminSetter.AsStdFunction()); + EXPECT_TRUE(connection.isAdmin()); + }); +} From cbe715da35386552668c3820b94ff6d5300b4225 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 29 Oct 2024 16:12:09 +0000 Subject: [PATCH 61/81] Fix potential data race --- src/web/interface/ConnectionBase.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/interface/ConnectionBase.hpp b/src/web/interface/ConnectionBase.hpp index 439e0c660..b02911439 100644 --- a/src/web/interface/ConnectionBase.hpp +++ b/src/web/interface/ConnectionBase.hpp @@ -26,7 +26,7 @@ #include #include -#include +#include #include #include #include @@ -55,7 +55,7 @@ struct ConnectionBase : public util::Taggable { * This is used to track the api version of this connection, which mainly is used by subscription. It is different * from the api version in Context, which is only used for the current request. */ - std::uint32_t apiSubVersion = 0; + std::atomic_uint32_t apiSubVersion = 0; /** * @brief Create a new connection base. From 98390b80aab6b08e07f349679041e3df8e523601 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 29 Oct 2024 16:12:38 +0000 Subject: [PATCH 62/81] Make connection's coroutine sleep while processing request --- src/web/ng/RPCServerHandler.hpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/web/ng/RPCServerHandler.hpp b/src/web/ng/RPCServerHandler.hpp index 0ad9eba40..b1bde872e 100644 --- a/src/web/ng/RPCServerHandler.hpp +++ b/src/web/ng/RPCServerHandler.hpp @@ -26,6 +26,7 @@ #include "rpc/RPCHelpers.hpp" #include "rpc/common/impl/APIVersionParser.hpp" #include "util/Assert.hpp" +#include "util/CoroutineGroup.hpp" #include "util/JsonUtils.hpp" #include "util/Profiler.hpp" #include "util/Taggable.hpp" @@ -38,6 +39,7 @@ #include "web/ng/impl/ErrorHandling.hpp" #include +#include #include #include #include @@ -113,8 +115,14 @@ class RPCServerHandler { ) { std::optional response; + util::CoroutineGroup coroutineGroup{yield, 1}; + auto const onTaskComplete = coroutineGroup.registerForeign(); + ASSERT(onTaskComplete.has_value(), "Corouine group can't be full"); + bool const postSuccessful = rpcEngine_->post( - [this, &request, &response, connectionContext, isAdmin](boost::asio::yield_context yield) mutable { + [this, &request, &response, onTaskComplete = onTaskComplete.value(), connectionContext, isAdmin]( + boost::asio::yield_context yield + ) mutable { try { auto parsedRequest = boost::json::parse(request.message()).as_object(); LOG(perfLog_.debug()) << connectionContext.tag() << "Adding to work queue"; @@ -138,6 +146,8 @@ class RPCServerHandler { rpcEngine_->notifyInternalError(); response = impl::ErrorHelper{request}.makeInternalError(); } + + onTaskComplete(); }, connectionContext.ip() ); @@ -146,6 +156,8 @@ class RPCServerHandler { rpcEngine_->notifyTooBusy(); return impl::ErrorHelper{request}.makeTooBusyError(); } + + coroutineGroup.asyncWait(yield); ASSERT(response.has_value(), "Woke up coroutine without setting response"); return std::move(response).value(); } From 56e59a6b9879aedfe7127434b5967ee75a55538a Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 29 Oct 2024 18:04:25 +0000 Subject: [PATCH 63/81] WIP: Add SubscriptionContextInterface --- src/web/SubscriptionContextInterface.hpp | 76 ++++++++++++++++++++++++ src/web/impl/HttpBase.hpp | 6 ++ src/web/interface/ConnectionBase.hpp | 9 +++ 3 files changed, 91 insertions(+) create mode 100644 src/web/SubscriptionContextInterface.hpp diff --git a/src/web/SubscriptionContextInterface.hpp b/src/web/SubscriptionContextInterface.hpp new file mode 100644 index 000000000..b692a3875 --- /dev/null +++ b/src/web/SubscriptionContextInterface.hpp @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace web { + +/** + * @brief An interface to provide connection functionality for subscriptions. + */ +class SubscriptionContextInterface { +public: + virtual ~SubscriptionContextInterface() = default; + + /** + * @brief Send message to the client + * + * @param message The message to send. + */ + virtual void + send(std::shared_ptr message) = 0; + + /** + * @brief Connect a slot to onDisconnect connection signal. + * + * @param slot The slot to connect. + */ + virtual void + onDisconnect(std::function slot) = 0; + + /** + * @brief Set the API subversion. + * @param value The value to set. + */ + virtual void + setApiSubversion(uint32_t value) = 0; + + /** + * @brief Get the API subversion. + * + * @return The API subversion. + */ + virtual uint32_t + apiSubversion() const = 0; +}; + +/** + * @brief An alias for shared pointer to a SubscriptionContextInterface. + */ +using SubscriptionContextPtr = std::shared_ptr; + +} // namespace web diff --git a/src/web/impl/HttpBase.hpp b/src/web/impl/HttpBase.hpp index f986800a3..c0deacb21 100644 --- a/src/web/impl/HttpBase.hpp +++ b/src/web/impl/HttpBase.hpp @@ -24,6 +24,7 @@ #include "util/build/Build.hpp" #include "util/log/Logger.hpp" #include "util/prometheus/Http.hpp" +#include "web/SubscriptionContextInterface.hpp" #include "web/dosguard/DOSGuardInterface.hpp" #include "web/impl/AdminVerificationStrategy.hpp" #include "web/interface/Concepts.hpp" @@ -275,6 +276,11 @@ class HttpBase : public ConnectionBase { sender_(httpResponse(status, "application/json", std::move(msg))); } + SubscriptionContextPtr + subscriptionContext() const override + { + } + void onWrite(bool close, boost::beast::error_code ec, std::size_t bytes_transferred) { diff --git a/src/web/interface/ConnectionBase.hpp b/src/web/interface/ConnectionBase.hpp index b02911439..8c7fdec9c 100644 --- a/src/web/interface/ConnectionBase.hpp +++ b/src/web/interface/ConnectionBase.hpp @@ -20,6 +20,7 @@ #pragma once #include "util/Taggable.hpp" +#include "web/SubscriptionContextInterface.hpp" #include #include @@ -94,6 +95,14 @@ struct ConnectionBase : public util::Taggable { throw std::logic_error("web server can not send the shared payload"); } + /** + *@brief Get the subscription context for this connection. + * + * @return The subscription context for this connection. + */ + virtual SubscriptionContextPtr + subscriptionContext() const = 0; + /** * @brief Indicates whether the connection had an error and is considered dead. * From 0bcecc8546e0e7c9010196d9b1e5fa88af2345db Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 30 Oct 2024 16:02:51 +0000 Subject: [PATCH 64/81] Add SubscriptionContext --- src/feed/Types.hpp | 4 +- src/feed/impl/ProposedTransactionFeed.cpp | 6 +- src/feed/impl/SingleFeedBase.cpp | 2 +- src/feed/impl/TrackableSignalMap.hpp | 1 + src/feed/impl/TransactionFeed.cpp | 22 ++--- src/feed/impl/TransactionFeed.hpp | 6 +- src/rpc/common/Types.hpp | 3 +- src/rpc/handlers/Subscribe.cpp | 11 ++- src/rpc/handlers/Subscribe.hpp | 14 ++- src/rpc/handlers/Unsubscribe.cpp | 20 ++-- src/rpc/handlers/Unsubscribe.hpp | 16 ++-- src/web/CMakeLists.txt | 5 +- src/web/Context.hpp | 9 +- src/web/SubscriptionContext.cpp | 78 ++++++++++++++++ src/web/SubscriptionContext.hpp | 107 ++++++++++++++++++++++ src/web/SubscriptionContextInterface.hpp | 21 ++++- src/web/impl/HttpBase.hpp | 5 +- src/web/impl/WsBase.hpp | 23 +++++ src/web/interface/Concepts.hpp | 4 +- src/web/interface/ConnectionBase.hpp | 18 +--- 20 files changed, 292 insertions(+), 83 deletions(-) create mode 100644 src/web/SubscriptionContext.cpp create mode 100644 src/web/SubscriptionContext.hpp diff --git a/src/feed/Types.hpp b/src/feed/Types.hpp index c4399d09c..e02f60e46 100644 --- a/src/feed/Types.hpp +++ b/src/feed/Types.hpp @@ -19,12 +19,12 @@ #pragma once -#include "web/interface/ConnectionBase.hpp" +#include "web/SubscriptionContextInterface.hpp" #include namespace feed { -using Subscriber = web::ConnectionBase; +using Subscriber = web::SubscriptionContextInterface; using SubscriberPtr = Subscriber*; using SubscriberSharedPtr = std::shared_ptr; diff --git a/src/feed/impl/ProposedTransactionFeed.cpp b/src/feed/impl/ProposedTransactionFeed.cpp index 8ceae9d2c..780d16e8b 100644 --- a/src/feed/impl/ProposedTransactionFeed.cpp +++ b/src/feed/impl/ProposedTransactionFeed.cpp @@ -48,7 +48,7 @@ ProposedTransactionFeed::sub(SubscriberSharedPtr const& subscriber) if (added) { LOG(logger_.info()) << subscriber->tag() << "Subscribed tx_proposed"; ++subAllCount_.get(); - subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubInternal(connection); }); + subscriber->onDisconnect([this](SubscriberPtr connection) { unsubInternal(connection); }); } } @@ -73,9 +73,7 @@ ProposedTransactionFeed::sub(ripple::AccountID const& account, SubscriberSharedP if (added) { LOG(logger_.info()) << subscriber->tag() << "Subscribed accounts_proposed " << account; ++subAccountCount_.get(); - subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) { - unsubInternal(account, connection); - }); + subscriber->onDisconnect([this, account](SubscriberPtr connection) { unsubInternal(account, connection); }); } } diff --git a/src/feed/impl/SingleFeedBase.cpp b/src/feed/impl/SingleFeedBase.cpp index f6dc085b5..4650cb267 100644 --- a/src/feed/impl/SingleFeedBase.cpp +++ b/src/feed/impl/SingleFeedBase.cpp @@ -49,7 +49,7 @@ SingleFeedBase::sub(SubscriberSharedPtr const& subscriber) if (added) { LOG(logger_.info()) << subscriber->tag() << "Subscribed " << name_; ++subCount_.get(); - subscriber->onDisconnect.connect([this](SubscriberPtr connectionDisconnecting) { + subscriber->onDisconnect([this](SubscriberPtr connectionDisconnecting) { unsubInternal(connectionDisconnecting); }); }; diff --git a/src/feed/impl/TrackableSignalMap.hpp b/src/feed/impl/TrackableSignalMap.hpp index 2e79ad305..38dd91be5 100644 --- a/src/feed/impl/TrackableSignalMap.hpp +++ b/src/feed/impl/TrackableSignalMap.hpp @@ -23,6 +23,7 @@ #include +#include #include #include #include diff --git a/src/feed/impl/TransactionFeed.cpp b/src/feed/impl/TransactionFeed.cpp index b58304c62..ad368eb4f 100644 --- a/src/feed/impl/TransactionFeed.cpp +++ b/src/feed/impl/TransactionFeed.cpp @@ -53,14 +53,14 @@ namespace feed::impl { void TransactionFeed::TransactionSlot::operator()(AllVersionTransactionsType const& allVersionMsgs) const { - if (auto connection = connectionWeakPtr.lock(); connection) { + if (auto connection = subscriptionContextWeakPtr.lock(); connection) { // Check if this connection already sent if (feed.get().notified_.contains(connection.get())) return; feed.get().notified_.insert(connection.get()); - if (connection->apiSubVersion < 2u) { + if (connection->apiSubversion() < 2u) { connection->send(allVersionMsgs[0]); return; } @@ -75,7 +75,7 @@ TransactionFeed::sub(SubscriberSharedPtr const& subscriber) if (added) { LOG(logger_.info()) << subscriber->tag() << "Subscribed transactions"; ++subAllCount_.get(); - subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubInternal(connection); }); + subscriber->onDisconnect([this](SubscriberPtr connection) { unsubInternal(connection); }); } } @@ -86,18 +86,16 @@ TransactionFeed::sub(ripple::AccountID const& account, SubscriberSharedPtr const if (added) { LOG(logger_.info()) << subscriber->tag() << "Subscribed account " << account; ++subAccountCount_.get(); - subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) { - unsubInternal(account, connection); - }); + subscriber->onDisconnect([this, account](SubscriberPtr connection) { unsubInternal(account, connection); }); } } void TransactionFeed::subProposed(SubscriberSharedPtr const& subscriber) { - auto const added = txProposedsignal_.connectTrackableSlot(subscriber, TransactionSlot(*this, subscriber)); + auto const added = txProposedSignal_.connectTrackableSlot(subscriber, TransactionSlot(*this, subscriber)); if (added) { - subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubProposedInternal(connection); }); + subscriber->onDisconnect([this](SubscriberPtr connection) { unsubProposedInternal(connection); }); } } @@ -107,7 +105,7 @@ TransactionFeed::subProposed(ripple::AccountID const& account, SubscriberSharedP auto const added = accountProposedSignal_.connectTrackableSlot(subscriber, account, TransactionSlot(*this, subscriber)); if (added) { - subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) { + subscriber->onDisconnect([this, account](SubscriberPtr connection) { unsubProposedInternal(account, connection); }); } @@ -120,7 +118,7 @@ TransactionFeed::sub(ripple::Book const& book, SubscriberSharedPtr const& subscr if (added) { LOG(logger_.info()) << subscriber->tag() << "Subscribed book " << book; ++subBookCount_.get(); - subscriber->onDisconnect.connect([this, book](SubscriberPtr connection) { unsubInternal(book, connection); }); + subscriber->onDisconnect([this, book](SubscriberPtr connection) { unsubInternal(book, connection); }); } } @@ -284,7 +282,7 @@ TransactionFeed::pub( // clear the notified set. If the same connection subscribes both transactions + proposed_transactions, // rippled SENDS the same message twice notified_.clear(); - txProposedsignal_.emit(allVersionsMsgs); + txProposedSignal_.emit(allVersionsMsgs); notified_.clear(); // check duplicate for account and proposed_account, this prevents sending the same message multiple times // if it affects multiple accounts watched by the same connection @@ -322,7 +320,7 @@ TransactionFeed::unsubInternal(ripple::AccountID const& account, SubscriberPtr s void TransactionFeed::unsubProposedInternal(SubscriberPtr subscriber) { - txProposedsignal_.disconnect(subscriber); + txProposedSignal_.disconnect(subscriber); } void diff --git a/src/feed/impl/TransactionFeed.hpp b/src/feed/impl/TransactionFeed.hpp index e57cc12bf..787fd7614 100644 --- a/src/feed/impl/TransactionFeed.hpp +++ b/src/feed/impl/TransactionFeed.hpp @@ -52,10 +52,10 @@ class TransactionFeed { struct TransactionSlot { std::reference_wrapper feed; - std::weak_ptr connectionWeakPtr; + std::weak_ptr subscriptionContextWeakPtr; TransactionSlot(TransactionFeed& feed, SubscriberSharedPtr const& connection) - : feed(feed), connectionWeakPtr(connection) + : feed(feed), subscriptionContextWeakPtr(connection) { } @@ -76,7 +76,7 @@ class TransactionFeed { // Signals for proposed tx subscribers TrackableSignalMap accountProposedSignal_; - TrackableSignal txProposedsignal_; + TrackableSignal txProposedSignal_; std::unordered_set notified_; // Used by slots to prevent double notifications if tx contains multiple subscribed accounts diff --git a/src/rpc/common/Types.hpp b/src/rpc/common/Types.hpp index 0ae5ffad1..2d9ee0704 100644 --- a/src/rpc/common/Types.hpp +++ b/src/rpc/common/Types.hpp @@ -20,6 +20,7 @@ #pragma once #include "rpc/Errors.hpp" +#include "web/SubscriptionContextInterface.hpp" #include #include @@ -117,7 +118,7 @@ struct VoidOutput {}; */ struct Context { boost::asio::yield_context yield; - std::shared_ptr session = {}; // NOLINT(readability-redundant-member-init) + web::SubscriptionContextPtr session = {}; // NOLINT(readability-redundant-member-init) bool isAdmin = false; std::string clientIp = {}; // NOLINT(readability-redundant-member-init) uint32_t apiVersion = 0u; // invalid by default diff --git a/src/rpc/handlers/Subscribe.cpp b/src/rpc/handlers/Subscribe.cpp index 118484ac1..d4c7d67cd 100644 --- a/src/rpc/handlers/Subscribe.cpp +++ b/src/rpc/handlers/Subscribe.cpp @@ -30,6 +30,7 @@ #include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" #include "rpc/common/Validators.hpp" +#include "web/SubscriptionContextInterface.hpp" #include #include @@ -114,7 +115,7 @@ SubscribeHandler::process(Input input, Context const& ctx) const auto output = Output{}; // Mimic rippled. No matter what the request is, the api version changes for the whole session - ctx.session->apiSubVersion = ctx.apiVersion; + ctx.session->setApiSubversion(ctx.apiVersion); if (input.streams) { auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session); @@ -138,7 +139,7 @@ boost::json::object SubscribeHandler::subscribeToStreams( boost::asio::yield_context yield, std::vector const& streams, - std::shared_ptr const& session + web::SubscriptionContextPtr const& session ) const { auto response = boost::json::object{}; @@ -165,7 +166,7 @@ SubscribeHandler::subscribeToStreams( void SubscribeHandler::subscribeToAccountsProposed( std::vector const& accounts, - std::shared_ptr const& session + web::SubscriptionContextPtr const& session ) const { for (auto const& account : accounts) { @@ -177,7 +178,7 @@ SubscribeHandler::subscribeToAccountsProposed( void SubscribeHandler::subscribeToAccounts( std::vector const& accounts, - std::shared_ptr const& session + web::SubscriptionContextPtr const& session ) const { for (auto const& account : accounts) { @@ -189,7 +190,7 @@ SubscribeHandler::subscribeToAccounts( void SubscribeHandler::subscribeToBooks( std::vector const& books, - std::shared_ptr const& session, + web::SubscriptionContextPtr const& session, boost::asio::yield_context yield, Output& output ) const diff --git a/src/rpc/handlers/Subscribe.hpp b/src/rpc/handlers/Subscribe.hpp index 2f0824776..2fafce03d 100644 --- a/src/rpc/handlers/Subscribe.hpp +++ b/src/rpc/handlers/Subscribe.hpp @@ -23,6 +23,7 @@ #include "feed/SubscriptionManagerInterface.hpp" #include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" +#include "web/SubscriptionContextInterface.hpp" #include #include @@ -128,23 +129,20 @@ class SubscribeHandler { subscribeToStreams( boost::asio::yield_context yield, std::vector const& streams, - std::shared_ptr const& session + web::SubscriptionContextPtr const& session ) const; void - subscribeToAccounts(std::vector const& accounts, std::shared_ptr const& session) - const; + subscribeToAccounts(std::vector const& accounts, web::SubscriptionContextPtr const& session) const; void - subscribeToAccountsProposed( - std::vector const& accounts, - std::shared_ptr const& session - ) const; + subscribeToAccountsProposed(std::vector const& accounts, web::SubscriptionContextPtr const& session) + const; void subscribeToBooks( std::vector const& books, - std::shared_ptr const& session, + web::SubscriptionContextPtr const& session, boost::asio::yield_context yield, Output& output ) const; diff --git a/src/rpc/handlers/Unsubscribe.cpp b/src/rpc/handlers/Unsubscribe.cpp index ff8e316b6..177b4d2e6 100644 --- a/src/rpc/handlers/Unsubscribe.cpp +++ b/src/rpc/handlers/Unsubscribe.cpp @@ -21,6 +21,7 @@ #include "data/BackendInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp" +#include "feed/Types.hpp" #include "rpc/Errors.hpp" #include "rpc/JS.hpp" #include "rpc/RPCHelpers.hpp" @@ -106,10 +107,11 @@ UnsubscribeHandler::process(Input input, Context const& ctx) const return Output{}; } + void UnsubscribeHandler::unsubscribeFromStreams( std::vector const& streams, - std::shared_ptr const& session + feed::SubscriberSharedPtr const& session ) const { for (auto const& stream : streams) { @@ -130,21 +132,21 @@ UnsubscribeHandler::unsubscribeFromStreams( } } } + void -UnsubscribeHandler::unsubscribeFromAccounts( - std::vector accounts, - std::shared_ptr const& session -) const +UnsubscribeHandler::unsubscribeFromAccounts(std::vector accounts, feed::SubscriberSharedPtr const& session) + const { for (auto const& account : accounts) { auto const accountID = accountFromStringStrict(account); subscriptions_->unsubAccount(*accountID, session); } } + void UnsubscribeHandler::unsubscribeFromProposedAccounts( std::vector accountsProposed, - std::shared_ptr const& session + feed::SubscriberSharedPtr const& session ) const { for (auto const& account : accountsProposed) { @@ -153,10 +155,8 @@ UnsubscribeHandler::unsubscribeFromProposedAccounts( } } void -UnsubscribeHandler::unsubscribeFromBooks( - std::vector const& books, - std::shared_ptr const& session -) const +UnsubscribeHandler::unsubscribeFromBooks(std::vector const& books, feed::SubscriberSharedPtr const& session) + const { for (auto const& orderBook : books) { subscriptions_->unsubBook(orderBook.book, session); diff --git a/src/rpc/handlers/Unsubscribe.hpp b/src/rpc/handlers/Unsubscribe.hpp index ef20b43e9..1f89022b0 100644 --- a/src/rpc/handlers/Unsubscribe.hpp +++ b/src/rpc/handlers/Unsubscribe.hpp @@ -21,6 +21,7 @@ #include "data/BackendInterface.hpp" #include "feed/SubscriptionManagerInterface.hpp" +#include "feed/Types.hpp" #include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" @@ -105,22 +106,17 @@ class UnsubscribeHandler { private: void - unsubscribeFromStreams(std::vector const& streams, std::shared_ptr const& session) - const; + unsubscribeFromStreams(std::vector const& streams, feed::SubscriberSharedPtr const& session) const; void - unsubscribeFromAccounts(std::vector accounts, std::shared_ptr const& session) - const; + unsubscribeFromAccounts(std::vector accounts, feed::SubscriberSharedPtr const& session) const; void - unsubscribeFromProposedAccounts( - std::vector accountsProposed, - std::shared_ptr const& session - ) const; + unsubscribeFromProposedAccounts(std::vector accountsProposed, feed::SubscriberSharedPtr const& session) + const; void - unsubscribeFromBooks(std::vector const& books, std::shared_ptr const& session) - const; + unsubscribeFromBooks(std::vector const& books, feed::SubscriberSharedPtr const& session) const; /** * @brief Convert a JSON object to an Input diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 414cd1755..a8aa1065b 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -2,8 +2,7 @@ add_library(clio_web) target_sources( clio_web - PRIVATE Resolver.cpp - dosguard/DOSGuard.cpp + PRIVATE dosguard/DOSGuard.cpp dosguard/IntervalSweepHandler.cpp dosguard/WhitelistHandler.cpp impl/AdminVerificationStrategy.cpp @@ -15,6 +14,8 @@ target_sources( ng/Server.cpp ng/Request.cpp ng/Response.cpp + Resolver.cpp + SubscriptionContext.cpp ) target_link_libraries(clio_web PUBLIC clio_util) diff --git a/src/web/Context.hpp b/src/web/Context.hpp index 43514537d..35650ce8f 100644 --- a/src/web/Context.hpp +++ b/src/web/Context.hpp @@ -22,6 +22,7 @@ #include "data/Types.hpp" #include "util/Taggable.hpp" #include "util/log/Logger.hpp" +#include "web/SubscriptionContextInterface.hpp" #include "web/interface/ConnectionBase.hpp" #include @@ -43,7 +44,7 @@ struct Context : util::Taggable { std::string method; std::uint32_t apiVersion; boost::json::object params; - std::shared_ptr session; + SubscriptionContextPtr session; data::LedgerRange range; std::string clientIp; bool isAdmin; @@ -55,7 +56,7 @@ struct Context : util::Taggable { * @param command The method/command requested * @param apiVersion The api_version parsed from the request * @param params Request's parameters/data as a JSON object - * @param session The connection to the peer + * @param connection The connection to the peer * @param tagFactory A factory that is used to generate tags to track requests and connections * @param range The ledger range that is available at the time of the request * @param clientIp IP of the peer @@ -66,7 +67,7 @@ struct Context : util::Taggable { std::string command, std::uint32_t apiVersion, boost::json::object params, - std::shared_ptr const& session, + std::shared_ptr const& connection, util::TagDecoratorFactory const& tagFactory, data::LedgerRange const& range, std::string clientIp, @@ -77,7 +78,7 @@ struct Context : util::Taggable { , method(std::move(command)) , apiVersion(apiVersion) , params(std::move(params)) - , session(session) + , session(connection->subscriptionContext(tagFactory)) , range(range) , clientIp(std::move(clientIp)) , isAdmin(isAdmin) diff --git a/src/web/SubscriptionContext.cpp b/src/web/SubscriptionContext.cpp new file mode 100644 index 000000000..5f4e0e8c8 --- /dev/null +++ b/src/web/SubscriptionContext.cpp @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "web/SubscriptionContext.hpp" + +#include "util/Taggable.hpp" +#include "web/SubscriptionContextInterface.hpp" +#include "web/interface/ConnectionBase.hpp" + +#include +#include +#include +#include +#include + +namespace web { + +SubscriptionContext::SubscriptionContext( + util::TagDecoratorFactory const& factory, + std::shared_ptr connection +) + : SubscriptionContextInterface{factory}, connection_{connection} +{ +} + +void +SubscriptionContext::send(std::shared_ptr message) +{ + if (auto connection = connection_.lock(); connection != nullptr) + connection->send(std::move(message)); +} + +void +SubscriptionContext::onDisconnect(std::function const& slot) +{ + if (auto connection = connection_.lock(); connection != nullptr) { + onDisconnect_.connect(slot); + } else { + slot(this); + } +} + +void +SubscriptionContext::setApiSubversion(uint32_t value) +{ + apiSubVersion_ = value; +} + +uint32_t +SubscriptionContext::apiSubversion() const +{ + return apiSubVersion_; +} + +void +SubscriptionContext::disconnect() +{ + connection_.reset(); + onDisconnect_(this); +} + +} // namespace web diff --git a/src/web/SubscriptionContext.hpp b/src/web/SubscriptionContext.hpp new file mode 100644 index 000000000..0988ec504 --- /dev/null +++ b/src/web/SubscriptionContext.hpp @@ -0,0 +1,107 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/Taggable.hpp" +#include "web/SubscriptionContextInterface.hpp" +#include "web/interface/Concepts.hpp" +#include "web/interface/ConnectionBase.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace web { + +/** + * @brief A context of a WsBase connection for subscriptions. + */ +class SubscriptionContext : public SubscriptionContextInterface { + std::weak_ptr connection_; + boost::signals2::signal onDisconnect_; + /** + * @brief The API version of the web stream client. + * This is used to track the api version of this connection, which mainly is used by subscription. It is different + * from the api version in Context, which is only used for the current request. + */ + std::atomic_uint32_t apiSubVersion_ = 0; + +public: + /** + * @brief Construct a new Subscription Context object + * + * @param factory The tag decorator factory to use to init taggable. + * @param connection The connection for which the context is created. + */ + SubscriptionContext(util::TagDecoratorFactory const& factory, std::shared_ptr connection); + + /** + * @brief Get tag decorator. + * + * @return Reference to the tag decorator + */ + util::BaseTagDecorator const& + tag() const; + + /** + * @brief Send message to the client + * @note This method will not do anything if the related connection got disconnected. + * + * @param message The message to send. + */ + void + send(std::shared_ptr message) override; + + /** + * @brief Connect a slot to onDisconnect connection signal. + * @note This method will call the slot immediately if the related connection is already disconnected. + * + * @param slot The slot to connect. + */ + void + onDisconnect(std::function const& slot) override; + + /** + * @brief Set the API subversion. + * @param value The value to set. + */ + void + setApiSubversion(uint32_t value) override; + + /** + * @brief Get the API subversion. + * + * @return The API subversion. + */ + uint32_t + apiSubversion() const override; + + /** + * @brief Notify the context that connection has been disconnected. + */ + void + disconnect() override; +}; + +} // namespace web diff --git a/src/web/SubscriptionContextInterface.hpp b/src/web/SubscriptionContextInterface.hpp index b692a3875..cad356886 100644 --- a/src/web/SubscriptionContextInterface.hpp +++ b/src/web/SubscriptionContextInterface.hpp @@ -19,6 +19,8 @@ #pragma once +#include "util/Taggable.hpp" + #include #include @@ -31,10 +33,15 @@ namespace web { /** * @brief An interface to provide connection functionality for subscriptions. + * @note Since subscription is only allowed for websocket connection, this interface is used only for websocket + * connections. */ -class SubscriptionContextInterface { +class SubscriptionContextInterface : public util::Taggable { public: - virtual ~SubscriptionContextInterface() = default; + /** + * @brief Reusing Taggable constructor + */ + using util::Taggable::Taggable; /** * @brief Send message to the client @@ -50,7 +57,7 @@ class SubscriptionContextInterface { * @param slot The slot to connect. */ virtual void - onDisconnect(std::function slot) = 0; + onDisconnect(std::function const& slot) = 0; /** * @brief Set the API subversion. @@ -66,6 +73,14 @@ class SubscriptionContextInterface { */ virtual uint32_t apiSubversion() const = 0; + + // TODO: make disconnect protected and add WsBase as a friend. It seems apple clang 15 doesn't support this. + + /** + * @brief Notify the context that connection has been disconnected. + */ + virtual void + disconnect() = 0; }; /** diff --git a/src/web/impl/HttpBase.hpp b/src/web/impl/HttpBase.hpp index c0deacb21..c476432bf 100644 --- a/src/web/impl/HttpBase.hpp +++ b/src/web/impl/HttpBase.hpp @@ -20,6 +20,7 @@ #pragma once #include "rpc/Errors.hpp" +#include "util/Assert.hpp" #include "util/Taggable.hpp" #include "util/build/Build.hpp" #include "util/log/Logger.hpp" @@ -277,8 +278,10 @@ class HttpBase : public ConnectionBase { } SubscriptionContextPtr - subscriptionContext() const override + subscriptionContext(util::TagDecoratorFactory const&) override { + ASSERT(false, "SubscriptionContext can't be created for a HTTP connection"); + std::unreachable(); } void diff --git a/src/web/impl/WsBase.hpp b/src/web/impl/WsBase.hpp index bffd7beee..8b5cf1fb2 100644 --- a/src/web/impl/WsBase.hpp +++ b/src/web/impl/WsBase.hpp @@ -26,6 +26,8 @@ #include "util/prometheus/Gauge.hpp" #include "util/prometheus/Label.hpp" #include "util/prometheus/Prometheus.hpp" +#include "web/SubscriptionContext.hpp" +#include "web/SubscriptionContextInterface.hpp" #include "web/dosguard/DOSGuardInterface.hpp" #include "web/interface/Concepts.hpp" #include "web/interface/ConnectionBase.hpp" @@ -41,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +85,8 @@ class WsBase : public ConnectionBase, public std::enable_shared_from_this> messages_; std::shared_ptr const handler_; + SubscriptionContextPtr subscriptionContext_; + protected: util::Logger log_{"WebServer"}; util::Logger perfLog_{"Performance"}; @@ -125,6 +130,9 @@ class WsBase : public ConnectionBase, public std::enable_shared_from_thisdisconnect(); + LOG(perfLog_.debug()) << tag() << "session closed"; if (!messages_.empty()) messagesLength_.get() -= messages_.size(); @@ -188,6 +196,21 @@ class WsBase : public ConnectionBase, public std::enable_shared_from_this(factory, shared_from_this()); + } + return subscriptionContext_; + } + /** * @brief Send a message to the client * @param msg The message to send diff --git a/src/web/interface/Concepts.hpp b/src/web/interface/Concepts.hpp index b8b90602b..b80285c2a 100644 --- a/src/web/interface/Concepts.hpp +++ b/src/web/interface/Concepts.hpp @@ -19,8 +19,6 @@ #pragma once -#include "web/interface/ConnectionBase.hpp" - #include #include @@ -29,6 +27,8 @@ namespace web { +struct ConnectionBase; + /** * @brief Specifies the requirements a Webserver handler must fulfill. */ diff --git a/src/web/interface/ConnectionBase.hpp b/src/web/interface/ConnectionBase.hpp index 8c7fdec9c..d2857ce6b 100644 --- a/src/web/interface/ConnectionBase.hpp +++ b/src/web/interface/ConnectionBase.hpp @@ -27,7 +27,6 @@ #include #include -#include #include #include #include @@ -50,13 +49,6 @@ struct ConnectionBase : public util::Taggable { public: std::string const clientIp; bool upgraded = false; - boost::signals2::signal onDisconnect; - /** - * @brief The API version of the web stream client. - * This is used to track the api version of this connection, which mainly is used by subscription. It is different - * from the api version in Context, which is only used for the current request. - */ - std::atomic_uint32_t apiSubVersion = 0; /** * @brief Create a new connection base. @@ -69,11 +61,6 @@ struct ConnectionBase : public util::Taggable { { } - ~ConnectionBase() override - { - onDisconnect(this); - }; - /** * @brief Send the response to the client. * @@ -96,12 +83,13 @@ struct ConnectionBase : public util::Taggable { } /** - *@brief Get the subscription context for this connection. + * @brief Get the subscription context for this connection. * + * @param factory Tag TagDecoratorFactory to use to create the context. * @return The subscription context for this connection. */ virtual SubscriptionContextPtr - subscriptionContext() const = 0; + subscriptionContext(util::TagDecoratorFactory const& factory) = 0; /** * @brief Indicates whether the connection had an error and is considered dead. From abdede497f6c146e80e1e608afd7ebcd87cab9a6 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 31 Oct 2024 14:16:12 +0000 Subject: [PATCH 65/81] Fixed tests --- src/web/Context.hpp | 6 +- src/web/SubscriptionContext.cpp | 9 +- src/web/SubscriptionContext.hpp | 8 +- src/web/SubscriptionContextInterface.hpp | 7 +- tests/common/feed/FeedTestUtil.hpp | 24 +--- tests/common/util/HandlerBaseTestFixture.hpp | 17 +-- tests/common/util/MockWsBase.hpp | 12 +- tests/unit/feed/BookChangesFeedTests.cpp | 1 + tests/unit/feed/ForwardFeedTests.cpp | 12 +- tests/unit/feed/LedgerFeedTests.cpp | 9 ++ .../feed/ProposedTransactionFeedTests.cpp | 83 +++++++++--- tests/unit/feed/SingleFeedBaseTests.cpp | 35 +++-- tests/unit/feed/SubscriptionManagerTests.cpp | 73 ++++++---- tests/unit/feed/TrackableSignalTests.cpp | 24 +--- tests/unit/feed/TransactionFeedTests.cpp | 127 +++++++++++++----- tests/unit/rpc/handlers/SubscribeTests.cpp | 55 ++++---- tests/unit/rpc/handlers/UnsubscribeTests.cpp | 16 +-- tests/unit/web/RPCServerHandlerTests.cpp | 28 ++-- tests/unit/web/impl/ErrorHandlingTests.cpp | 7 + 19 files changed, 331 insertions(+), 222 deletions(-) diff --git a/src/web/Context.hpp b/src/web/Context.hpp index 35650ce8f..29b93a674 100644 --- a/src/web/Context.hpp +++ b/src/web/Context.hpp @@ -78,7 +78,11 @@ struct Context : util::Taggable { , method(std::move(command)) , apiVersion(apiVersion) , params(std::move(params)) - , session(connection->subscriptionContext(tagFactory)) + , session([&connection, &tagFactory]() { + if (connection == nullptr) + return SubscriptionContextPtr{}; + return connection->subscriptionContext(tagFactory); + }()) , range(range) , clientIp(std::move(clientIp)) , isAdmin(isAdmin) diff --git a/src/web/SubscriptionContext.cpp b/src/web/SubscriptionContext.cpp index 5f4e0e8c8..297e47abe 100644 --- a/src/web/SubscriptionContext.cpp +++ b/src/web/SubscriptionContext.cpp @@ -24,7 +24,6 @@ #include "web/interface/ConnectionBase.hpp" #include -#include #include #include #include @@ -39,6 +38,11 @@ SubscriptionContext::SubscriptionContext( { } +SubscriptionContext::~SubscriptionContext() +{ + onDisconnect_(this); +} + void SubscriptionContext::send(std::shared_ptr message) { @@ -47,7 +51,7 @@ SubscriptionContext::send(std::shared_ptr message) } void -SubscriptionContext::onDisconnect(std::function const& slot) +SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot) { if (auto connection = connection_.lock(); connection != nullptr) { onDisconnect_.connect(slot); @@ -72,7 +76,6 @@ void SubscriptionContext::disconnect() { connection_.reset(); - onDisconnect_(this); } } // namespace web diff --git a/src/web/SubscriptionContext.hpp b/src/web/SubscriptionContext.hpp index 0988ec504..36896a609 100644 --- a/src/web/SubscriptionContext.hpp +++ b/src/web/SubscriptionContext.hpp @@ -28,7 +28,6 @@ #include #include -#include #include #include @@ -56,6 +55,11 @@ class SubscriptionContext : public SubscriptionContextInterface { */ SubscriptionContext(util::TagDecoratorFactory const& factory, std::shared_ptr connection); + /** + * @brief Destroy the Subscription Context object + */ + ~SubscriptionContext() override; + /** * @brief Get tag decorator. * @@ -80,7 +84,7 @@ class SubscriptionContext : public SubscriptionContextInterface { * @param slot The slot to connect. */ void - onDisconnect(std::function const& slot) override; + onDisconnect(OnDisconnectSlot const& slot) override; /** * @brief Set the API subversion. diff --git a/src/web/SubscriptionContextInterface.hpp b/src/web/SubscriptionContextInterface.hpp index cad356886..a3980f2f0 100644 --- a/src/web/SubscriptionContextInterface.hpp +++ b/src/web/SubscriptionContextInterface.hpp @@ -51,13 +51,18 @@ class SubscriptionContextInterface : public util::Taggable { virtual void send(std::shared_ptr message) = 0; + /** + * @brief Alias for on disconnect slot. + */ + using OnDisconnectSlot = std::function; + /** * @brief Connect a slot to onDisconnect connection signal. * * @param slot The slot to connect. */ virtual void - onDisconnect(std::function const& slot) = 0; + onDisconnect(OnDisconnectSlot const& slot) = 0; /** * @brief Set the API subversion. diff --git a/tests/common/feed/FeedTestUtil.hpp b/tests/common/feed/FeedTestUtil.hpp index dfe872fb5..4de08b884 100644 --- a/tests/common/feed/FeedTestUtil.hpp +++ b/tests/common/feed/FeedTestUtil.hpp @@ -23,7 +23,7 @@ #include "util/MockPrometheus.hpp" #include "util/MockWsBase.hpp" #include "util/SyncExecutionCtxFixture.hpp" -#include "web/interface/ConnectionBase.hpp" +#include "web/SubscriptionContextInterface.hpp" #include #include @@ -37,25 +37,9 @@ template struct FeedBaseTest : util::prometheus::WithPrometheus, MockBackendTest, SyncExecutionCtxFixture { protected: - std::shared_ptr sessionPtr; - std::shared_ptr testFeedPtr; - MockSession* mockSessionPtr = nullptr; - - void - SetUp() override - { - testFeedPtr = std::make_shared(ctx); - sessionPtr = std::make_shared(); - sessionPtr->apiSubVersion = 1; - mockSessionPtr = dynamic_cast(sessionPtr.get()); - } - - void - TearDown() override - { - sessionPtr.reset(); - testFeedPtr.reset(); - } + web::SubscriptionContextPtr sessionPtr = std::make_shared(); + std::shared_ptr testFeedPtr = std::make_shared(ctx); + MockSession* mockSessionPtr = dynamic_cast(sessionPtr.get()); }; namespace impl { diff --git a/tests/common/util/HandlerBaseTestFixture.hpp b/tests/common/util/HandlerBaseTestFixture.hpp index 8823d1b2c..e5d88eb53 100644 --- a/tests/common/util/HandlerBaseTestFixture.hpp +++ b/tests/common/util/HandlerBaseTestFixture.hpp @@ -34,22 +34,7 @@ template