Compare commits

..

7 Commits

Author SHA1 Message Date
Ayaz Salikhov
c88ffa7c92 chore: Use LLVM tools 21 (#3051) 2026-04-29 16:34:21 +01:00
Ayaz Salikhov
32aeda0ce7 chore: Fix clang-tidy 21 issues (#3052) 2026-04-29 16:17:32 +01:00
Ayaz Salikhov
f174b47f49 ci: Build image with LLVM tools 21 (#3049) 2026-04-29 14:25:10 +01:00
Ayaz Salikhov
80cdb3234a ci: Upload clang-tidy git diff (#3050) 2026-04-29 13:28:46 +01:00
Sergey Kuznetsov
f1460de5d3 feat: Optional log rotation (#3016)
This PR adds an option to disable log rotation.
2026-04-27 15:30:53 +01:00
github-actions[bot]
198773f86a style: clang-tidy auto fixes (#3047)
Fixes #3046. Please review and commit clang-tidy fixes.

Co-authored-by: godexsoft <385326+godexsoft@users.noreply.github.com>
2026-04-27 13:02:05 +01:00
Sergey Kuznetsov
fe0bf736fb refactor: Use error code in make_address() calls (#3044)
Function `ip::make_address()` throws an exception on an invalid IP.
Refactor to a better error handling without exceptions.
2026-04-27 11:32:07 +01:00
21 changed files with 454 additions and 97 deletions

View File

@@ -20,7 +20,7 @@ concurrency:
env: env:
CONAN_PROFILE: clang CONAN_PROFILE: clang
LLVM_TOOLS_VERSION: 20 LLVM_TOOLS_VERSION: 21
defaults: defaults:
run: run:
@@ -31,7 +31,7 @@ jobs:
if: github.event_name != 'push' || contains(github.event.head_commit.message, 'clang-tidy auto fixes') if: github.event_name != 'push' || contains(github.event.head_commit.message, 'clang-tidy auto fixes')
runs-on: heavy runs-on: heavy
container: container:
image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696 image: ghcr.io/xrplf/clio-ci:f174b47f4909ae41b80406d836ab52adc39eacc6
permissions: permissions:
contents: write contents: write
@@ -84,6 +84,19 @@ jobs:
pre-commit run --all-files fix-local-includes || true pre-commit run --all-files fix-local-includes || true
pre-commit run --all-files clang-format || true pre-commit run --all-files clang-format || true
- name: Generate git diff
if: ${{ steps.files_changed.outcome != 'success' }}
run: |
git diff | tee clang-tidy-git-diff.txt
- name: Upload clang-tidy diff output
if: ${{ steps.files_changed.outcome != 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: clang-tidy-git-diff
path: clang-tidy-git-diff.txt
retention-days: 30
- name: Create an issue - name: Create an issue
if: ${{ (steps.clang_tidy.outcome != 'success' || steps.files_changed.outcome != 'success') && github.event_name != 'pull_request' }} if: ${{ (steps.clang_tidy.outcome != 'success' || steps.files_changed.outcome != 'success') && github.event_name != 'pull_request' }}
id: create_issue id: create_issue

View File

@@ -11,6 +11,7 @@
#include <barrier> #include <barrier>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -24,9 +25,15 @@ static constexpr auto kLOG_FORMAT = "%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v";
struct BenchmarkLoggingInitializer { struct BenchmarkLoggingInitializer {
[[nodiscard]] static std::shared_ptr<spdlog::sinks::sink> [[nodiscard]] static std::shared_ptr<spdlog::sinks::sink>
createFileSink(LogService::FileLoggingParams const& params) createFileSink(std::string const& logDir, uint32_t sizeMB, uint32_t maxFiles)
{ {
return LogService::createFileSink(params, kLOG_FORMAT); return LogService::createFileSink(
LogService::FileLoggingParams{
.logDir = logDir,
.rotation = LogService::RotationParams{.sizeMB = sizeMB, .maxFiles = maxFiles},
},
kLOG_FORMAT
);
} }
static Logger static Logger
@@ -68,11 +75,7 @@ benchmarkConcurrentFileLogging(benchmark::State& state)
static constexpr size_t kTHREAD_COUNT = 1; static constexpr size_t kTHREAD_COUNT = 1;
spdlog::init_thread_pool(kQUEUE_SIZE, kTHREAD_COUNT); spdlog::init_thread_pool(kQUEUE_SIZE, kTHREAD_COUNT);
auto fileSink = BenchmarkLoggingInitializer::createFileSink({ auto fileSink = BenchmarkLoggingInitializer::createFileSink(logDir, 5, 25);
.logDir = logDir,
.rotationSizeMB = 5,
.dirMaxFiles = 25,
});
std::vector<std::thread> threads; std::vector<std::thread> threads;
threads.reserve(numThreads); threads.reserve(numThreads);

View File

@@ -61,7 +61,7 @@ RUN pip install -q --no-cache-dir \
pre-commit pre-commit
# Install LLVM tools # Install LLVM tools
ARG LLVM_TOOLS_VERSION=20 ARG LLVM_TOOLS_VERSION=21
RUN echo "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-${LLVM_TOOLS_VERSION} main" >> /etc/apt/sources.list \ RUN echo "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-${LLVM_TOOLS_VERSION} main" >> /etc/apt/sources.list \
&& wget --progress=dot:giga -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && wget --progress=dot:giga -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -

View File

@@ -14,6 +14,7 @@ The image is based on Ubuntu 20.04 and contains:
- GDB 17.1 - GDB 17.1
- gh 2.83.2 - gh 2.83.2
- git-cliff 2.11.0 - git-cliff 2.11.0
- LLVM Tools 21
- mold 2.40.4 - mold 2.40.4
- Ninja 1.13.2 - Ninja 1.13.2
- Python 3.8 - Python 3.8

View File

@@ -230,7 +230,7 @@ This document provides a list of all available Clio configuration properties in
- **Required**: False - **Required**: False
- **Type**: string - **Type**: string
- **Default value**: None - **Default value**: None
- **Constraints**: None - **Constraints**: The value must be a valid IP address.
- **Description**: The list of IP addresses to whitelist for DOS protection. - **Description**: The list of IP addresses to whitelist for DOS protection.
### dos_guard.max_fetches ### dos_guard.max_fetches
@@ -342,7 +342,7 @@ This document provides a list of all available Clio configuration properties in
- **Required**: True - **Required**: True
- **Type**: string - **Type**: string
- **Default value**: None - **Default value**: None
- **Constraints**: None - **Constraints**: The value must be a valid IP address.
- **Description**: List of proxy ip addresses. When Clio receives a request from proxy it will use `Forwarded` value (if any) as client ip. When this option is used together with `server.proxy.tokens` Clio will identify proxy by ip or by token. - **Description**: List of proxy ip addresses. When Clio receives a request from proxy it will use `Forwarded` value (if any) as client ip. When this option is used together with `server.proxy.tokens` Clio will identify proxy by ip or by token.
### server.proxy.tokens.[] ### server.proxy.tokens.[]
@@ -561,6 +561,14 @@ Documentation can be found at: <https://github.com/gabime/spdlog/wiki/Custom-for
- **Constraints**: The minimum value is `1`. The maximum value is `4294967295`. - **Constraints**: The minimum value is `1`. The maximum value is `4294967295`.
- **Description**: The maximum number of log files in the directory. - **Description**: The maximum number of log files in the directory.
### log.rotate
- **Required**: True
- **Type**: boolean
- **Default value**: `True`
- **Constraints**: None
- **Description**: Enables or disables log file rotation. When disabled, a single log file is used without size-based rotation. Useful when rotation is managed externally (e.g., via logrotate).
### log.tag_style ### log.tag_style
- **Required**: True - **Required**: True

View File

@@ -92,9 +92,14 @@ ClioApplication::run(bool const useNgWebServer)
boost::asio::io_context ioc{threads}; boost::asio::io_context ioc{threads};
// Rate limiter, to prevent abuse // Rate limiter, to prevent abuse
auto whitelistHandler = web::dosguard::WhitelistHandler{config_}; auto whitelistHandler = web::dosguard::WhitelistHandler::create(config_);
if (not whitelistHandler.has_value()) {
LOG(util::LogService::fatal()) << whitelistHandler.error();
return EXIT_FAILURE;
}
auto const dosguardWeights = web::dosguard::Weights::make(config_); auto const dosguardWeights = web::dosguard::Weights::make(config_);
auto dosGuard = web::dosguard::DOSGuard{config_, whitelistHandler, dosguardWeights}; auto dosGuard = web::dosguard::DOSGuard{config_, *whitelistHandler, dosguardWeights};
auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard}; auto sweepHandler = web::dosguard::IntervalSweepHandler{config_, ioc, dosGuard};
auto cache = data::LedgerCache{}; auto cache = data::LedgerCache{};
@@ -222,10 +227,15 @@ ClioApplication::run(bool const useNgWebServer)
config_, backend, rpcEngine, etl, dosGuard config_, backend, rpcEngine, etl, dosGuard
); );
auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler, cache); auto const expectedHttpServer = web::makeHttpServer(config_, ioc, dosGuard, handler, cache);
if (not expectedHttpServer.has_value()) {
LOG(util::LogService::fatal()) << expectedHttpServer.error();
return EXIT_FAILURE;
}
appStopper_.setOnStop( appStopper_.setOnStop(
Stopper::makeOnStopCallback( Stopper::makeOnStopCallback(
*httpServer, **expectedHttpServer,
*balancer, *balancer,
*etl, *etl,
*subscriptions, *subscriptions,

View File

@@ -13,7 +13,7 @@ concept TableSpec = requires {
// Check that 'row' exists and is a tuple // Check that 'row' exists and is a tuple
// keys types are at the beginning and the other fields types sort in alphabetical order // keys types are at the beginning and the other fields types sort in alphabetical order
typename T::Row; typename T::Row;
requires std::tuple_size<typename T::Row>::value >= 0; // Ensures 'row' is a tuple requires std::tuple_size_v<typename T::Row> >= 0; // Ensures 'row' is a tuple
// Check that static constexpr members 'partitionKey' and 'tableName' exist // Check that static constexpr members 'partitionKey' and 'tableName' exist
{ T::kPARTITION_KEY } -> std::convertible_to<char const*>; { T::kPARTITION_KEY } -> std::convertible_to<char const*>;

View File

@@ -312,7 +312,8 @@ getClioConfig()
{"num_markers", {"num_markers",
ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateNumMarkers)}, ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateNumMarkers)},
{"dos_guard.whitelist.[]", Array{ConfigValue{ConfigType::String}.optional()}}, {"dos_guard.whitelist.[]",
Array{ConfigValue{ConfigType::String}.optional().withConstraint(gValidateIp)}},
{"dos_guard.max_fetches", {"dos_guard.max_fetches",
ConfigValue{ConfigType::Integer}.defaultValue(1000'000u).withConstraint(gValidateUint32)}, ConfigValue{ConfigType::Integer}.defaultValue(1000'000u).withConstraint(gValidateUint32)},
{"dos_guard.max_connections", {"dos_guard.max_connections",
@@ -361,7 +362,8 @@ getClioConfig()
{"server.ws_max_sending_queue_size", {"server.ws_max_sending_queue_size",
ConfigValue{ConfigType::Integer}.defaultValue(1500).withConstraint(gValidateUint32)}, ConfigValue{ConfigType::Integer}.defaultValue(1500).withConstraint(gValidateUint32)},
{"server.__ng_web_server", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, {"server.__ng_web_server", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}}, {"server.proxy.ips.[]",
Array{ConfigValue{ConfigType::String}.withConstraint(gValidateIp)}},
{"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}}, {"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
{"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)}, {"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
@@ -420,6 +422,8 @@ getClioConfig()
{"log.directory_max_files", {"log.directory_max_files",
ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(gValidateUint32)}, ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(gValidateUint32)},
{"log.rotate", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"log.tag_style", {"log.tag_style",
ConfigValue{ConfigType::String}.defaultValue("none").withConstraint(gValidateLogTag)}, ConfigValue{ConfigType::String}.defaultValue("none").withConstraint(gValidateLogTag)},

View File

@@ -357,6 +357,10 @@ Documentation can be found at: <https://github.com/gabime/spdlog/wiki/Custom-for
"file starts."}, "file starts."},
KV{.key = "log.directory_max_files", KV{.key = "log.directory_max_files",
.value = "The maximum number of log files in the directory."}, .value = "The maximum number of log files in the directory."},
KV{.key = "log.rotate",
.value = "Enables or disables log file rotation. When disabled, a single log file is "
"used without size-based rotation. Useful when rotation is managed externally "
"(e.g., via logrotate)."},
KV{.key = "log.tag_style", KV{.key = "log.tag_style",
.value = "Log tags are unique identifiers for log messages. `uint`/`int` starts logging " .value = "Log tags are unique identifiers for log messages. `uint`/`int` starts logging "
"from 0 and increments, " "from 0 and increments, "

View File

@@ -17,6 +17,7 @@
#include <spdlog/formatter.h> #include <spdlog/formatter.h>
#include <spdlog/logger.h> #include <spdlog/logger.h>
#include <spdlog/pattern_formatter.h> #include <spdlog/pattern_formatter.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/rotating_file_sink.h> #include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
@@ -184,12 +185,18 @@ spdlog::sink_ptr
LogService::createFileSink(FileLoggingParams const& params, std::string const& format) LogService::createFileSink(FileLoggingParams const& params, std::string const& format)
{ {
std::filesystem::path const dirPath(params.logDir); std::filesystem::path const dirPath(params.logDir);
// the below are taken from user in MB, but spdlog needs it to be in bytes auto fileSink = [&]() -> std::shared_ptr<spdlog::sinks::sink> {
auto const rotationSize = mbToBytes(params.rotationSizeMB); auto const logPath = (dirPath / "clio.log").string();
if (params.rotation.has_value()) {
// rotation sizes are taken from user in MB, but spdlog needs bytes
auto const rotationSize = mbToBytes(params.rotation->sizeMB);
return std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
logPath, rotationSize, params.rotation->maxFiles
);
}
return std::make_shared<spdlog::sinks::basic_file_sink_mt>(logPath, /*truncate=*/false);
}();
auto fileSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
(dirPath / "clio.log").string(), rotationSize, params.dirMaxFiles
);
fileSink->set_level(spdlog::level::trace); fileSink->set_level(spdlog::level::trace);
fileSink->set_formatter(std::make_unique<spdlog::pattern_formatter>(format)); fileSink->set_formatter(std::make_unique<spdlog::pattern_formatter>(format));
@@ -336,10 +343,17 @@ LogService::getSinks(config::ClioConfigDefinition const& config)
} }
} }
std::optional<RotationParams> rotation = std::nullopt;
if (config.get<bool>("log.rotate")) {
rotation = RotationParams{
.sizeMB = config.get<uint32_t>("log.rotation_size"),
.maxFiles = config.get<uint32_t>("log.directory_max_files"),
};
}
FileLoggingParams const params{ FileLoggingParams const params{
.logDir = logDir.value(), .logDir = logDir.value(),
.rotationSizeMB = config.get<uint32_t>("log.rotation_size"), .rotation = rotation,
.dirMaxFiles = config.get<uint32_t>("log.directory_max_files"),
}; };
allSinks.push_back(createFileSink(params, format)); allSinks.push_back(createFileSink(params, format));
} }

View File

@@ -28,6 +28,7 @@ class sink; // NOLINT(readability-identifier-naming)
struct BenchmarkLoggingInitializer; struct BenchmarkLoggingInitializer;
class LoggerFixture; class LoggerFixture;
struct LogServiceInitTests; struct LogServiceInitTests;
struct LogFileRotationTests;
namespace util { namespace util {
@@ -229,6 +230,7 @@ class LogServiceState {
protected: protected:
friend struct ::LogServiceInitTests; friend struct ::LogServiceInitTests;
friend class ::LoggerFixture; friend class ::LoggerFixture;
friend struct ::LogFileRotationTests;
friend class Logger; friend class Logger;
friend class ::util::impl::OnAssert; friend class ::util::impl::OnAssert;
@@ -388,11 +390,14 @@ private:
expected<std::vector<std::shared_ptr<spdlog::sinks::sink>>, std::string> expected<std::vector<std::shared_ptr<spdlog::sinks::sink>>, std::string>
getSinks(config::ClioConfigDefinition const& config); getSinks(config::ClioConfigDefinition const& config);
struct RotationParams {
uint32_t sizeMB;
uint32_t maxFiles;
};
struct FileLoggingParams { struct FileLoggingParams {
std::string logDir; std::string logDir;
std::optional<RotationParams> rotation; ///< nullopt when rotation is disabled
uint32_t rotationSizeMB;
uint32_t dirMaxFiles;
}; };
friend struct ::BenchmarkLoggingInitializer; friend struct ::BenchmarkLoggingInitializer;

View File

@@ -375,7 +375,7 @@ using HttpServer = Server<HttpSession, SslHttpSession, HandlerType>;
* @return The server instance * @return The server instance
*/ */
template <typename HandlerType> template <typename HandlerType>
static std::shared_ptr<HttpServer<HandlerType>> static std::expected<std::shared_ptr<HttpServer<HandlerType>>, std::string>
makeHttpServer( makeHttpServer(
util::config::ClioConfigDefinition const& config, util::config::ClioConfigDefinition const& config,
boost::asio::io_context& ioc, boost::asio::io_context& ioc,
@@ -388,19 +388,24 @@ makeHttpServer(
auto expectedSslContext = ng::impl::makeServerSslContext(config); auto expectedSslContext = ng::impl::makeServerSslContext(config);
if (not expectedSslContext) { if (not expectedSslContext) {
LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error(); return std::unexpected(
return nullptr; fmt::format("Failed to create SSL context: {}", expectedSslContext.error())
);
} }
auto const serverConfig = config.getObject("server"); auto const serverConfig = config.getObject("server");
auto const address = boost::asio::ip::make_address(serverConfig.get<std::string>("ip"));
auto const ipFromConfig = serverConfig.get<std::string>("ip");
boost::system::error_code ec;
auto const address = boost::asio::ip::make_address(ipFromConfig, ec);
if (ec.failed())
return std::unexpected(fmt::format("Invalid 'server.ip' config value: {}", ipFromConfig));
auto const port = serverConfig.get<unsigned short>("port"); auto const port = serverConfig.get<unsigned short>("port");
auto expectedAdminVerification = makeAdminVerificationStrategy(config); auto expectedAdminVerification = makeAdminVerificationStrategy(config);
if (not expectedAdminVerification.has_value()) { if (not expectedAdminVerification.has_value())
LOG(log.error()) << expectedAdminVerification.error(); return std::unexpected(expectedAdminVerification.error());
throw std::logic_error{expectedAdminVerification.error()};
}
// If the transactions number is 200 per ledger, A client which subscribes everything will send // 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 // 400+ feeds for each ledger. we allow user delay 3 ledgers by default

View File

@@ -8,32 +8,48 @@
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <regex> #include <regex>
#include <stdexcept>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <utility>
#include <vector> #include <vector>
namespace web::dosguard { namespace web::dosguard {
void WhitelistHandler::WhitelistHandler(Whitelist whitelist) : whitelist_(std::move(whitelist))
{
}
std::expected<void, std::string>
Whitelist::add(std::string_view net) Whitelist::add(std::string_view net)
{ {
using namespace boost::asio; using namespace boost::asio;
if (not isMask(net)) { if (not isMask(net)) {
ips_.push_back(ip::make_address(net)); boost::system::error_code ec;
return; auto const ip = ip::make_address(net, ec);
if (ec.failed())
return std::unexpected{fmt::format("Malformed whitelist ip address: {}. ", net)};
ips_.push_back(ip);
return {};
} }
if (isV4(net)) { if (isV4(net)) {
subnetsV4_.push_back(ip::make_network_v4(net)); boost::system::error_code ec;
auto const net4 = ip::make_network_v4(net, ec);
if (ec.failed())
return std::unexpected{fmt::format("Malformed network: {}. ", net)};
subnetsV4_.push_back(net4);
} else if (isV6(net)) { } else if (isV6(net)) {
subnetsV6_.push_back(ip::make_network_v6(net)); boost::system::error_code ec;
auto const net6 = ip::make_network_v6(net, ec);
if (ec.failed())
return std::unexpected{fmt::format("Malformed network: {}. ", net)};
subnetsV6_.push_back(net6);
} else { } else {
throw std::runtime_error(fmt::format("malformed network: {}", net.data())); return std::unexpected{fmt::format("Malformed network: {}. ", net)};
} }
return {};
} }
bool bool
@@ -41,7 +57,11 @@ Whitelist::isWhiteListed(std::string_view ip) const
{ {
using namespace boost::asio; using namespace boost::asio;
auto const addr = ip::make_address(ip); boost::system::error_code ec;
auto const addr = ip::make_address(ip, ec);
if (ec.failed())
return false;
if (std::ranges::find(ips_, addr) != std::end(ips_)) if (std::ranges::find(ips_, addr) != std::end(ips_))
return true; return true;
@@ -95,7 +115,7 @@ Whitelist::isV6(std::string_view net)
bool bool
Whitelist::isMask(std::string_view net) Whitelist::isMask(std::string_view net)
{ {
return net.find('/') != std::string_view::npos; return net.contains('/');
} }
} // namespace web::dosguard } // namespace web::dosguard

View File

@@ -14,11 +14,11 @@
#include <fmt/format.h> #include <fmt/format.h>
#include <algorithm> #include <algorithm>
#include <regex> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <utility>
#include <vector> #include <vector>
namespace web::dosguard { namespace web::dosguard {
@@ -36,16 +36,15 @@ public:
* @brief Add network address to whitelist. * @brief Add network address to whitelist.
* *
* @param net Network part of the ip address * @param net Network part of the ip address
* @throws std::runtime::error when the network address is not valid * @return void on success, or an error string if the address is not valid
*/ */
void std::expected<void, std::string>
add(std::string_view net); add(std::string_view net);
/** /**
* @brief Checks to see if ip address is whitelisted. * @brief Checks to see if ip address is whitelisted.
* *
* @param ip IP address * @param ip IP address
* @throws std::runtime::error when the network address is not valid
* @return true if the given IP is whitelisted; false otherwise * @return true if the given IP is whitelisted; false otherwise
*/ */
bool bool
@@ -76,21 +75,38 @@ class WhitelistHandler : public WhitelistHandlerInterface {
public: public:
/** /**
* @brief Adds all whitelisted IPs and masks from the given config. * @brief Constructs a WhitelistHandler from an already-built Whitelist.
*
* @param whitelist The whitelist to use
*/
explicit WhitelistHandler(Whitelist whitelist);
/**
* @brief Creates a WhitelistHandler by loading all whitelisted IPs and masks from config.
* *
* @param config The Clio config to use * @param config The Clio config to use
* @param resolver The resolver to use for hostname resolution * @param resolver The resolver to use for hostname resolution
* @return The WhitelistHandler on success, or an error string if any whitelist entry is invalid
*/ */
template <SomeResolver HostnameResolverType = Resolver> template <SomeResolver HostnameResolverType = Resolver>
WhitelistHandler( static std::expected<WhitelistHandler, std::string>
util::config::ClioConfigDefinition const& config, create(util::config::ClioConfigDefinition const& config, HostnameResolverType&& resolver = {})
HostnameResolverType&& resolver = {}
)
{ {
std::unordered_set<std::string> const arr = std::unordered_set<std::string> const arr =
getWhitelist(config, std::forward<HostnameResolverType>(resolver)); getWhitelist(config, std::forward<HostnameResolverType>(resolver));
for (auto const& net : arr) Whitelist whitelist;
whitelist_.add(net); std::optional<std::string> errors;
for (auto const& net : arr) {
if (auto result = whitelist.add(net); !result.has_value()) {
if (!errors.has_value())
errors.emplace();
errors->append(std::move(result).error());
}
}
if (errors.has_value()) {
return std::unexpected{std::move(errors).value()};
}
return WhitelistHandler(std::move(whitelist));
} }
/** /**

View File

@@ -120,10 +120,10 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
"book_changes": 2 "book_changes": 2
})JSON"; })JSON";
web::SubscriptionContextPtr const session1 = std::make_shared<MockSession>(); web::SubscriptionContextPtr const session1 = std::make_shared<MockSession>();
MockSession* mockSession1 = dynamic_cast<MockSession*>(session1.get()); auto const* mockSession1 = dynamic_cast<MockSession const*>(session1.get());
web::SubscriptionContextPtr session2 = std::make_shared<MockSession>(); web::SubscriptionContextPtr session2 = std::make_shared<MockSession>();
MockSession* mockSession2 = dynamic_cast<MockSession*>(session2.get()); auto const* mockSession2 = dynamic_cast<MockSession const*>(session2.get());
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> session2OnDisconnectSlots; std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> session2OnDisconnectSlots;
ON_CALL(*mockSession2, onDisconnect).WillByDefault([&session2OnDisconnectSlots](auto slot) { ON_CALL(*mockSession2, onDisconnect).WillByDefault([&session2OnDisconnectSlots](auto slot) {
session2OnDisconnectSlots.push_back(slot); session2OnDisconnectSlots.push_back(slot);

View File

@@ -81,7 +81,9 @@ struct RPCEngineTest : util::prometheus::WithPrometheus,
util::TagDecoratorFactory tagFactory{cfg}; util::TagDecoratorFactory tagFactory{cfg};
WorkQueue queue = WorkQueue::makeWorkQueue(cfg); WorkQueue queue = WorkQueue::makeWorkQueue(cfg);
web::dosguard::WhitelistHandler whitelistHandler{cfg}; web::dosguard::WhitelistHandler whitelistHandler{
web::dosguard::WhitelistHandler::create(cfg).value()
};
web::dosguard::Weights weights{1, {}}; web::dosguard::Weights weights{1, {}};
web::dosguard::DOSGuard dosGuard{cfg, whitelistHandler, weights}; web::dosguard::DOSGuard dosGuard{cfg, whitelistHandler, weights};
std::shared_ptr<MockHandlerProvider> handlerProvider = std::make_shared<MockHandlerProvider>(); std::shared_ptr<MockHandlerProvider> handlerProvider = std::make_shared<MockHandlerProvider>();

View File

@@ -166,8 +166,8 @@ TEST_F(RPCServerInfoHandlerTest, NoFeesErrorsOutWithInternal)
TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent) TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
@@ -209,8 +209,8 @@ TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent)
TEST_F(RPCServerInfoHandlerTest, AmendmentBlockedIsPresentIfSet) TEST_F(RPCServerInfoHandlerTest, AmendmentBlockedIsPresentIfSet)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
@@ -250,8 +250,8 @@ TEST_F(RPCServerInfoHandlerTest, AmendmentBlockedIsPresentIfSet)
TEST_F(RPCServerInfoHandlerTest, CorruptionDetectedIsPresentIfSet) TEST_F(RPCServerInfoHandlerTest, CorruptionDetectedIsPresentIfSet)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
@@ -291,7 +291,7 @@ TEST_F(RPCServerInfoHandlerTest, CorruptionDetectedIsPresentIfSet)
TEST_F(RPCServerInfoHandlerTest, CacheReportsEnabledFlagCorrectly) TEST_F(RPCServerInfoHandlerTest, CacheReportsEnabledFlagCorrectly)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2).WillRepeatedly(Return(ledgerHeader)); EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2).WillRepeatedly(Return(ledgerHeader));
@@ -346,9 +346,9 @@ TEST_F(RPCServerInfoHandlerTest, CacheReportsEnabledFlagCorrectly)
TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet) TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = json::object{}; auto const empty = json::object{};
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
@@ -389,9 +389,9 @@ TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet)
TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam) TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = json::object{}; auto const empty = json::object{};
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
@@ -439,9 +439,9 @@ TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam)
TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent) TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = json::object{}; auto const empty = json::object{};
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old
@@ -493,9 +493,9 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent)
TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown) TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown)
{ {
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters* rawCountersPtr = mockCountersPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService* rawETLServicePtr = mockETLServicePtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();
auto const empty = json::object{}; auto const empty = json::object{};
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30, 3); // 3 seconds old

View File

@@ -66,6 +66,8 @@ protected:
{"log.directory_max_files", {"log.directory_max_files",
ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(config::gValidateUint32)}, ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(config::gValidateUint32)},
{"log.rotate", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")}, {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")},
}; };
@@ -219,3 +221,21 @@ TEST_F(LogServiceInitTests, LogSizeAndHourRotationCannotBeZero)
); );
} }
} }
TEST_F(LogServiceInitTests, RotateDefaultsToTrue)
{
auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::object{}});
ASSERT_FALSE(parsingErrors.has_value());
EXPECT_TRUE(config_.get<bool>("log.rotate"));
}
TEST_F(LogServiceInitTests, RotationDisabledConfigParsesSuccessfully)
{
auto const parsingErrors = config_.parse(
ConfigFileJson{boost::json::object{{"log", boost::json::object{{"rotate", false}}}}}
);
ASSERT_FALSE(parsingErrors.has_value());
EXPECT_FALSE(config_.get<bool>("log.rotate"));
}

View File

@@ -1,14 +1,30 @@
#include "util/LoggerFixtures.hpp" #include "util/LoggerFixtures.hpp"
#include "util/config/Array.hpp"
#include "util/config/ConfigConstraints.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "util/log/Logger.hpp" #include "util/log/Logger.hpp"
#include <boost/json/object.hpp>
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <fmt/core.h>
#include <fmt/format.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <spdlog/logger.h> #include <spdlog/logger.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <cstddef> #include <cstddef>
#include <filesystem>
#include <memory> #include <memory>
#include <string> #include <string>
using namespace util; using namespace util;
using util::config::Array;
using util::config::ConfigFileJson;
using util::config::ConfigType;
using util::config::ConfigValue;
namespace { namespace {
size_t size_t
@@ -91,3 +107,138 @@ TEST_F(LoggerTest, ManyDynamicLoggers)
ASSERT_EQ(loggersNum(), initialLoggers); ASSERT_EQ(loggersNum(), initialLoggers);
} }
/**
* @brief Fixture for testing real log-file rotation behaviour.
*
* Unlike LoggerTest (which uses LoggerFixture's in-memory buffer), this fixture
* initialises the LogService with a real file sink and redirects all spdlog
* loggers to that sink so that written messages actually land on disk.
*/
struct LogFileRotationTests : ::testing::Test {
std::filesystem::path const tmpDir = std::filesystem::temp_directory_path() /
fmt::format("clio_log_rotation_tests_{}",
boost::uuids::to_string(boost::uuids::random_generator{}()));
util::config::ClioConfigDefinition config{
{"log.channels.[].channel", Array{ConfigValue{ConfigType::String}}},
{"log.channels.[].level", Array{ConfigValue{ConfigType::String}}},
{"log.level", ConfigValue{ConfigType::String}.defaultValue("info")},
{"log.format",
ConfigValue{ConfigType::String}.defaultValue(R"(%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v)")},
{"log.is_async", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"log.enable_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"log.directory", ConfigValue{ConfigType::String}.optional()},
{"log.rotation_size",
ConfigValue{ConfigType::Integer}.defaultValue(2048).withConstraint(
util::config::gValidateUint32
)},
{"log.directory_max_files",
ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(
util::config::gValidateUint32
)},
{"log.rotate", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")},
};
LogFileRotationTests()
{
std::filesystem::remove_all(tmpDir);
if (LogServiceState::initialized())
LogServiceState::reset();
}
~LogFileRotationTests() override
{
if (LogService::initialized())
LogService::reset();
// Leave state initialised so that subsequent tests can call reset().
LogServiceState::init(false, Severity::FTL, {});
std::filesystem::remove_all(tmpDir);
}
/**
* @brief Initialises LogService with the current config and redirects all
* existing spdlog loggers to the newly created file sink.
*
* LogService::init() skips updating sinks on loggers that already exist in
* the spdlog registry. Calling replaceSinks() here ensures every logger
* writes to the file sink regardless of prior test state.
*/
void
initFileLogging() const
{
ASSERT_TRUE(LogService::init(config));
LogServiceState::replaceSinks(LogServiceState::sinks_);
}
/** @brief Returns the number of regular files in tmpDir_. */
[[nodiscard]] std::size_t
countLogFiles() const
{
std::size_t count = 0;
for (auto const& entry : std::filesystem::directory_iterator(tmpDir)) {
if (entry.is_regular_file())
++count;
}
return count;
}
};
TEST_F(LogFileRotationTests, RotationDisabledProducesSingleLogFile)
{
auto const parsingErrors = config.parse(
ConfigFileJson{boost::json::object{
{"log",
boost::json::object{
{"directory", tmpDir.string()},
{"rotate", false},
}}
}}
);
ASSERT_FALSE(parsingErrors.has_value());
initFileLogging();
// Write enough data to trigger rotation if it were enabled (> 1 MB).
// Writing at error level flushes immediately because flush_on(err) is set.
Logger const log{"General"};
std::string const bigMessage(1000, 'x');
for (int i = 0; i < 1100; ++i)
log.error() << bigMessage;
EXPECT_EQ(countLogFiles(), 1u);
}
TEST_F(LogFileRotationTests, RotationEnabledProducesMultipleLogFiles)
{
auto const parsingErrors = config.parse(
ConfigFileJson{boost::json::object{
{"log",
boost::json::object{
{"directory", tmpDir.string()},
{"rotate", true},
{"rotation_size", 1},
{"directory_max_files", 2},
}}
}}
);
ASSERT_FALSE(parsingErrors.has_value());
initFileLogging();
Logger const log{"General"};
std::string const bigMessage(1000, 'x');
for (int i = 0; i < 1100; ++i)
log.error() << bigMessage;
EXPECT_GT(countLogFiles(), 1u);
}

View File

@@ -150,13 +150,15 @@ struct WebServerTest : public virtual ::testing::Test {
boost::asio::io_context ctxSync; boost::asio::io_context ctxSync;
std::string const port = std::to_string(tests::util::generateFreePort()); std::string const port = std::to_string(tests::util::generateFreePort());
ClioConfigDefinition cfg{getParseServerConfig(generateJSONWithDynamicPort(port))}; ClioConfigDefinition cfg{getParseServerConfig(generateJSONWithDynamicPort(port))};
dosguard::WhitelistHandler whitelistHandler{cfg}; dosguard::WhitelistHandler whitelistHandler{dosguard::WhitelistHandler::create(cfg).value()};
dosguard::Weights dosguardWeights{1, {}}; dosguard::Weights dosguardWeights{1, {}};
dosguard::DOSGuard dosGuard{cfg, whitelistHandler, dosguardWeights}; dosguard::DOSGuard dosGuard{cfg, whitelistHandler, dosguardWeights};
dosguard::IntervalSweepHandler sweepHandler{cfg, ctxSync, dosGuard}; dosguard::IntervalSweepHandler sweepHandler{cfg, ctxSync, dosGuard};
ClioConfigDefinition cfgOverload{getParseServerConfig(generateJSONDataOverload(port))}; ClioConfigDefinition cfgOverload{getParseServerConfig(generateJSONDataOverload(port))};
dosguard::WhitelistHandler whitelistHandlerOverload{cfgOverload}; dosguard::WhitelistHandler whitelistHandlerOverload{
dosguard::WhitelistHandler::create(cfgOverload).value()
};
dosguard::DOSGuard dosGuardOverload{cfgOverload, whitelistHandlerOverload, dosguardWeights}; dosguard::DOSGuard dosGuardOverload{cfgOverload, whitelistHandlerOverload, dosguardWeights};
dosguard::IntervalSweepHandler sweepHandlerOverload{cfgOverload, ctxSync, dosGuardOverload}; dosguard::IntervalSweepHandler sweepHandlerOverload{cfgOverload, ctxSync, dosGuardOverload};
// this ctx is for http server // this ctx is for http server
@@ -216,11 +218,26 @@ makeServerSync(
std::reference_wrapper<data::LedgerCacheInterface const> cache std::reference_wrapper<data::LedgerCacheInterface const> cache
) )
{ {
return web::makeHttpServer(config, ioc, dosGuard, handler, cache); auto result = web::makeHttpServer(config, ioc, dosGuard, handler, cache);
[&]() { ASSERT_TRUE(result.has_value()); }();
return std::move(result).value();
} }
} // namespace } // namespace
TEST_F(WebServerTest, InvalidIpAddress)
{
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["server"].as_object()["ip"] = "not-an-ip";
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
auto const result =
web::makeHttpServer(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(result.error(), testing::HasSubstr("Invalid 'server.ip' config value"));
}
TEST_F(WebServerTest, Http) TEST_F(WebServerTest, Http)
{ {
auto cache = MockLedgerCache(); auto cache = MockLedgerCache();
@@ -294,8 +311,9 @@ TEST_F(WebServerTest, IncompleteSslConfig)
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path; jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
auto cache = MockLedgerCache(); auto cache = MockLedgerCache();
auto const server = makeServerSync(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache); auto const result =
EXPECT_EQ(server, nullptr); web::makeHttpServer(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
EXPECT_FALSE(result.has_value());
} }
TEST_F(WebServerTest, WrongSslConfig) TEST_F(WebServerTest, WrongSslConfig)
@@ -307,8 +325,9 @@ TEST_F(WebServerTest, WrongSslConfig)
jsonConfig.as_object()["ssl_cert_file"] = "wrong_path"; jsonConfig.as_object()["ssl_cert_file"] = "wrong_path";
auto cache = MockLedgerCache(); auto cache = MockLedgerCache();
auto const server = makeServerSync(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache); auto const result =
EXPECT_EQ(server, nullptr); web::makeHttpServer(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
EXPECT_FALSE(result.has_value());
} }
TEST_F(WebServerTest, Https) TEST_F(WebServerTest, Https)
@@ -694,9 +713,8 @@ TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminSet)
)}; )};
MockLedgerCache cache; MockLedgerCache cache;
EXPECT_THROW( auto const result = web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache);
web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache), std::logic_error EXPECT_FALSE(result.has_value());
);
} }
TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse) TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse)
@@ -719,9 +737,8 @@ TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse)
)}; )};
MockLedgerCache cache; MockLedgerCache cache;
EXPECT_THROW( auto const result = web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache);
web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache), std::logic_error EXPECT_FALSE(result.has_value());
);
} }
struct WebServerPrometheusTest : util::prometheus::WithPrometheus, WebServerTest {}; struct WebServerPrometheusTest : util::prometheus::WithPrometheus, WebServerTest {};

View File

@@ -11,6 +11,7 @@
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <expected>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@@ -62,7 +63,9 @@ TEST_F(WhitelistHandlerTest, TestWhiteListIPV4)
ClioConfigDefinition const cfg{ ClioConfigDefinition const cfg{
getParseWhitelistHandlerConfig(boost::json::parse(kJSON_DATA_IP_V4)) getParseWhitelistHandlerConfig(boost::json::parse(kJSON_DATA_IP_V4))
}; };
WhitelistHandler const whitelistHandler{cfg, mockResolver}; auto const result = WhitelistHandler::create(cfg, mockResolver);
ASSERT_TRUE(result.has_value());
auto const& whitelistHandler = *result;
EXPECT_TRUE(whitelistHandler.isWhiteListed("192.168.1.10")); EXPECT_TRUE(whitelistHandler.isWhiteListed("192.168.1.10"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("193.168.0.123")); EXPECT_FALSE(whitelistHandler.isWhiteListed("193.168.0.123"));
@@ -86,7 +89,9 @@ TEST_F(WhitelistHandlerTest, TestWhiteListResolvesHostname)
ClioConfigDefinition const cfg{ ClioConfigDefinition const cfg{
getParseWhitelistHandlerConfig(boost::json::parse(kJSON_DATA_IP_V4)) getParseWhitelistHandlerConfig(boost::json::parse(kJSON_DATA_IP_V4))
}; };
WhitelistHandler const whitelistHandler{cfg}; auto const result = WhitelistHandler::create(cfg);
ASSERT_TRUE(result.has_value());
auto const& whitelistHandler = *result;
EXPECT_TRUE(whitelistHandler.isWhiteListed("127.0.0.1")); EXPECT_TRUE(whitelistHandler.isWhiteListed("127.0.0.1"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("193.168.0.123")); EXPECT_FALSE(whitelistHandler.isWhiteListed("193.168.0.123"));
@@ -110,10 +115,69 @@ TEST_F(WhitelistHandlerTest, TestWhiteListIPV6)
ClioConfigDefinition const cfg{ ClioConfigDefinition const cfg{
getParseWhitelistHandlerConfig(boost::json::parse(kJSON_DATA_IP_V6)) getParseWhitelistHandlerConfig(boost::json::parse(kJSON_DATA_IP_V6))
}; };
WhitelistHandler const whitelistHandler{cfg}; auto const result = WhitelistHandler::create(cfg);
ASSERT_TRUE(result.has_value());
auto const& whitelistHandler = *result;
EXPECT_TRUE(whitelistHandler.isWhiteListed("2002:1dd8:85a7:0000:0000:8a6e:0000:1111")); EXPECT_TRUE(whitelistHandler.isWhiteListed("2002:1dd8:85a7:0000:0000:8a6e:0000:1111"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("2002:1dd8:85a7:1101:0000:8a6e:0000:1111")); EXPECT_FALSE(whitelistHandler.isWhiteListed("2002:1dd8:85a7:1101:0000:8a6e:0000:1111"));
EXPECT_TRUE(whitelistHandler.isWhiteListed("2001:0db8:85a3:0000:0000:8a2e:0000:0000")); EXPECT_TRUE(whitelistHandler.isWhiteListed("2001:0db8:85a3:0000:0000:8a2e:0000:0000"));
EXPECT_TRUE(whitelistHandler.isWhiteListed("2001:0db8:85a3:0000:1111:8a2e:0370:7334")); EXPECT_TRUE(whitelistHandler.isWhiteListed("2001:0db8:85a3:0000:1111:8a2e:0370:7334"));
} }
struct WhitelistTest : public virtual ::testing::Test {};
TEST_F(WhitelistTest, AddValidIPV4)
{
Whitelist whitelist;
EXPECT_TRUE(whitelist.add("1.2.3.4").has_value());
}
TEST_F(WhitelistTest, AddInvalidIP)
{
Whitelist whitelist;
auto const result = whitelist.add("not-an-ip");
ASSERT_FALSE(result.has_value());
EXPECT_THAT(result.error(), testing::HasSubstr("not-an-ip"));
}
TEST_F(WhitelistTest, AddInvalidNetwork)
{
Whitelist whitelist;
auto const result = whitelist.add("not-a-net/24");
ASSERT_FALSE(result.has_value());
EXPECT_THAT(result.error(), testing::HasSubstr("not-a-net/24"));
}
TEST_F(WhitelistTest, IsWhiteListedWithInvalidIP)
{
Whitelist const whitelist;
EXPECT_FALSE(whitelist.isWhiteListed("not-an-ip"));
}
TEST_F(WhitelistHandlerTest, CreateWithInvalidIPFails)
{
struct MockResolver {
MOCK_METHOD(std::vector<std::string>, resolve, (std::string_view, std::string_view));
MOCK_METHOD(std::vector<std::string>, resolve, (std::string_view));
};
static constexpr auto kJSON = R"JSON(
{
"dos_guard": {
"whitelist": ["not-an-ip"]
}
}
)JSON";
testing::StrictMock<MockResolver> mockResolver;
EXPECT_CALL(mockResolver, resolve(testing::_))
.WillOnce([](auto hostname) -> std::vector<std::string> {
return {std::string{hostname}};
});
ClioConfigDefinition const cfg{getParseWhitelistHandlerConfig(boost::json::parse(kJSON))};
auto const result = WhitelistHandler::create(cfg, mockResolver);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(result.error(), testing::HasSubstr("not-an-ip"));
}