//------------------------------------------------------------------------------ /* 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/LedgerCacheInterface.hpp" #include "util/Taggable.hpp" #include "util/log/Logger.hpp" #include "web/AdminVerificationStrategy.hpp" #include "web/HttpSession.hpp" #include "web/ProxyIpResolver.hpp" #include "web/SslHttpSession.hpp" #include "web/dosguard/DOSGuardInterface.hpp" #include "web/interface/Concepts.hpp" #include "web/ng/impl/ServerSslContext.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @brief This namespace implements the web server and related components. * * The web server is leveraging the power of `boost::asio` with it's coroutine support thru `boost::asio::yield_context` * and `boost::asio::spawn`. * * Majority of the code is based on examples that came with boost. */ namespace web { /** * @brief The Detector class to detect if the connection is a ssl or not. * * If it is an SSL connection, the Detector will pass the ownership of the socket to SslSessionType, otherwise to * PlainSessionType. * * @tparam PlainSessionType The plain session type * @tparam SslSessionType The SSL session type * @tparam HandlerType The executor to handle the requests */ template < 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; util::Logger log_{"WebServer"}; boost::beast::tcp_stream stream_; std::optional> ctx_; std::reference_wrapper tagFactory_; std::reference_wrapper const dosGuard_; std::shared_ptr const handler_; std::reference_wrapper cache_; boost::beast::flat_buffer buffer_; std::shared_ptr const adminVerification_; std::uint32_t maxWsSendingQueueSize_; std::shared_ptr proxyIpResolver_; public: /** * @brief Create a new detector. * * @param socket The socket. Ownership is transferred * @param ctx The SSL context if any * @param tagFactory A factory that is used to generate tags to track requests and sessions * @param dosGuard The denial of service guard to use * @param handler The server handler to use * @param cache The ledger cache to use * @param adminVerification The admin verification strategy to use * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket * @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy */ Detector( tcp::socket&& socket, std::optional> ctx, std::reference_wrapper tagFactory, std::reference_wrapper dosGuard, std::shared_ptr handler, std::reference_wrapper cache, std::shared_ptr adminVerification, std::uint32_t maxWsSendingQueueSize, std::shared_ptr proxyIpResolver ) : stream_(std::move(socket)) , ctx_(ctx) , tagFactory_(std::cref(tagFactory)) , dosGuard_(dosGuard) , handler_(std::move(handler)) , cache_(cache) , adminVerification_(std::move(adminVerification)) , maxWsSendingQueueSize_(maxWsSendingQueueSize) , proxyIpResolver_(std::move(proxyIpResolver)) { } /** * @brief A helper function that is called when any error occurs. * * @param ec The error code * @param message The message to include in the log */ void fail(boost::system::error_code ec, char const* message) { if (ec == boost::asio::ssl::error::stream_truncated) return; LOG(log_.info()) << "Detector failed (" << message << "): " << ec.message(); } /** @brief Initiate the detection. */ void run() { boost::beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); async_detect_ssl(stream_, buffer_, boost::beast::bind_front_handler(&Detector::onDetect, shared_from_this())); } /** * @brief Handles detection result. * * @param ec The error code * @param result true if SSL is detected; false otherwise */ void onDetect(boost::beast::error_code ec, bool result) { if (ec) return fail(ec, "detect"); std::string ip; try { ip = stream_.socket().remote_endpoint().address().to_string(); } catch (std::exception const&) { return fail(ec, "cannot get remote endpoint"); } if (result) { if (!ctx_) return fail(ec, "SSL is not supported by this server"); std::make_shared>( stream_.release_socket(), ip, adminVerification_, proxyIpResolver_, *ctx_, tagFactory_, dosGuard_, handler_, cache_, std::move(buffer_), maxWsSendingQueueSize_ ) ->run(); return; } std::make_shared>( stream_.release_socket(), ip, adminVerification_, proxyIpResolver_, tagFactory_, dosGuard_, handler_, cache_, std::move(buffer_), maxWsSendingQueueSize_ ) ->run(); } }; /** * @brief The WebServer class. It creates server socket and start listening on it. * * Once there is client connection, it will accept it and pass the socket to Detector to detect ssl or plain. * * @tparam PlainSessionType The plain session to handle non-ssl connection. * @tparam SslSessionType The SSL session to handle SSL connection. * @tparam HandlerType The handler to process the request and return response. */ template < 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; util::Logger log_{"WebServer"}; std::reference_wrapper ioc_; std::optional ctx_; util::TagDecoratorFactory tagFactory_; std::reference_wrapper dosGuard_; std::shared_ptr handler_; std::reference_wrapper cache_; tcp::acceptor acceptor_; std::shared_ptr adminVerification_; std::uint32_t maxWsSendingQueueSize_; std::shared_ptr proxyIpResolver_; public: /** * @brief Create a new instance of the web server. * * @param ioc The io_context to run the server on * @param ctx The SSL context if any * @param endpoint The endpoint to listen on * @param tagFactory A factory that is used to generate tags to track requests and sessions * @param dosGuard The denial of service guard to use * @param handler The server handler to use * @param cache The ledger cache to use * @param adminVerification The admin verification strategy to use * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket * @param proxyIpResolver The client ip resolver if a request was forwarded by a proxy */ Server( boost::asio::io_context& ioc, std::optional ctx, tcp::endpoint endpoint, util::TagDecoratorFactory tagFactory, dosguard::DOSGuardInterface& dosGuard, std::shared_ptr handler, std::reference_wrapper cache, std::shared_ptr adminVerification, std::uint32_t maxWsSendingQueueSize, ProxyIpResolver proxyIpResolver ) : ioc_(std::ref(ioc)) , ctx_(std::move(ctx)) , tagFactory_(tagFactory) , dosGuard_(std::ref(dosGuard)) , handler_(std::move(handler)) , cache_(cache) , acceptor_(boost::asio::make_strand(ioc)) , adminVerification_(std::move(adminVerification)) , maxWsSendingQueueSize_(maxWsSendingQueueSize) , proxyIpResolver_(std::make_shared(std::move(proxyIpResolver))) { boost::beast::error_code ec; acceptor_.open(endpoint.protocol(), ec); if (ec) return; acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec); if (ec) return; acceptor_.bind(endpoint, ec); if (ec) { LOG(log_.error()) << "Failed to bind to endpoint: " << endpoint << ". message: " << ec.message(); throw std::runtime_error( fmt::format("Failed to bind to endpoint: {}:{}", endpoint.address().to_string(), endpoint.port()) ); } acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec); if (ec) { LOG(log_.error()) << "Failed to listen at endpoint: " << endpoint << ". message: " << ec.message(); throw std::runtime_error( fmt::format("Failed to listen at endpoint: {}:{}", endpoint.address().to_string(), endpoint.port()) ); } } /** @brief Start accepting incoming connections. */ void run() { doAccept(); } private: void doAccept() { acceptor_.async_accept( boost::asio::make_strand(ioc_.get()), boost::beast::bind_front_handler(&Server::onAccept, shared_from_this()) ); } void onAccept(boost::beast::error_code ec, tcp::socket socket) { if (!ec) { auto ctxRef = ctx_ ? std::optional>{ctx_.value()} : std::nullopt; std::make_shared>( std::move(socket), ctxRef, std::cref(tagFactory_), dosGuard_, handler_, cache_, adminVerification_, maxWsSendingQueueSize_, proxyIpResolver_ ) ->run(); } doAccept(); } }; /** @brief The final type of the HttpServer used by Clio. */ template using HttpServer = Server; /** * @brief A factory function that spawns a ready to use HTTP server. * * @tparam HandlerType The type of handler to process the request * @param config The config to create server * @param ioc The server will run under this io_context * @param dosGuard The dos guard to protect the server * @param handler The handler to process the request * @param cache The ledger cache to use * @return The server instance */ template static std::shared_ptr> makeHttpServer( util::config::ClioConfigDefinition const& config, boost::asio::io_context& ioc, dosguard::DOSGuardInterface& dosGuard, std::shared_ptr const& handler, std::reference_wrapper cache ) { static util::Logger const log{"WebServer"}; // NOLINT(readability-identifier-naming) auto expectedSslContext = ng::impl::makeServerSslContext(config); if (not expectedSslContext) { LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error(); return nullptr; } auto const serverConfig = config.getObject("server"); auto const address = boost::asio::ip::make_address(serverConfig.get("ip")); auto const port = serverConfig.get("port"); auto expectedAdminVerification = makeAdminVerificationStrategy(config); if (not expectedAdminVerification.has_value()) { LOG(log.error()) << expectedAdminVerification.error(); throw std::logic_error{expectedAdminVerification.error()}; } // If the transactions number is 200 per ledger, A client which subscribes everything will send 400+ feeds for // each ledger. we allow user delay 3 ledgers by default auto const maxWsSendingQueueSize = serverConfig.get("ws_max_sending_queue_size"); auto proxyIpResolver = ProxyIpResolver::fromConfig(config); auto server = std::make_shared>( ioc, std::move(expectedSslContext).value(), boost::asio::ip::tcp::endpoint{address, port}, util::TagDecoratorFactory(config), dosGuard, handler, cache, std::move(expectedAdminVerification).value(), maxWsSendingQueueSize, std::move(proxyIpResolver) ); server->run(); return server; } } // namespace web