From 2fef03d76687b6c7a8b10bf25ced66718fa742a9 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 1 Aug 2024 10:53:17 +0100 Subject: [PATCH] refactor: Refactor main (#1555) For #442. --- .github/workflows/build.yml | 15 +- .github/workflows/nightly.yml | 11 + .gitignore | 2 +- cmake/Build.cpp.in | 7 +- cmake/ClioVersion.cmake | 2 +- src/CMakeLists.txt | 1 + src/app/CMakeLists.txt | 4 + src/app/CliArgs.cpp | 70 ++++++ src/app/CliArgs.hpp | 96 ++++++++ src/app/ClioApplication.cpp | 142 +++++++++++ src/app/ClioApplication.hpp | 51 ++++ src/data/cassandra/impl/Cluster.cpp | 12 +- src/main/CMakeLists.txt | 9 +- src/main/Main.cpp | 228 ++---------------- src/rpc/Errors.cpp | 14 +- src/rpc/handlers/ServerInfo.hpp | 4 +- src/util/CMakeLists.txt | 3 +- src/util/OverloadSet.hpp | 42 ++++ src/{main => util/build}/Build.hpp | 4 +- src/util/requests/impl/SslContext.cpp | 4 +- src/util/requests/impl/SslContext.hpp | 2 +- src/util/requests/impl/StreamData.hpp | 2 +- src/web/CMakeLists.txt | 5 +- src/web/Server.cpp | 48 ++++ src/web/Server.hpp | 30 ++- src/web/impl/HttpBase.hpp | 4 +- src/web/impl/ServerSslContext.cpp | 81 +++++++ src/web/impl/ServerSslContext.hpp | 32 +++ tests/common/CMakeLists.txt | 2 +- tests/unit/CMakeLists.txt | 8 + tests/unit/app/CliArgsTests.cpp | 76 ++++++ tests/unit/test_data/cert.pem | 22 ++ tests/unit/test_data/key.pem | 27 +++ tests/unit/util/requests/SslContextTests.cpp | 2 +- tests/unit/web/ServerTests.cpp | 186 ++++++-------- tests/unit/web/impl/ServerSslContextTests.cpp | 48 ++++ 36 files changed, 920 insertions(+), 376 deletions(-) create mode 100644 src/app/CMakeLists.txt create mode 100644 src/app/CliArgs.cpp create mode 100644 src/app/CliArgs.hpp create mode 100644 src/app/ClioApplication.cpp create mode 100644 src/app/ClioApplication.hpp create mode 100644 src/util/OverloadSet.hpp rename src/{main => util/build}/Build.hpp (95%) create mode 100644 src/web/Server.cpp create mode 100644 src/web/impl/ServerSslContext.cpp create mode 100644 src/web/impl/ServerSslContext.hpp create mode 100644 tests/unit/app/CliArgsTests.cpp create mode 100644 tests/unit/test_data/cert.pem create mode 100644 tests/unit/test_data/key.pem create mode 100644 tests/unit/web/impl/ServerSslContextTests.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f9027fd8..8342fd030 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: run: | ./.githooks/check-format --diff shell: bash - + check_docs: name: Check documentation runs-on: ubuntu-20.04 @@ -149,6 +149,13 @@ 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: @@ -211,6 +218,12 @@ jobs: - uses: actions/download-artifact@v4 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 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 44269adb0..32ff87b31 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -71,6 +71,12 @@ jobs: name: clio_tests_${{ runner.os }}_${{ matrix.build_type }} path: build/clio_*tests + - name: Upload test data + uses: actions/upload-artifact@v4 + with: + name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }} + path: build/tests/unit/test_data + - name: Compress clio_server shell: bash run: | @@ -124,6 +130,11 @@ jobs: with: name: clio_tests_${{ runner.os }}_${{ matrix.build_type }} + - uses: actions/download-artifact@v4 + with: + name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }} + path: tests/unit/test_data + - name: Run clio_tests run: | chmod +x ./clio_tests diff --git a/.gitignore b/.gitignore index ac2a7062c..2f3da47ef 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ .DS_Store CMakeUserPresets.json config.json -src/main/impl/Build.cpp +src/util/build/Build.cpp diff --git a/cmake/Build.cpp.in b/cmake/Build.cpp.in index 574ec03fd..495799160 100644 --- a/cmake/Build.cpp.in +++ b/cmake/Build.cpp.in @@ -17,11 +17,12 @@ */ //============================================================================== -#include "main/Build.hpp" +#include "util/build/Build.hpp" #include -namespace Build { +namespace util::build { + static constexpr char versionString[] = "@CLIO_VERSION@"; std::string const& @@ -38,4 +39,4 @@ getClioFullVersionString() return value; } -} // namespace Build +} // namespace util::build diff --git a/cmake/ClioVersion.cmake b/cmake/ClioVersion.cmake index d8e194e35..4a97e3db0 100644 --- a/cmake/ClioVersion.cmake +++ b/cmake/ClioVersion.cmake @@ -45,4 +45,4 @@ endif () message(STATUS "Build version: ${CLIO_VERSION}") -configure_file(${CMAKE_CURRENT_LIST_DIR}/Build.cpp.in ${CMAKE_CURRENT_LIST_DIR}/../src/main/impl/Build.cpp) +configure_file(${CMAKE_CURRENT_LIST_DIR}/Build.cpp.in ${CMAKE_CURRENT_LIST_DIR}/../src/util/build/Build.cpp) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 212156583..02a38ad1f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,4 +4,5 @@ add_subdirectory(etl) add_subdirectory(feed) add_subdirectory(rpc) add_subdirectory(web) +add_subdirectory(app) add_subdirectory(main) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 000000000..e775205aa --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(clio_app) +target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp) + +target_link_libraries(clio_app PUBLIC clio_etl clio_feed clio_web clio_rpc) diff --git a/src/app/CliArgs.cpp b/src/app/CliArgs.cpp new file mode 100644 index 000000000..4780f2f69 --- /dev/null +++ b/src/app/CliArgs.cpp @@ -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. +*/ +//============================================================================== + +#include "app/CliArgs.hpp" + +#include "util/build/Build.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace app { + +CliArgs::Action +CliArgs::parse(int argc, char const* argv[]) +{ + namespace po = boost::program_options; + // clang-format off + po::options_description description("Options"); + description.add_options() + ("help,h", "print help message and exit") + ("version,v", "print version and exit") + ("conf,c", po::value()->default_value(defaultConfigPath), "configuration file") + ; + // clang-format on + po::positional_options_description positional; + positional.add("conf", 1); + + po::variables_map parsed; + po::store(po::command_line_parser(argc, argv).options(description).positional(positional).run(), parsed); + po::notify(parsed); + + if (parsed.count("help") != 0u) { + std::cout << "Clio server " << util::build::getClioFullVersionString() << "\n\n" << description; + return Action{Action::Exit{EXIT_SUCCESS}}; + } + + if (parsed.count("version") != 0u) { + std::cout << util::build::getClioFullVersionString() << '\n'; + return Action{Action::Exit{EXIT_SUCCESS}}; + } + + auto configPath = parsed["conf"].as(); + return Action{Action::Run{std::move(configPath)}}; +} + +} // namespace app diff --git a/src/app/CliArgs.hpp b/src/app/CliArgs.hpp new file mode 100644 index 000000000..bc7dd738c --- /dev/null +++ b/src/app/CliArgs.hpp @@ -0,0 +1,96 @@ +//------------------------------------------------------------------------------ +/* + 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/OverloadSet.hpp" + +#include +#include + +namespace app { + +/** + * @brief Parsed command line arguments representation. + */ +class CliArgs { +public: + /** + * @brief Default configuration path. + */ + static constexpr char defaultConfigPath[] = "/etc/opt/clio/config.json"; + + /** + * @brief An action parsed from the command line. + */ + class Action { + public: + /** @brief Run action. */ + struct Run { + /** @brief Configuration file path. */ + std::string configPath; + }; + + /** @brief Exit action. */ + struct Exit { + /** @brief Exit code. */ + int exitCode; + }; + + /** + * @brief Construct an action from a Run. + * + * @param action Run action. + */ + template + requires std::is_same_v or std::is_same_v + explicit Action(ActionType&& action) : action_(std::forward(action)) + { + } + + /** + * @brief Apply a function to the action. + * + * @tparam Processors Action processors types. Must be callable with the action type and return int. + * @param processors Action processors. + * @return Exit code. + */ + template + int + apply(Processors&&... processors) const + { + return std::visit(util::OverloadSet{std::forward(processors)...}, action_); + } + + private: + std::variant action_; + }; + + /** + * @brief Parse command line arguments. + * + * @param argc Number of arguments. + * @param argv Array of arguments. + * @return Parsed command line arguments. + */ + static Action + parse(int argc, char const* argv[]); +}; + +} // namespace app diff --git a/src/app/ClioApplication.cpp b/src/app/ClioApplication.cpp new file mode 100644 index 000000000..458482fc8 --- /dev/null +++ b/src/app/ClioApplication.cpp @@ -0,0 +1,142 @@ +//------------------------------------------------------------------------------ +/* + 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 "app/ClioApplication.hpp" + +#include "data/AmendmentCenter.hpp" +#include "data/BackendFactory.hpp" +#include "etl/ETLService.hpp" +#include "etl/LoadBalancer.hpp" +#include "etl/NetworkValidatedLedgers.hpp" +#include "feed/SubscriptionManager.hpp" +#include "rpc/Counters.hpp" +#include "rpc/RPCEngine.hpp" +#include "rpc/WorkQueue.hpp" +#include "rpc/common/impl/HandlerProvider.hpp" +#include "util/build/Build.hpp" +#include "util/config/Config.hpp" +#include "util/log/Logger.hpp" +#include "util/prometheus/Prometheus.hpp" +#include "web/DOSGuard.hpp" +#include "web/IntervalSweepHandler.hpp" +#include "web/RPCServerHandler.hpp" +#include "web/Server.hpp" +#include "web/WhitelistHandler.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace app { + +namespace { + +/** + * @brief Start context threads + * + * @param ioc Context + * @param numThreads Number of worker threads to start + */ +void +start(boost::asio::io_context& ioc, std::uint32_t numThreads) +{ + std::vector v; + v.reserve(numThreads - 1); + for (auto i = numThreads - 1; i > 0; --i) + v.emplace_back([&ioc] { ioc.run(); }); + + ioc.run(); + for (auto& t : v) + t.join(); +} + +} // namespace + +ClioApplication::ClioApplication(util::Config const& config) : config_(config), signalsHandler_{config_} +{ + LOG(util::LogService::info()) << "Clio version: " << util::build::getClioFullVersionString(); + PrometheusService::init(config); +} + +int +ClioApplication::run() +{ + 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 sweepHandler = web::IntervalSweepHandler{config_, ioc}; + auto whitelistHandler = web::WhitelistHandler{config_}; + auto dosGuard = web::DOSGuard{config_, whitelistHandler, sweepHandler}; + + // Interface to the database + auto backend = data::make_Backend(config_); + + // Manages clients subscribed to streams + auto subscriptionsRunner = feed::SubscriptionManagerRunner(config_, backend); + + auto const subscriptions = subscriptionsRunner.getManager(); + + // 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 + ); + auto const rpcEngine = + rpc::RPCEngine::make_RPCEngine(backend, balancer, dosGuard, workQueue, counters, handlerProvider); + + // 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; +} + +} // namespace app diff --git a/src/app/ClioApplication.hpp b/src/app/ClioApplication.hpp new file mode 100644 index 000000000..6bb31ea7a --- /dev/null +++ b/src/app/ClioApplication.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 "util/SignalsHandler.hpp" +#include "util/config//Config.hpp" + +namespace app { + +/** + * @brief The main application class + */ +class ClioApplication { + util::Config const& config_; + util::SignalsHandler signalsHandler_; + +public: + /** + * @brief Construct a new ClioApplication object + * + * @param config The configuration of the application + */ + ClioApplication(util::Config const& config); + + /** + * @brief Run the application + * + * @return exit code + */ + int + run(); +}; + +} // namespace app diff --git a/src/data/cassandra/impl/Cluster.cpp b/src/data/cassandra/impl/Cluster.cpp index 7989241aa..80c6b4e7c 100644 --- a/src/data/cassandra/impl/Cluster.cpp +++ b/src/data/cassandra/impl/Cluster.cpp @@ -21,6 +21,7 @@ #include "data/cassandra/impl/ManagedObject.hpp" #include "data/cassandra/impl/SslContext.hpp" +#include "util/OverloadSet.hpp" #include "util/log/Logger.hpp" #include @@ -31,16 +32,9 @@ #include namespace { -constexpr auto clusterDeleter = [](CassCluster* ptr) { cass_cluster_free(ptr); }; -template -struct overloadSet : Ts... { - using Ts::operator()...; -}; +constexpr auto clusterDeleter = [](CassCluster* ptr) { cass_cluster_free(ptr); }; -// explicit deduction guide (not needed as of C++20, but clang be clang) -template -overloadSet(Ts...) -> overloadSet; }; // namespace namespace data::cassandra::impl { @@ -90,7 +84,7 @@ void Cluster::setupConnection(Settings const& settings) { std::visit( - overloadSet{ + util::OverloadSet{ [this](Settings::ContactPoints const& points) { setupContactPoints(points); }, [this](Settings::SecureConnectionBundle const& bundle) { setupSecureBundle(bundle); } }, diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index e8a701acc..45ae9823f 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -1,14 +1,7 @@ -add_library(clio) -target_sources(clio PRIVATE impl/Build.cpp) - -target_link_libraries(clio PUBLIC clio_etl clio_feed clio_web clio_rpc) - -target_compile_features(clio PUBLIC cxx_std_23) - # Clio server add_executable(clio_server) target_sources(clio_server PRIVATE Main.cpp) -target_link_libraries(clio_server PRIVATE clio) +target_link_libraries(clio_server PRIVATE clio_app) if (static) if (san) diff --git a/src/main/Main.cpp b/src/main/Main.cpp index 9fec31c56..929c37e1f 100644 --- a/src/main/Main.cpp +++ b/src/main/Main.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2022-2023, the clio developers. + Copyright (c) 2022-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 @@ -17,234 +17,44 @@ */ //============================================================================== -#include "data/AmendmentCenter.hpp" -#include "data/BackendFactory.hpp" -#include "etl/ETLService.hpp" -#include "etl/NetworkValidatedLedgers.hpp" -#include "feed/SubscriptionManager.hpp" -#include "main/Build.hpp" -#include "rpc/Counters.hpp" -#include "rpc/RPCEngine.hpp" -#include "rpc/WorkQueue.hpp" +#include "app/CliArgs.hpp" +#include "app/ClioApplication.hpp" #include "rpc/common/impl/HandlerProvider.hpp" -#include "util/SignalsHandler.hpp" #include "util/TerminationHandler.hpp" #include "util/config/Config.hpp" #include "util/log/Logger.hpp" -#include "util/prometheus/Prometheus.hpp" -#include "web/DOSGuard.hpp" -#include "web/IntervalSweepHandler.hpp" -#include "web/RPCServerHandler.hpp" -#include "web/Server.hpp" #include #include #include -#include -#include -#include -#include -#include -#include #include #include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include - -using namespace util; -using namespace boost::asio; - -namespace po = boost::program_options; - -/** - * @brief Parse command line and return path to configuration file - * - * @param argc - * @param argv - * @return Path to configuration file - */ -std::string -parseCli(int argc, char* argv[]) -{ - static constexpr char defaultConfigPath[] = "/etc/opt/clio/config.json"; - - // clang-format off - po::options_description description("Options"); - description.add_options() - ("help,h", "print help message and exit") - ("version,v", "print version and exit") - ("conf,c", po::value()->default_value(defaultConfigPath), "configuration file") - ; - // clang-format on - po::positional_options_description positional; - positional.add("conf", 1); - - po::variables_map parsed; - po::store(po::command_line_parser(argc, argv).options(description).positional(positional).run(), parsed); - po::notify(parsed); - - if (parsed.count("version") != 0u) { - std::cout << Build::getClioFullVersionString() << '\n'; - std::exit(EXIT_SUCCESS); - } - - if (parsed.count("help") != 0u) { - std::cout << "Clio server " << Build::getClioFullVersionString() << "\n\n" << description; - std::exit(EXIT_SUCCESS); - } - - return parsed["conf"].as(); -} - -/** - * @brief Parse certificates from configuration file - * - * @param config The configuration - * @return SSL context if certificates were parsed - */ -std::optional -parseCerts(Config const& config) -{ - if (!config.contains("ssl_cert_file") || !config.contains("ssl_key_file")) - return {}; - - auto certFilename = config.value("ssl_cert_file"); - auto keyFilename = config.value("ssl_key_file"); - - std::ifstream const readCert(certFilename, std::ios::in | std::ios::binary); - if (!readCert) - return {}; - - std::stringstream contents; - contents << readCert.rdbuf(); - std::string cert = contents.str(); - - std::ifstream readKey(keyFilename, std::ios::in | std::ios::binary); - if (!readKey) - return {}; - - contents.str(""); - contents << readKey.rdbuf(); - readKey.close(); - std::string key = contents.str(); - - ssl::context ctx{ssl::context::tls_server}; - ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2); - ctx.use_certificate_chain(buffer(cert.data(), cert.size())); - ctx.use_private_key(buffer(key.data(), key.size()), ssl::context::file_format::pem); - - return ctx; -} - -/** - * @brief Start context threads - * - * @param ioc Context - * @param numThreads Number of worker threads to start - */ -void -start(io_context& ioc, std::uint32_t numThreads) -{ - std::vector v; - v.reserve(numThreads - 1); - for (auto i = numThreads - 1; i > 0; --i) - v.emplace_back([&ioc] { ioc.run(); }); - - ioc.run(); - for (auto& t : v) - t.join(); -} int -main(int argc, char* argv[]) +main(int argc, char const* argv[]) try { util::setTerminationHandler(); - auto const configPath = parseCli(argc, argv); - auto const config = ConfigReader::open(configPath); - if (!config) { - std::cerr << "Couldnt parse config '" << configPath << "'." << std::endl; - return EXIT_FAILURE; - } - util::SignalsHandler signalsHandler{config}; - - LogService::init(config); - LOG(LogService::info()) << "Clio version: " << Build::getClioFullVersionString(); - - PrometheusService::init(config); - - auto const threads = config.valueOr("io_threads", 2); - if (threads <= 0) { - LOG(LogService::fatal()) << "io_threads is less than 1"; - return EXIT_FAILURE; - } - LOG(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. - io_context ioc{threads}; - - // Rate limiter, to prevent abuse - auto sweepHandler = web::IntervalSweepHandler{config, ioc}; - auto whitelistHandler = web::WhitelistHandler{config}; - auto dosGuard = web::DOSGuard{config, whitelistHandler, sweepHandler}; - - // Interface to the database - auto backend = data::make_Backend(config); - - // Manages clients subscribed to streams - auto subscriptionsRunner = feed::SubscriptionManagerRunner(config, backend); - - auto const subscriptions = subscriptionsRunner.getManager(); - - // 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 + auto const action = app::CliArgs::parse(argc, argv); + return action.apply( + [](app::CliArgs::Action::Exit const& exit) { return exit.exitCode; }, + [](app::CliArgs::Action::Run const& run) { + auto const config = util::ConfigReader::open(run.configPath); + if (!config) { + std::cerr << "Couldnt parse config '" << run.configPath << "'." << std::endl; + return EXIT_FAILURE; + } + util::LogService::init(config); + app::ClioApplication clio{config}; + return clio.run(); + } ); - auto const rpcEngine = - rpc::RPCEngine::make_RPCEngine(backend, balancer, dosGuard, workQueue, counters, handlerProvider); - - // Init the web server - auto handler = - std::make_shared>(config, backend, rpcEngine, etl); - auto ctx = parseCerts(config); - auto const ctxRef = ctx ? std::optional>{ctx.value()} : std::nullopt; - auto const httpServer = web::make_HttpServer(config, ioc, ctxRef, 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; } catch (std::exception const& e) { - LOG(LogService::fatal()) << "Exit on exception: " << e.what(); + LOG(util::LogService::fatal()) << "Exit on exception: " << e.what(); return EXIT_FAILURE; } catch (...) { - LOG(LogService::fatal()) << "Exit on exception: unknown"; + LOG(util::LogService::fatal()) << "Exit on exception: unknown"; return EXIT_FAILURE; } diff --git a/src/rpc/Errors.cpp b/src/rpc/Errors.cpp index 5d47b8e0d..622c7ecab 100644 --- a/src/rpc/Errors.cpp +++ b/src/rpc/Errors.cpp @@ -20,6 +20,7 @@ #include "rpc/Errors.hpp" #include "rpc/JS.hpp" +#include "util/OverloadSet.hpp" #include #include @@ -36,17 +37,6 @@ using namespace std; -namespace { -template -struct overloadSet : Ts... { - using Ts::operator()...; -}; - -// explicit deduction guide (not needed as of C++20, but clang be clang) -template -overloadSet(Ts...) -> overloadSet; -} // namespace - namespace rpc { WarningInfo const& @@ -149,7 +139,7 @@ makeError(Status const& status) auto wrapOptional = [](string_view const& str) { return str.empty() ? nullopt : make_optional(str); }; auto res = visit( - overloadSet{ + util::OverloadSet{ [&status, &wrapOptional](RippledError err) { if (err == ripple::rpcUNKNOWN) return boost::json::object{{"error", status.message}, {"type", "response"}, {"status", "error"}}; diff --git a/src/rpc/handlers/ServerInfo.hpp b/src/rpc/handlers/ServerInfo.hpp index 4b42237dc..efbc26a66 100644 --- a/src/rpc/handlers/ServerInfo.hpp +++ b/src/rpc/handlers/ServerInfo.hpp @@ -22,11 +22,11 @@ #include "data/BackendInterface.hpp" #include "data/DBHelpers.hpp" #include "feed/SubscriptionManagerInterface.hpp" -#include "main/Build.hpp" #include "rpc/Errors.hpp" #include "rpc/JS.hpp" #include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" +#include "util/build/Build.hpp" #include #include @@ -124,7 +124,7 @@ class BaseServerInfoHandler { uint32_t loadFactor = 1u; std::chrono::time_point time = std::chrono::system_clock::now(); std::chrono::seconds uptime = {}; - std::string clioVersion = Build::getClioVersionString(); + std::string clioVersion = util::build::getClioVersionString(); std::string xrplVersion = ripple::BuildInfo::getVersionString(); std::optional rippledInfo = std::nullopt; ValidatedLedgerSection validatedLedger = {}; diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index f32af5c07..c07168cde 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -2,7 +2,8 @@ add_library(clio_util) target_sources( clio_util - PRIVATE config/Config.cpp + PRIVATE build/Build.cpp + config/Config.cpp log/Logger.cpp prometheus/Http.cpp prometheus/Label.cpp diff --git a/src/util/OverloadSet.hpp b/src/util/OverloadSet.hpp new file mode 100644 index 000000000..f9e2eb264 --- /dev/null +++ b/src/util/OverloadSet.hpp @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +/* + 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 util { + +/** + * @brief Overload set for lambdas + * + * @tparam Ts Types of lambdas + */ +template +struct OverloadSet : Ts... { + using Ts::operator()...; +}; + +/** + * @brief Deduction guide for OverloadSet + * + * @tparam Ts Types of lambdas + */ +template +OverloadSet(Ts...) -> OverloadSet; + +} // namespace util diff --git a/src/main/Build.hpp b/src/util/build/Build.hpp similarity index 95% rename from src/main/Build.hpp rename to src/util/build/Build.hpp index 2d396a7c0..0230b95c8 100644 --- a/src/main/Build.hpp +++ b/src/util/build/Build.hpp @@ -21,7 +21,7 @@ #include -namespace Build { +namespace util::build { std::string const& getClioVersionString(); @@ -29,4 +29,4 @@ getClioVersionString(); std::string const& getClioFullVersionString(); -} // namespace Build +} // namespace util::build diff --git a/src/util/requests/impl/SslContext.cpp b/src/util/requests/impl/SslContext.cpp index c9e9af442..d982c100f 100644 --- a/src/util/requests/impl/SslContext.cpp +++ b/src/util/requests/impl/SslContext.cpp @@ -82,9 +82,9 @@ getRootCertificate() } // namespace std::expected -makeSslContext() +makeClientSslContext() { - ssl::context context{ssl::context::sslv23_client}; + ssl::context context{ssl::context::tls_client}; context.set_verify_mode(ssl::verify_peer); auto const rootCertificate = getRootCertificate(); if (not rootCertificate.has_value()) { diff --git a/src/util/requests/impl/SslContext.hpp b/src/util/requests/impl/SslContext.hpp index 158e7aa8f..d4dac6ac1 100644 --- a/src/util/requests/impl/SslContext.hpp +++ b/src/util/requests/impl/SslContext.hpp @@ -31,7 +31,7 @@ namespace util::requests::impl { std::expected -makeSslContext(); +makeClientSslContext(); std::optional sslErrorToString(boost::beast::error_code const& error); diff --git a/src/util/requests/impl/StreamData.hpp b/src/util/requests/impl/StreamData.hpp index e026b3fac..571960e15 100644 --- a/src/util/requests/impl/StreamData.hpp +++ b/src/util/requests/impl/StreamData.hpp @@ -61,7 +61,7 @@ class SslStreamData { static std::expected create(boost::asio::yield_context yield) { - auto sslContext = makeSslContext(); + auto sslContext = makeClientSslContext(); if (not sslContext.has_value()) { return std::unexpected{std::move(sslContext.error())}; } diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 9e9daa0db..fa33c2bcf 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -1,5 +1,8 @@ add_library(clio_web) -target_sources(clio_web PRIVATE impl/AdminVerificationStrategy.cpp IntervalSweepHandler.cpp Resolver.cpp) +target_sources( + clio_web PRIVATE impl/AdminVerificationStrategy.cpp impl/ServerSslContext.cpp IntervalSweepHandler.cpp Resolver.cpp + Server.cpp +) target_link_libraries(clio_web PUBLIC clio_util) diff --git a/src/web/Server.cpp b/src/web/Server.cpp new file mode 100644 index 000000000..a20d72ee3 --- /dev/null +++ b/src/web/Server.cpp @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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 373c57c5f..76a0c8120 100644 --- a/src/web/Server.hpp +++ b/src/web/Server.hpp @@ -24,6 +24,7 @@ #include "web/DOSGuard.hpp" #include "web/HttpSession.hpp" #include "web/SslHttpSession.hpp" +#include "web/impl/ServerSslContext.hpp" #include "web/interface/Concepts.hpp" #include @@ -58,6 +59,15 @@ */ 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. * @@ -201,7 +211,7 @@ class Server : public std::enable_shared_from_this ioc_; - std::optional> ctx_; + std::optional ctx_; util::TagDecoratorFactory tagFactory_; std::reference_wrapper dosGuard_; std::shared_ptr handler_; @@ -222,7 +232,7 @@ class Server : public std::enable_shared_from_this> ctx, + std::optional ctx, tcp::endpoint endpoint, util::TagDecoratorFactory tagFactory, web::DOSGuard& dosGuard, @@ -230,7 +240,7 @@ class Server : public std::enable_shared_from_this adminPassword ) : ioc_(std::ref(ioc)) - , ctx_(ctx) + , ctx_(std::move(ctx)) , tagFactory_(tagFactory) , dosGuard_(std::ref(dosGuard)) , handler_(std::move(handler)) @@ -285,7 +295,8 @@ class Server : public std::enable_shared_from_this>{ctx_} : std::nullopt; + auto ctxRef = + ctx_ ? std::optional>{ctx_.value()} : std::nullopt; std::make_shared>( std::move(socket), ctxRef, std::cref(tagFactory_), dosGuard_, handler_, adminVerification_ @@ -307,7 +318,6 @@ using HttpServer = Server; * @tparam HandlerType The tyep of handler to process the request * @param config The config to create server * @param ioc The server will run under this io_context - * @param ctx The SSL context if any * @param dosGuard The dos guard to protect the server * @param handler The handler to process the request * @return The server instance @@ -317,12 +327,18 @@ static std::shared_ptr> make_HttpServer( util::Config const& config, boost::asio::io_context& ioc, - std::optional> const& ctx, web::DOSGuard& dosGuard, std::shared_ptr const& handler ) { static util::Logger const log{"WebServer"}; + + auto expectedSslContext = makeServerSslContext(config); + if (not expectedSslContext) { + LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error(); + return nullptr; + } + if (!config.contains("server")) return nullptr; @@ -347,7 +363,7 @@ make_HttpServer( auto server = std::make_shared>( ioc, - ctx, + std::move(expectedSslContext).value(), boost::asio::ip::tcp::endpoint{address, port}, util::TagDecoratorFactory(config), dosGuard, diff --git a/src/web/impl/HttpBase.hpp b/src/web/impl/HttpBase.hpp index 957f1f8fc..45b958a70 100644 --- a/src/web/impl/HttpBase.hpp +++ b/src/web/impl/HttpBase.hpp @@ -19,9 +19,9 @@ #pragma once -#include "main/Build.hpp" #include "rpc/Errors.hpp" #include "util/Taggable.hpp" +#include "util/build/Build.hpp" #include "util/log/Logger.hpp" #include "util/prometheus/Http.hpp" #include "web/DOSGuard.hpp" @@ -297,7 +297,7 @@ class HttpBase : public ConnectionBase { httpResponse(http::status status, std::string content_type, std::string message) const { http::response res{status, req_.version()}; - res.set(http::field::server, "clio-server-" + Build::getClioVersionString()); + res.set(http::field::server, "clio-server-" + util::build::getClioVersionString()); res.set(http::field::content_type, content_type); res.keep_alive(req_.keep_alive()); res.body() = std::move(message); diff --git a/src/web/impl/ServerSslContext.cpp b/src/web/impl/ServerSslContext.cpp new file mode 100644 index 000000000..2a3127fb8 --- /dev/null +++ b/src/web/impl/ServerSslContext.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/impl/ServerSslContext.hpp" + +#include +#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/impl/ServerSslContext.hpp b/src/web/impl/ServerSslContext.hpp new file mode 100644 index 000000000..06698f732 --- /dev/null +++ b/src/web/impl/ServerSslContext.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 + +#include +#include + +namespace web::impl { + +std::expected +makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath); + +} // namespace web::impl diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index c8ce74573..afaefd702 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -8,4 +8,4 @@ target_sources( include(deps/gtest) target_include_directories(clio_testing_common PUBLIC .) -target_link_libraries(clio_testing_common PUBLIC clio gtest::gtest) +target_link_libraries(clio_testing_common PUBLIC clio_app gtest::gtest) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index c9e471c14..a2dea8dd4 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources( clio_tests PRIVATE # Common ConfigTests.cpp + app/CliArgsTests.cpp data/AmendmentCenterTests.cpp data/BackendCountersTests.cpp data/BackendInterfaceTests.cpp @@ -125,12 +126,19 @@ target_sources( util/TxUtilTests.cpp # Webserver web/AdminVerificationTests.cpp + web/impl/ServerSslContextTests.cpp web/RPCServerHandlerTests.cpp web/ServerTests.cpp web/SweepHandlerTests.cpp web/WhitelistHandlerTests.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/app/CliArgsTests.cpp b/tests/unit/app/CliArgsTests.cpp new file mode 100644 index 000000000..cf3b1dc31 --- /dev/null +++ b/tests/unit/app/CliArgsTests.cpp @@ -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. +*/ +//============================================================================== + +#include "app/CliArgs.hpp" + +#include +#include + +#include +#include +#include + +using namespace app; + +struct CliArgsTests : testing::Test { + testing::StrictMock> onRunMock; + testing::StrictMock> onExitMock; +}; + +TEST_F(CliArgsTests, Parse_NoArgs) +{ + std::array argv{"clio_server"}; + auto const action = CliArgs::parse(argv.size(), argv.data()); + + int const returnCode = 123; + EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) { + EXPECT_EQ(run.configPath, CliArgs::defaultConfigPath); + return returnCode; + }); + EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode); +} + +TEST_F(CliArgsTests, Parse_VersionHelp) +{ + for (auto& argv : + {std::array{"clio_server", "--version"}, + std::array{"clio_server", "-v"}, + std::array{"clio_server", "--help"}, + std::array{"clio_server", "-h"}}) { + auto const action = CliArgs::parse(argv.size(), const_cast(argv.data())); + + EXPECT_CALL(onExitMock, Call).WillOnce([](CliArgs::Action::Exit const& exit) { return exit.exitCode; }); + EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), EXIT_SUCCESS); + } +} + +TEST_F(CliArgsTests, Parse_Config) +{ + std::string_view configPath = "some_config_path"; + std::array argv{"clio_server", "--conf", configPath.data()}; + + auto const action = CliArgs::parse(argv.size(), argv.data()); + + int const returnCode = 123; + EXPECT_CALL(onRunMock, Call).WillOnce([&configPath](CliArgs::Action::Run const& run) { + EXPECT_EQ(run.configPath, configPath); + return returnCode; + }); + EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode); +} diff --git a/tests/unit/test_data/cert.pem b/tests/unit/test_data/cert.pem new file mode 100644 index 000000000..7ef61709e --- /dev/null +++ b/tests/unit/test_data/cert.pem @@ -0,0 +1,22 @@ +-----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 new file mode 100644 index 000000000..ff714e736 --- /dev/null +++ b/tests/unit/test_data/key.pem @@ -0,0 +1,27 @@ +-----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/util/requests/SslContextTests.cpp b/tests/unit/util/requests/SslContextTests.cpp index 0d48385bc..6f7cc177f 100644 --- a/tests/unit/util/requests/SslContextTests.cpp +++ b/tests/unit/util/requests/SslContextTests.cpp @@ -25,6 +25,6 @@ using namespace util::requests::impl; TEST(SslContext, Create) { - auto ctx = makeSslContext(); + auto ctx = makeClientSslContext(); EXPECT_TRUE(ctx); } diff --git a/tests/unit/web/ServerTests.cpp b/tests/unit/web/ServerTests.cpp index 6192da64f..44d7d3e1a 100644 --- a/tests/unit/web/ServerTests.cpp +++ b/tests/unit/web/ServerTests.cpp @@ -40,13 +40,13 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -60,12 +60,11 @@ using namespace util; using namespace web::impl; using namespace web; -using namespace boost::json; -std::string +boost::json::value generateJSONWithDynamicPort(std::string_view port) { - return fmt::format( + return boost::json::parse(fmt::format( R"JSON({{ "server": {{ "ip": "0.0.0.0", @@ -80,13 +79,13 @@ generateJSONWithDynamicPort(std::string_view port) }} }})JSON", port - ); + )); } -std::string +boost::json::value generateJSONDataOverload(std::string_view port) { - return fmt::format( + return boost::json::parse(fmt::format( R"JSON({{ "server": {{ "ip": "0.0.0.0", @@ -100,71 +99,18 @@ generateJSONDataOverload(std::string_view port) }} }})JSON", port - ); + )); } -// for testing, we use a self-signed certificate -std::optional -parseCertsForTest() +boost::json::value +addSslConfig(boost::json::value config) { - std::string const 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-----)"; - std::string const 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-----)"; - ssl::context ctx{ssl::context::tlsv12}; - ctx.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2); - ctx.use_certificate_chain(boost::asio::buffer(cert.data(), cert.size())); - ctx.use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context::file_format::pem); - return ctx; + config.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH; + config.as_object()["ssl_cert_file"] = TEST_DATA_SSL_CERT_PATH; + return config; } -class WebServerTest : public NoLoggerFixture { -public: +struct WebServerTest : NoLoggerFixture { ~WebServerTest() override { work.reset(); @@ -173,7 +119,6 @@ class WebServerTest : public NoLoggerFixture { runner->join(); } -protected: WebServerTest() { work.emplace(ctx); // make sure ctx does not stop on its own @@ -183,12 +128,12 @@ class WebServerTest : public NoLoggerFixture { // this ctx is for dos timer boost::asio::io_context ctxSync; std::string const port = std::to_string(tests::util::generateFreePort()); - Config cfg{parse(generateJSONWithDynamicPort(port))}; + Config cfg{generateJSONWithDynamicPort(port)}; IntervalSweepHandler sweepHandler = web::IntervalSweepHandler{cfg, ctxSync}; WhitelistHandler whitelistHandler = web::WhitelistHandler{cfg}; DOSGuard dosGuard = web::DOSGuard{cfg, whitelistHandler, sweepHandler}; - Config cfgOverload{parse(generateJSONDataOverload(port))}; + Config cfgOverload{generateJSONDataOverload(port)}; IntervalSweepHandler sweepHandlerOverload = web::IntervalSweepHandler{cfgOverload, ctxSync}; WhitelistHandler whitelistHandlerOverload = web::WhitelistHandler{cfgOverload}; DOSGuard dosGuardOverload = web::DOSGuard{cfgOverload, whitelistHandlerOverload, sweepHandlerOverload}; @@ -235,7 +180,6 @@ std::shared_ptr> makeServerSync( util::Config const& config, boost::asio::io_context& ioc, - std::optional> const& sslCtx, web::DOSGuard& dosGuard, std::shared_ptr const& handler ) @@ -245,7 +189,7 @@ makeServerSync( std::condition_variable cv; bool ready = false; boost::asio::dispatch(ioc.get_executor(), [&]() mutable { - server = web::make_HttpServer(config, ioc, sslCtx, dosGuard, handler); + server = web::make_HttpServer(config, ioc, dosGuard, handler); { std::lock_guard const lk(m); ready = true; @@ -264,7 +208,7 @@ makeServerSync( TEST_F(WebServerTest, Http) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuard, e); + auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const res = HttpSyncClient::syncPost("localhost", port, R"({"Hello":1})"); EXPECT_EQ(res, R"({"Hello":1})"); } @@ -272,7 +216,7 @@ TEST_F(WebServerTest, Http) TEST_F(WebServerTest, Ws) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuard, e); + auto const server = makeServerSync(cfg, ctx, dosGuard, e); WebSocketSyncClient wsClient; wsClient.connect("localhost", port); auto const res = wsClient.syncPost(R"({"Hello":1})"); @@ -283,7 +227,7 @@ TEST_F(WebServerTest, Ws) TEST_F(WebServerTest, HttpInternalError) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuard, e); + auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const res = HttpSyncClient::syncPost("localhost", port, R"({})"); EXPECT_EQ( res, @@ -294,7 +238,7 @@ TEST_F(WebServerTest, HttpInternalError) TEST_F(WebServerTest, WsInternalError) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuard, e); + auto const server = makeServerSync(cfg, ctx, dosGuard, e); WebSocketSyncClient wsClient; wsClient.connect("localhost", port); auto const res = wsClient.syncPost(R"({"id":"id1"})"); @@ -308,7 +252,7 @@ TEST_F(WebServerTest, WsInternalError) TEST_F(WebServerTest, WsInternalErrorNotJson) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuard, e); + auto const server = makeServerSync(cfg, ctx, dosGuard, e); WebSocketSyncClient wsClient; wsClient.connect("localhost", port); auto const res = wsClient.syncPost("not json"); @@ -319,12 +263,34 @@ TEST_F(WebServerTest, WsInternalErrorNotJson) ); } +TEST_F(WebServerTest, IncompleteSslConfig) +{ + auto e = std::make_shared(); + + auto jsonConfig = generateJSONWithDynamicPort(port); + jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH; + + auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e); + EXPECT_EQ(server, nullptr); +} + +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_cert_file"] = "wrong_path"; + + auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e); + EXPECT_EQ(server, nullptr); +} + TEST_F(WebServerTest, Https) { auto e = std::make_shared(); - auto sslCtx = parseCertsForTest(); - auto const ctxSslRef = sslCtx ? std::optional>{sslCtx.value()} : std::nullopt; - auto const server = makeServerSync(cfg, ctx, ctxSslRef, dosGuard, e); + cfg = Config{addSslConfig(generateJSONWithDynamicPort(port))}; + auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const res = HttpsSyncClient::syncPost("localhost", port, R"({"Hello":1})"); EXPECT_EQ(res, R"({"Hello":1})"); } @@ -332,10 +298,8 @@ TEST_F(WebServerTest, Https) TEST_F(WebServerTest, Wss) { auto e = std::make_shared(); - auto sslCtx = parseCertsForTest(); - auto const ctxSslRef = sslCtx ? std::optional>{sslCtx.value()} : std::nullopt; - - auto server = makeServerSync(cfg, ctx, ctxSslRef, dosGuard, e); + cfg = Config{addSslConfig(generateJSONWithDynamicPort(port))}; + auto server = makeServerSync(cfg, ctx, dosGuard, e); WebServerSslSyncClient wsClient; wsClient.connect("localhost", port); auto const res = wsClient.syncPost(R"({"Hello":1})"); @@ -346,7 +310,7 @@ TEST_F(WebServerTest, Wss) TEST_F(WebServerTest, HttpRequestOverload) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuardOverload, e); + auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e); auto res = HttpSyncClient::syncPost("localhost", port, R"({})"); EXPECT_EQ(res, "{}"); res = HttpSyncClient::syncPost("localhost", port, R"({})"); @@ -359,7 +323,7 @@ TEST_F(WebServerTest, HttpRequestOverload) TEST_F(WebServerTest, WsRequestOverload) { auto e = std::make_shared(); - auto const server = makeServerSync(cfg, ctx, std::nullopt, dosGuardOverload, e); + auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e); WebSocketSyncClient wsClient; wsClient.connect("localhost", port); auto res = wsClient.syncPost(R"({})"); @@ -379,7 +343,7 @@ TEST_F(WebServerTest, HttpPayloadOverload) { std::string const s100(100, 'a'); auto e = std::make_shared(); - auto server = makeServerSync(cfg, ctx, std::nullopt, dosGuardOverload, e); + auto server = makeServerSync(cfg, ctx, dosGuardOverload, e); auto const res = HttpSyncClient::syncPost("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100)); EXPECT_EQ( res, @@ -391,7 +355,7 @@ TEST_F(WebServerTest, WsPayloadOverload) { std::string const s100(100, 'a'); auto e = std::make_shared(); - auto server = makeServerSync(cfg, ctx, std::nullopt, dosGuardOverload, e); + auto server = makeServerSync(cfg, ctx, dosGuardOverload, e); WebSocketSyncClient wsClient; wsClient.connect("localhost", port); auto const res = wsClient.syncPost(fmt::format(R"({{"payload":"{}"}})", s100)); @@ -405,7 +369,7 @@ TEST_F(WebServerTest, WsPayloadOverload) TEST_F(WebServerTest, WsTooManyConnection) { auto e = std::make_shared(); - auto server = makeServerSync(cfg, ctx, std::nullopt, dosGuardOverload, e); + auto server = makeServerSync(cfg, ctx, dosGuardOverload, e); // max connection is 2, exception should happen when the third connection is made WebSocketSyncClient wsClient1; wsClient1.connect("localhost", port); @@ -485,7 +449,7 @@ JSONServerConfigWithNoSpecifiedAdmin(uint32_t const port) } // get this value from online sha256 generator -static auto constexpr SecertSha256 = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b"; +static auto constexpr SecretSha256 = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b"; class AdminCheckExecutor { public: @@ -513,8 +477,8 @@ class WebServerAdminTest : public WebServerTest, public ::testing::WithParamInte TEST_P(WebServerAdminTest, WsAdminCheck) { auto e = std::make_shared(); - Config const serverConfig{parse(GetParam().config)}; - auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuardOverload, e); + Config const serverConfig{boost::json::parse(GetParam().config)}; + auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e); WebSocketSyncClient wsClient; uint32_t const webServerPort = serverConfig.value("server.port"); wsClient.connect("localhost", std::to_string(webServerPort), GetParam().headers); @@ -527,8 +491,8 @@ TEST_P(WebServerAdminTest, WsAdminCheck) TEST_P(WebServerAdminTest, HttpAdminCheck) { auto e = std::make_shared(); - Config const serverConfig{parse(GetParam().config)}; - auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuardOverload, e); + Config const serverConfig{boost::json::parse(GetParam().config)}; + 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); @@ -556,27 +520,27 @@ INSTANTIATE_TEST_CASE_P( }, WebServerAdminTestParams{ .config = JSONServerConfigWithAdminPassword(tests::util::generateFreePort()), - .headers = {WebHeader(http::field::authorization, SecertSha256)}, + .headers = {WebHeader(http::field::authorization, SecretSha256)}, .expectedResponse = "user" }, WebServerAdminTestParams{ .config = JSONServerConfigWithAdminPassword(tests::util::generateFreePort()), .headers = {WebHeader( http::field::authorization, - fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256) + fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecretSha256) )}, .expectedResponse = "admin" }, WebServerAdminTestParams{ .config = JSONServerConfigWithBothAdminPasswordAndLocalAdminFalse(tests::util::generateFreePort()), - .headers = {WebHeader(http::field::authorization, SecertSha256)}, + .headers = {WebHeader(http::field::authorization, SecretSha256)}, .expectedResponse = "user" }, WebServerAdminTestParams{ .config = JSONServerConfigWithBothAdminPasswordAndLocalAdminFalse(tests::util::generateFreePort()), .headers = {WebHeader( http::field::authorization, - fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256) + fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecretSha256) )}, .expectedResponse = "admin" }, @@ -584,7 +548,7 @@ INSTANTIATE_TEST_CASE_P( .config = JSONServerConfigWithAdminPassword(tests::util::generateFreePort()), .headers = {WebHeader( http::field::authentication_info, - fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256) + fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecretSha256) )}, .expectedResponse = "user" }, @@ -618,8 +582,8 @@ TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminSet) ); auto e = std::make_shared(); - Config const serverConfig{parse(JSONServerConfigWithBothAdminPasswordAndLocalAdmin)}; - EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, std::nullopt, dosGuardOverload, e), std::logic_error); + Config const serverConfig{boost::json::parse(JSONServerConfigWithBothAdminPasswordAndLocalAdmin)}; + EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, dosGuardOverload, e), std::logic_error); } TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse) @@ -637,8 +601,8 @@ TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse) ); auto e = std::make_shared(); - Config const serverConfig{parse(JSONServerConfigWithNoAdminPasswordAndLocalAdminFalse)}; - EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, std::nullopt, dosGuardOverload, e), std::logic_error); + Config const serverConfig{boost::json::parse(JSONServerConfigWithNoAdminPasswordAndLocalAdminFalse)}; + EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, dosGuardOverload, e), std::logic_error); } struct WebServerPrometheusTest : util::prometheus::WithPrometheus, WebServerTest {}; @@ -647,8 +611,8 @@ TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword) { auto e = std::make_shared(); uint32_t const webServerPort = tests::util::generateFreePort(); - Config const serverConfig{parse(JSONServerConfigWithAdminPassword(webServerPort))}; - auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuard, e); + 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"); EXPECT_EQ(res, "Only admin is allowed to collect metrics"); } @@ -669,9 +633,9 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled) ); auto e = std::make_shared(); - Config const serverConfig{parse(JSONServerConfigWithDisabledPrometheus)}; + Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)}; PrometheusService::init(serverConfig); - auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuard, e); + auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto const res = HttpSyncClient::syncGet( "localhost", std::to_string(webServerPort), @@ -679,7 +643,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled) "/metrics", {WebHeader( http::field::authorization, - fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256) + fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecretSha256) )} ); EXPECT_EQ(res, "Prometheus is disabled in clio config"); @@ -691,8 +655,8 @@ TEST_F(WebServerPrometheusTest, validResponse) auto& testCounter = PrometheusService::counterInt("test_counter", util::prometheus::Labels()); ++testCounter; auto e = std::make_shared(); - Config const serverConfig{parse(JSONServerConfigWithAdminPassword(webServerPort))}; - auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuard, e); + 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), @@ -700,7 +664,7 @@ TEST_F(WebServerPrometheusTest, validResponse) "/metrics", {WebHeader( http::field::authorization, - fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecertSha256) + fmt::format("{}{}", PasswordAdminVerificationStrategy::passwordPrefix, SecretSha256) )} ); EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n"); diff --git a/tests/unit/web/impl/ServerSslContextTests.cpp b/tests/unit/web/impl/ServerSslContextTests.cpp new file mode 100644 index 000000000..3febd5dca --- /dev/null +++ b/tests/unit/web/impl/ServerSslContextTests.cpp @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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); +}