mirror of
https://github.com/XRPLF/clio.git
synced 2026-07-09 08:10:25 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159f79d085 | ||
|
|
5cd1e1115b | ||
|
|
bcc0345c35 |
20
.github/workflows/reusable-test.yml
vendored
20
.github/workflows/reusable-test.yml
vendored
@@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
services:
|
||||
scylladb:
|
||||
image: ${{ inputs.container != '' && 'scylladb/scylla' || '' }}
|
||||
image: ${{ inputs.container != '' && 'scylladb/scylla:2026.1' || '' }}
|
||||
options: >-
|
||||
--health-cmd "cqlsh -e 'describe cluster'"
|
||||
--health-interval 10s
|
||||
@@ -138,15 +138,15 @@ jobs:
|
||||
run: |
|
||||
docker rm --force scylladb || true
|
||||
docker run \
|
||||
--detach \
|
||||
--name scylladb \
|
||||
--health-cmd "cqlsh -e 'describe cluster'" \
|
||||
--health-interval 10s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 5 \
|
||||
--publish 9042:9042 \
|
||||
--memory 16G \
|
||||
scylladb/scylla
|
||||
--detach \
|
||||
--name scylladb \
|
||||
--health-cmd "cqlsh -e 'describe cluster'" \
|
||||
--health-interval 10s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 5 \
|
||||
--publish 9042:9042 \
|
||||
--memory 16G \
|
||||
scylladb/scylla:2026.1
|
||||
|
||||
until [ "$(docker inspect -f '{{.State.Health.Status}}' scylladb)" == "healthy" ]; do
|
||||
sleep 5
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
|
||||
"xrpl/3.2.0-rc3+e5cf1a0#694aadb2bdc6226effdb3b1920463e37%1780518730.129",
|
||||
"xrpl/3.2.0#c3c124909c6461012758a4fab7c90cd5%1781569913.756",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1778091117.311",
|
||||
"spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1768312128.781",
|
||||
"soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
|
||||
|
||||
@@ -16,7 +16,7 @@ class ClioConan(ConanFile):
|
||||
"fmt/12.1.0",
|
||||
"libbacktrace/cci.20210118",
|
||||
"spdlog/1.17.0",
|
||||
"xrpl/3.2.0-rc3+e5cf1a0",
|
||||
"xrpl/3.2.0",
|
||||
]
|
||||
|
||||
default_options = {
|
||||
|
||||
@@ -13,7 +13,7 @@ If you see the error log message `Could not connect to Cassandra: No hosts avail
|
||||
If you would like to run a local ScyllaDB, you can call:
|
||||
|
||||
```sh
|
||||
docker run --rm -p 9042:9042 --name clio-scylla -d scylladb/scylla
|
||||
docker run --rm -p 9042:9042 --name clio-scylla -d scylladb/scylla:2026.1
|
||||
```
|
||||
|
||||
## Check the server status of Clio
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "util/build/Build.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/requests/SslContext.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/RPCServerHandler.hpp"
|
||||
#include "web/Server.hpp"
|
||||
@@ -97,6 +98,11 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
|
||||
int
|
||||
ClioApplication::run(bool const useNgWebServer)
|
||||
{
|
||||
if (auto const sslContext = util::requests::initClientSslContext(); not sslContext.has_value()) {
|
||||
LOG(util::LogService::fatal()) << "Failed to create client SSL context: " << sslContext.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
auto const threads = config_.get<uint16_t>("io_threads");
|
||||
LOG(util::LogService::info()) << "Number of io threads = " << threads;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ target_sources(
|
||||
Retry.cpp
|
||||
Repeat.cpp
|
||||
requests/RequestBuilder.cpp
|
||||
requests/SslContext.cpp
|
||||
requests/Types.cpp
|
||||
requests/WsConnection.cpp
|
||||
requests/impl/SslContext.cpp
|
||||
|
||||
@@ -136,18 +136,16 @@ std::expected<std::string, RequestError>
|
||||
RequestBuilder::doSslRequest(asio::yield_context yield, beast::http::verb method)
|
||||
{
|
||||
auto streamData = impl::SslTcpStreamData::create(yield);
|
||||
if (not streamData.has_value())
|
||||
return std::unexpected{std::move(streamData).error()};
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
||||
if (!SSL_set_tlsext_host_name(streamData->stream.native_handle(), host_.c_str())) {
|
||||
if (!SSL_set_tlsext_host_name(streamData.stream.native_handle(), host_.c_str())) {
|
||||
#pragma GCC diagnostic pop
|
||||
beast::error_code errorCode;
|
||||
errorCode.assign(static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category());
|
||||
return std::unexpected{RequestError{"SSL setup failed", errorCode}};
|
||||
}
|
||||
return doRequestImpl(std::move(streamData).value(), yield, method);
|
||||
return doRequestImpl(std::move(streamData), yield, method);
|
||||
}
|
||||
|
||||
std::expected<std::string, RequestError>
|
||||
|
||||
35
src/util/requests/SslContext.cpp
Normal file
35
src/util/requests/SslContext.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2026, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/requests/SslContext.hpp"
|
||||
|
||||
#include "util/requests/impl/SslContext.hpp"
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
namespace util::requests {
|
||||
|
||||
std::expected<void, std::string>
|
||||
initClientSslContext()
|
||||
{
|
||||
return impl::initClientSslContext();
|
||||
}
|
||||
|
||||
} // namespace util::requests
|
||||
40
src/util/requests/SslContext.hpp
Normal file
40
src/util/requests/SslContext.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2026, 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 <expected>
|
||||
#include <string>
|
||||
|
||||
namespace util::requests {
|
||||
|
||||
/**
|
||||
* @brief Create and cache the shared client SSL context.
|
||||
*
|
||||
* Intended to be called once during application startup so that an unrecoverable SSL
|
||||
* misconfiguration (e.g. a missing system root certificate bundle) is reported immediately instead
|
||||
* of causing every outgoing request to fail later. The context (including the potentially expensive
|
||||
* parse of the root certificate bundle) is created once and reused for the lifetime of the process.
|
||||
*
|
||||
* @return An error message if the context could not be created
|
||||
*/
|
||||
std::expected<void, std::string>
|
||||
initClientSslContext();
|
||||
|
||||
} // namespace util::requests
|
||||
@@ -95,18 +95,16 @@ std::expected<WsConnectionPtr, RequestError>
|
||||
WsConnectionBuilder::sslConnect(asio::yield_context yield) const
|
||||
{
|
||||
auto streamData = impl::SslWsStreamData::create(yield);
|
||||
if (not streamData.has_value())
|
||||
return std::unexpected{std::move(streamData).error()};
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
||||
if (!SSL_set_tlsext_host_name(streamData->stream.next_layer().native_handle(), host_.c_str())) {
|
||||
if (!SSL_set_tlsext_host_name(streamData.stream.next_layer().native_handle(), host_.c_str())) {
|
||||
#pragma GCC diagnostic pop
|
||||
beast::error_code errorCode;
|
||||
errorCode.assign(static_cast<int>(::ERR_get_error()), beast::net::error::get_ssl_category());
|
||||
return std::unexpected{RequestError{"SSL setup failed", errorCode}};
|
||||
}
|
||||
return connectImpl(std::move(streamData).value(), yield);
|
||||
return connectImpl(std::move(streamData), yield);
|
||||
}
|
||||
|
||||
std::expected<WsConnectionPtr, RequestError>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -62,38 +63,82 @@ constexpr std::array kCERT_FILE_PATHS{
|
||||
"/system/etc/security/cacerts", // Android
|
||||
};
|
||||
|
||||
std::expected<std::string, RequestError>
|
||||
std::optional<std::string>
|
||||
readCertificateFile(std::filesystem::path const& path)
|
||||
{
|
||||
if (not std::filesystem::exists(path))
|
||||
return std::nullopt;
|
||||
|
||||
std::ifstream const fileStream{path, std::ios::in};
|
||||
if (not fileStream.is_open())
|
||||
return std::nullopt;
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << fileStream.rdbuf();
|
||||
|
||||
return std::move(buffer).str();
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
getRootCertificate()
|
||||
{
|
||||
for (auto const& path : kCERT_FILE_PATHS) {
|
||||
if (std::filesystem::exists(path)) {
|
||||
std::ifstream const fileStream{path, std::ios::in};
|
||||
if (not fileStream.is_open()) {
|
||||
continue;
|
||||
}
|
||||
std::stringstream buffer;
|
||||
buffer << fileStream.rdbuf();
|
||||
return std::move(buffer).str();
|
||||
}
|
||||
// Honor the OpenSSL-standard SSL_CERT_FILE environment variable first. Some
|
||||
// environments (e.g. the Nix-based CI/runtime image) point it at their CA
|
||||
// bundle instead of installing certificates at the well-known system paths.
|
||||
if (char const* const certFile = std::getenv("SSL_CERT_FILE"); certFile != nullptr) {
|
||||
if (auto contents = readCertificateFile(certFile); contents.has_value())
|
||||
return contents;
|
||||
}
|
||||
return std::unexpected{RequestError{"SSL setup failed: could not find root certificate"}};
|
||||
|
||||
for (auto const& path : kCERT_FILE_PATHS) {
|
||||
if (auto contents = readCertificateFile(path); contents.has_value())
|
||||
return contents;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::expected<ssl::context, RequestError>&
|
||||
cachedClientSslContext()
|
||||
{
|
||||
static std::expected<ssl::context, RequestError> kCONTEXT = makeClientSslContext(getRootCertificate());
|
||||
return kCONTEXT;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::expected<boost::asio::ssl::context, RequestError>
|
||||
makeClientSslContext()
|
||||
std::expected<ssl::context, RequestError>
|
||||
makeClientSslContext(std::optional<std::string> const& rootCertificate)
|
||||
{
|
||||
if (not rootCertificate.has_value())
|
||||
return std::unexpected{RequestError{"SSL setup failed: could not find root certificate"}};
|
||||
|
||||
ssl::context context{ssl::context::tls_client};
|
||||
context.set_verify_mode(ssl::verify_peer);
|
||||
auto const rootCertificate = getRootCertificate();
|
||||
if (not rootCertificate.has_value()) {
|
||||
return std::unexpected{rootCertificate.error()};
|
||||
}
|
||||
context.add_certificate_authority(asio::buffer(rootCertificate->data(), rootCertificate->size()));
|
||||
context.add_certificate_authority( //
|
||||
asio::buffer(rootCertificate->data(), rootCertificate->size())
|
||||
);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
std::expected<void, std::string>
|
||||
initClientSslContext()
|
||||
{
|
||||
auto const& context = cachedClientSslContext();
|
||||
if (not context.has_value())
|
||||
return std::unexpected{context.error().message()};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ssl::context&
|
||||
getClientSslContext()
|
||||
{
|
||||
// initClientSslContext() called during startup
|
||||
return cachedClientSslContext().value();
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
sslErrorToString(boost::beast::error_code const& error)
|
||||
{
|
||||
|
||||
@@ -31,7 +31,13 @@
|
||||
namespace util::requests::impl {
|
||||
|
||||
std::expected<boost::asio::ssl::context, RequestError>
|
||||
makeClientSslContext();
|
||||
makeClientSslContext(std::optional<std::string> const& rootCertificate);
|
||||
|
||||
std::expected<void, std::string>
|
||||
initClientSslContext();
|
||||
|
||||
boost::asio::ssl::context&
|
||||
getClientSslContext();
|
||||
|
||||
std::optional<std::string>
|
||||
sslErrorToString(boost::beast::error_code const& error);
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/requests/Types.hpp"
|
||||
#include "util/requests/impl/SslContext.hpp"
|
||||
|
||||
#include <boost/asio/associated_executor.hpp>
|
||||
@@ -32,9 +31,6 @@
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <utility>
|
||||
|
||||
namespace util::requests::impl {
|
||||
|
||||
template <typename StreamType>
|
||||
@@ -52,28 +48,19 @@ using TcpStreamData = PlainStreamData<boost::beast::tcp_stream>;
|
||||
using WsStreamData = PlainStreamData<boost::beast::websocket::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
template <typename StreamType>
|
||||
class SslStreamData {
|
||||
boost::asio::ssl::context sslContext_;
|
||||
|
||||
public:
|
||||
struct SslStreamData {
|
||||
static constexpr bool kSSL_ENABLED = true;
|
||||
|
||||
static std::expected<SslStreamData, RequestError>
|
||||
create(boost::asio::yield_context yield)
|
||||
{
|
||||
auto sslContext = makeClientSslContext();
|
||||
if (not sslContext.has_value()) {
|
||||
return std::unexpected{std::move(sslContext.error())};
|
||||
}
|
||||
return SslStreamData{std::move(sslContext).value(), yield};
|
||||
}
|
||||
|
||||
StreamType stream;
|
||||
|
||||
private:
|
||||
SslStreamData(boost::asio::ssl::context sslContext, boost::asio::yield_context yield)
|
||||
: sslContext_(std::move(sslContext)), stream(boost::asio::get_associated_executor(yield), sslContext_)
|
||||
static SslStreamData
|
||||
create(boost::asio::yield_context yield)
|
||||
{
|
||||
return SslStreamData{getClientSslContext(), yield};
|
||||
}
|
||||
|
||||
private:
|
||||
SslStreamData(boost::asio::ssl::context& sslContext, boost::asio::yield_context yield)
|
||||
: stream(boost::asio::get_associated_executor(yield), sslContext)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,12 +19,37 @@
|
||||
|
||||
#include "util/requests/impl/SslContext.hpp"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
using namespace util::requests::impl;
|
||||
|
||||
TEST(SslContext, Create)
|
||||
{
|
||||
auto ctx = makeClientSslContext();
|
||||
EXPECT_TRUE(ctx);
|
||||
auto& ctx = getClientSslContext();
|
||||
EXPECT_NE(ctx.native_handle(), nullptr);
|
||||
}
|
||||
|
||||
TEST(SslContext, IsCached)
|
||||
{
|
||||
auto& first = getClientSslContext();
|
||||
auto& second = getClientSslContext();
|
||||
|
||||
// Same shared instance is returned on every call
|
||||
EXPECT_EQ(&first, &second);
|
||||
}
|
||||
|
||||
TEST(SslContext, InitSucceedsWithSystemCertificates)
|
||||
{
|
||||
EXPECT_TRUE(initClientSslContext().has_value());
|
||||
}
|
||||
|
||||
TEST(SslContext, MakeWithoutRootCertificateReturnsError)
|
||||
{
|
||||
auto const ctx = makeClientSslContext(std::nullopt);
|
||||
|
||||
ASSERT_FALSE(ctx.has_value());
|
||||
EXPECT_THAT(ctx.error().message(), testing::HasSubstr("could not find root certificate"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user