mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-21 03:12:29 +00:00
Compare commits
189 Commits
a1q123456/
...
ximinez/on
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b7471af6 | ||
|
|
4b198cd5bb | ||
|
|
e0734986dd | ||
|
|
726f20c8f6 | ||
|
|
96643bb0fa | ||
|
|
e83818241a | ||
|
|
8440f479e5 | ||
|
|
ac390622e0 | ||
|
|
5d807f0d6d | ||
|
|
b93294f26b | ||
|
|
ef95ace0f9 | ||
|
|
fc58bf6edf | ||
|
|
348555d5ba | ||
|
|
302802e42d | ||
|
|
5d881f87a3 | ||
|
|
a8a8035b32 | ||
|
|
45af14231f | ||
|
|
380bf274d0 | ||
|
|
460ec5eeea | ||
|
|
14be8ca4ea | ||
|
|
948264d44c | ||
|
|
9c816b2043 | ||
|
|
f68402acd1 | ||
|
|
5358e25eaa | ||
|
|
a34d1e5537 | ||
|
|
78a122943d | ||
|
|
06135e7203 | ||
|
|
f20425fa4f | ||
|
|
3e94546acc | ||
|
|
3b088ed0dc | ||
|
|
26182ed52e | ||
|
|
de2a3e10f5 | ||
|
|
e17f8554fc | ||
|
|
386a7192ba | ||
|
|
12cc6e424d | ||
|
|
c9deecf1b7 | ||
|
|
ddd1b49f38 | ||
|
|
c1b2a24005 | ||
|
|
86d88eca31 | ||
|
|
ef09eaea00 | ||
|
|
c504cfb291 | ||
|
|
0c217dfa2b | ||
|
|
b0198d2566 | ||
|
|
7eee8ca802 | ||
|
|
2a079a0154 | ||
|
|
40989c1178 | ||
|
|
addc831eb3 | ||
|
|
b4efc6d116 | ||
|
|
125d075d6e | ||
|
|
370a775479 | ||
|
|
1a2ee706eb | ||
|
|
2a981357ba | ||
|
|
1ae475e724 | ||
|
|
a3e9401fbc | ||
|
|
9091469f9e | ||
|
|
17fa54f1f9 | ||
|
|
8fb5347c2d | ||
|
|
6739bf998f | ||
|
|
6eea38ba67 | ||
|
|
e9cf88b359 | ||
|
|
645b203476 | ||
|
|
be2aff1f4c | ||
|
|
56ed237e82 | ||
|
|
fd7b0fd135 | ||
|
|
e700994891 | ||
|
|
c76f7029ac | ||
|
|
d535c5fb2a | ||
|
|
54f860463e | ||
|
|
950434b8ff | ||
|
|
ee365e876d | ||
|
|
c6c59834b9 | ||
|
|
63b47914b8 | ||
|
|
9e02e5be2e | ||
|
|
093cd70fa1 | ||
|
|
376d65a483 | ||
|
|
a0d9a2458e | ||
|
|
456f639cf7 | ||
|
|
2c559ec2f3 | ||
|
|
619c81f463 | ||
|
|
f1490df960 | ||
|
|
7bdf74de98 | ||
|
|
1743d6fb98 | ||
|
|
ca7a5bb926 | ||
|
|
ce8b1a3f1e | ||
|
|
486fa75a10 | ||
|
|
f8d68cd3d3 | ||
|
|
ef7a3f5606 | ||
|
|
4f84ed7490 | ||
|
|
d534103131 | ||
|
|
82dff3c2ce | ||
|
|
30d73eb5ba | ||
|
|
1b2754bac2 | ||
|
|
cf80cafc75 | ||
|
|
b8897d51de | ||
|
|
3ff25eeb65 | ||
|
|
2bbfc4e786 | ||
|
|
2b1eb052e6 | ||
|
|
360e214e54 | ||
|
|
2618afed94 | ||
|
|
698ba2c788 | ||
|
|
b614e99588 | ||
|
|
fe8e4af2fa | ||
|
|
0a897f1528 | ||
|
|
cf8a3f5779 | ||
|
|
db39a39868 | ||
|
|
37a03d28c2 | ||
|
|
19d275425a | ||
|
|
88e9045602 | ||
|
|
5adbc536b6 | ||
|
|
e27af94ba9 | ||
|
|
43fe1e7e9c | ||
|
|
f456a858c8 | ||
|
|
084c3aa88e | ||
|
|
34f9b63921 | ||
|
|
bd3de79817 | ||
|
|
304eee2259 | ||
|
|
9e729b7f59 | ||
|
|
dd141468c4 | ||
|
|
933147c21f | ||
|
|
9201a4f591 | ||
|
|
5adb1e9b8b | ||
|
|
4df84d7988 | ||
|
|
cd87c0968b | ||
|
|
8a8e7c90bf | ||
|
|
e806069065 | ||
|
|
ce948cbec0 | ||
|
|
6ed34b3294 | ||
|
|
7161a235ca | ||
|
|
71463810de | ||
|
|
e997219a85 | ||
|
|
895cc13fa6 | ||
|
|
8d3c3ca29a | ||
|
|
9829553807 | ||
|
|
e551f9731a | ||
|
|
fd827bf58b | ||
|
|
5a3baba34d | ||
|
|
c78f5b160f | ||
|
|
485f78761a | ||
|
|
23cd2f7b21 | ||
|
|
5753266c43 | ||
|
|
4722d2607d | ||
|
|
85b5b4f855 | ||
|
|
a16f492f0f | ||
|
|
3633dc632c | ||
|
|
b3b30c3a86 | ||
|
|
c78a7684f4 | ||
|
|
cf83d92630 | ||
|
|
a56b1274d8 | ||
|
|
ae4bdd0492 | ||
|
|
e90102dd3b | ||
|
|
71f0e8db3d | ||
|
|
638929373a | ||
|
|
8440654377 | ||
|
|
9fa66c4741 | ||
|
|
38a9235145 | ||
|
|
c7a3cc9108 | ||
|
|
248337908d | ||
|
|
3d003619fd | ||
|
|
f163dca12c | ||
|
|
6e0ce458e5 | ||
|
|
5fae8480f1 | ||
|
|
e6587d374a | ||
|
|
376cc404e0 | ||
|
|
9898ca638f | ||
|
|
34b46d8f7c | ||
|
|
fe7d0798a7 | ||
|
|
0cecc09d71 | ||
|
|
e091d55561 | ||
|
|
69cf18158b | ||
|
|
6513c53817 | ||
|
|
e13baa58a5 | ||
|
|
951056fe9b | ||
|
|
67700ea6bd | ||
|
|
e5442cf3f1 | ||
|
|
da68076f04 | ||
|
|
b24116a118 | ||
|
|
f67398c6bf | ||
|
|
43d3eb1a24 | ||
|
|
0993315ed5 | ||
|
|
0bc383ada9 | ||
|
|
1841ceca43 | ||
|
|
2714cebabd | ||
|
|
e184db4ce2 | ||
|
|
ac6dc6943c | ||
|
|
ddd53806df | ||
|
|
e629a1f70e | ||
|
|
68076d969c | ||
|
|
d3009d3e1c | ||
|
|
54f7f3c894 |
@@ -1033,10 +1033,11 @@
|
||||
# The online delete process checks periodically
|
||||
# that xrpld is still in sync with the network,
|
||||
# and that the validated ledger is less than
|
||||
# 'age_threshold_seconds' old. If not, then continue
|
||||
# 'age_threshold_seconds' old, and that all
|
||||
# recent ledgers are available. If not, then continue
|
||||
# sleeping for this number of seconds and
|
||||
# checking until healthy.
|
||||
# Default is 5.
|
||||
# Default is 1.
|
||||
#
|
||||
# Notes:
|
||||
# The 'node_db' entry configures the primary, persistent storage.
|
||||
@@ -1416,6 +1417,12 @@
|
||||
# in this section to a comma-separated list of the addresses
|
||||
# of your Clio servers, in order to bypass xrpld's rate limiting.
|
||||
#
|
||||
# TLS/SSL can be enabled for gRPC by specifying ssl_cert and ssl_key.
|
||||
# Both parameters must be provided together. The ssl_cert_chain parameter
|
||||
# is optional and provides intermediate CA certificates for the certificate
|
||||
# chain. The ssl_client_ca parameter is optional and enables mutual TLS
|
||||
# (client certificate verification).
|
||||
#
|
||||
# This port is commented out but can be enabled by removing
|
||||
# the '#' from each corresponding line including the entry under [server]
|
||||
#
|
||||
@@ -1465,11 +1472,74 @@ admin = 127.0.0.1
|
||||
protocol = ws
|
||||
send_queue_limit = 500
|
||||
|
||||
# gRPC TLS/SSL Configuration
|
||||
#
|
||||
# The gRPC port supports optional TLS/SSL encryption. When TLS is not
|
||||
# configured, the gRPC server will accept unencrypted connections.
|
||||
#
|
||||
# ssl_cert = <filename>
|
||||
# ssl_key = <filename>
|
||||
#
|
||||
# To enable TLS for gRPC, both ssl_cert and ssl_key must be specified.
|
||||
# If only one is provided, xrpld will fail to start.
|
||||
#
|
||||
# ssl_cert: Path to the server's SSL certificate file in PEM format.
|
||||
# ssl_key: Path to the server's SSL private key file in PEM format.
|
||||
#
|
||||
# When configured, the gRPC server will only accept TLS-encrypted
|
||||
# connections. Clients must use TLS (secure) channel credentials rather
|
||||
# than plaintext / insecure connections.
|
||||
#
|
||||
# ssl_cert_chain = <filename>
|
||||
#
|
||||
# Optional. Path to intermediate CA certificate(s) in PEM format that
|
||||
# complete the server's certificate chain.
|
||||
#
|
||||
# This file should contain the intermediate CA certificate(s) needed
|
||||
# to build a trust chain from the server certificate (ssl_cert) to a
|
||||
# root CA that clients trust. Multiple certificates should be
|
||||
# concatenated in PEM format.
|
||||
#
|
||||
# This is needed when your server certificate was signed by an
|
||||
# intermediate CA rather than directly by a root CA. Without this,
|
||||
# clients may fail to verify your server certificate.
|
||||
#
|
||||
# If not specified, only the server certificate from ssl_cert will be
|
||||
# presented to clients.
|
||||
#
|
||||
# ssl_client_ca = <filename>
|
||||
#
|
||||
# Optional. Path to a CA certificate file in PEM format for verifying
|
||||
# client certificates (mutual TLS / mTLS).
|
||||
#
|
||||
# When specified, the gRPC server will verify client certificates
|
||||
# against this CA. This enables mutual authentication where both the
|
||||
# server and client verify each other's identity.
|
||||
#
|
||||
# This is typically NOT needed for public-facing gRPC servers. Only
|
||||
# use this if you want to restrict access to clients with valid
|
||||
# certificates signed by the specified CA.
|
||||
#
|
||||
# If not specified, the server will use one-way TLS (server
|
||||
# authentication only) and will accept connections from any client.
|
||||
#
|
||||
[port_grpc]
|
||||
port = 50051
|
||||
ip = 127.0.0.1
|
||||
secure_gateway = 127.0.0.1
|
||||
|
||||
# Optional TLS/SSL configuration for gRPC
|
||||
# To enable TLS, uncomment and configure both ssl_cert and ssl_key:
|
||||
#ssl_cert = /etc/ssl/certs/grpc-server.crt
|
||||
#ssl_key = /etc/ssl/private/grpc-server.key
|
||||
|
||||
# Optional: Include intermediate CA certificates for complete certificate chain
|
||||
#ssl_cert_chain = /etc/ssl/certs/grpc-intermediate-ca.crt
|
||||
|
||||
# Optional: Enable mutual TLS (client certificate verification)
|
||||
# Uncomment to require and verify client certificates:
|
||||
#ssl_client_ca = /etc/ssl/certs/grpc-client-ca.crt
|
||||
|
||||
#[port_ws_public]
|
||||
#port = 6005
|
||||
#ip = 127.0.0.1
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace xrpl {
|
||||
class ValidPermissionedDEX
|
||||
{
|
||||
bool regularOffers_ = false;
|
||||
bool badHybrids_ = false;
|
||||
bool badHybridsOld_ = false; // pre-fixSecurity3_1_3: missing field/domain or size > 1
|
||||
bool badHybrids_ = false; // post-fixSecurity3_1_3: also catches size == 0 (size != 1)
|
||||
hash_set<uint256> domains_;
|
||||
|
||||
public:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -61,10 +62,26 @@ offerInDomain(
|
||||
if (sleOffer->getFieldH256(sfDomainID) != domainID)
|
||||
return false; // LCOV_EXCL_LINE
|
||||
|
||||
if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
|
||||
if (view.rules().enabled(fixSecurity3_1_3))
|
||||
{
|
||||
JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
|
||||
return false; // LCOV_EXCL_LINE
|
||||
// post-fixSecurity3_1_3: also catches empty sfAdditionalBooks (size == 0)
|
||||
if (sleOffer->isFlag(lsfHybrid) &&
|
||||
(!sleOffer->isFieldPresent(sfAdditionalBooks) ||
|
||||
sleOffer->getFieldArray(sfAdditionalBooks).size() != 1))
|
||||
{
|
||||
JLOG(j.error()) << "Hybrid offer " << offerID
|
||||
<< " missing or malformed AdditionalBooks field";
|
||||
return false; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// pre-fixSecurity3_1_3: only check for missing sfAdditionalBooks
|
||||
if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
|
||||
{
|
||||
JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
|
||||
return false; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
return accountInDomain(view, sleOffer->getAccountID(sfAccount), domainID);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -40,11 +41,17 @@ ValidPermissionedDEX::visitEntry(
|
||||
regularOffers_ = true;
|
||||
}
|
||||
|
||||
// if a hybrid offer is missing domain or additional book, there's
|
||||
// something wrong
|
||||
// pre-fixSecurity3_1_3: hybrid offer missing domain, missing
|
||||
// sfAdditionalBooks, or sfAdditionalBooks has more than one entry
|
||||
if (after->isFlag(lsfHybrid) &&
|
||||
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
|
||||
after->getFieldArray(sfAdditionalBooks).size() > 1))
|
||||
badHybridsOld_ = true;
|
||||
|
||||
// post-fixSecurity3_1_3: same as above but also catches size == 0
|
||||
if (after->isFlag(lsfHybrid) &&
|
||||
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
|
||||
after->getFieldArray(sfAdditionalBooks).size() != 1))
|
||||
badHybrids_ = true;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +70,8 @@ ValidPermissionedDEX::finalize(
|
||||
|
||||
// For each offercreate transaction, check if
|
||||
// permissioned offers are valid
|
||||
if (txType == ttOFFER_CREATE && badHybrids_)
|
||||
bool const isMalformed = view.rules().enabled(fixSecurity3_1_3) ? badHybrids_ : badHybridsOld_;
|
||||
if (txType == ttOFFER_CREATE && isMalformed)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
|
||||
return false;
|
||||
|
||||
@@ -594,7 +594,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
|
||||
auto const cancelSequence = ctx_.tx[~sfOfferSequence];
|
||||
|
||||
// Note that we we use the value from the sequence or ticket as the
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
// offer sequence. For more explanation see comments in SeqProxy.h.
|
||||
auto const offerSequence = ctx_.tx.getSeqValue();
|
||||
|
||||
|
||||
@@ -439,7 +439,7 @@ EscrowCreate::doApply()
|
||||
return tecDST_TAG_NEEDED;
|
||||
}
|
||||
|
||||
// Create escrow in ledger. Note that we we use the value from the
|
||||
// Create escrow in ledger. Note that we use the value from the
|
||||
// sequence or ticket. For more explanation see comments in SeqProxy.h.
|
||||
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
|
||||
auto const slep = std::make_shared<SLE>(escrowKeylet);
|
||||
|
||||
@@ -134,7 +134,7 @@ PaymentChannelCreate::doApply()
|
||||
|
||||
// Create PayChan in ledger.
|
||||
//
|
||||
// Note that we we use the value from the sequence or ticket as the
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
// payChan sequence. For more explanation see comments in SeqProxy.h.
|
||||
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
|
||||
auto const slep = std::make_shared<SLE>(payChanKeylet);
|
||||
|
||||
@@ -231,6 +231,12 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
((*mutableFlags & (f.setFlag | f.clearFlag)));
|
||||
}))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Clearing lsfMPTRequireAuth is invalid when the issuance already has
|
||||
// a DomainID set, because a DomainID requires RequireAuth to be active.
|
||||
if ((*mutableFlags & tmfMPTClearRequireAuth) != 0u &&
|
||||
sleMptIssuance->isFieldPresent(sfDomainID))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))
|
||||
|
||||
851
src/test/app/GRPCServerTLS_test.cpp
Normal file
851
src/test/app/GRPCServerTLS_test.cpp
Normal file
@@ -0,0 +1,851 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/get_ledger.pb.h>
|
||||
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
|
||||
#include <grpcpp/client_context.h>
|
||||
#include <grpcpp/create_channel.h>
|
||||
#include <grpcpp/grpcpp.h>
|
||||
#include <grpcpp/security/credentials.h>
|
||||
#include <grpcpp/support/status.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kCA_CERT_CONTENT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"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\n"
|
||||
"fAbAYqu0rfMFHUYjzIVnu8WRCC56qYHO5tU=\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
constexpr std::string_view kSERVER_CERT_CONTENT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"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\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
constexpr std::string_view kSERVER_KEY_CONTENT =
|
||||
"-----BEGIN PRIVATE KEY-----\n"
|
||||
"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\n"
|
||||
"S/RYUSUkZ4VvqFUfo7wT8x18urb87w==\n"
|
||||
"-----END PRIVATE KEY-----\n";
|
||||
|
||||
constexpr std::string_view kCLIENT_CERT_CONTENT =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"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\n"
|
||||
"cTe8jkzRqYdUfAoV\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
constexpr std::string_view kCLIENT_KEY_CONTENT =
|
||||
"-----BEGIN PRIVATE KEY-----\n"
|
||||
"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\n"
|
||||
"+6f4hsbp29hg+BUZ+UPdk7QyCKpZD1A=\n"
|
||||
"-----END PRIVATE KEY-----\n";
|
||||
|
||||
/**
|
||||
* RAII helper for managing temporary TLS certificates in tests.
|
||||
*
|
||||
* Creates a temporary directory and writes test certificates to it.
|
||||
* Automatically cleans up the directory when destroyed.
|
||||
*/
|
||||
class TemporaryTLSCertificates
|
||||
{
|
||||
public:
|
||||
static constexpr std::string_view kCA_CERT_FILENAME = "ca.pem";
|
||||
static constexpr std::string_view kSERVER_CERT_FILENAME = "server_cert.pem";
|
||||
static constexpr std::string_view kSERVER_KEY_FILENAME = "server_key.pem";
|
||||
static constexpr std::string_view kCLIENT_CERT_FILENAME = "client_cert.pem";
|
||||
static constexpr std::string_view kCLIENT_KEY_FILENAME = "client_key.pem";
|
||||
static constexpr std::string_view kCERTS_DIR_PREFIX = "grpc_tls_test_";
|
||||
|
||||
TemporaryTLSCertificates()
|
||||
{
|
||||
auto tmpDir = std::filesystem::temp_directory_path();
|
||||
auto uniqueDirName =
|
||||
boost::filesystem::unique_path(std::string(kCERTS_DIR_PREFIX) + "%%%%%%%%");
|
||||
tempDir_ = tmpDir / uniqueDirName.string();
|
||||
std::filesystem::create_directories(tempDir_);
|
||||
|
||||
writeFile(tempDir_ / kCA_CERT_FILENAME, kCA_CERT_CONTENT);
|
||||
writeFile(tempDir_ / kSERVER_CERT_FILENAME, kSERVER_CERT_CONTENT);
|
||||
writeFile(tempDir_ / kSERVER_KEY_FILENAME, kSERVER_KEY_CONTENT);
|
||||
writeFile(tempDir_ / kCLIENT_CERT_FILENAME, kCLIENT_CERT_CONTENT);
|
||||
writeFile(tempDir_ / kCLIENT_KEY_FILENAME, kCLIENT_KEY_CONTENT);
|
||||
}
|
||||
|
||||
virtual ~TemporaryTLSCertificates()
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(tempDir_, ec);
|
||||
}
|
||||
|
||||
TemporaryTLSCertificates(TemporaryTLSCertificates const&) = delete;
|
||||
TemporaryTLSCertificates&
|
||||
operator=(TemporaryTLSCertificates const&) = delete;
|
||||
TemporaryTLSCertificates(TemporaryTLSCertificates&&) = delete;
|
||||
TemporaryTLSCertificates&
|
||||
operator=(TemporaryTLSCertificates&&) = delete;
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getCACertPath() const
|
||||
{
|
||||
return tempDir_ / kCA_CERT_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getServerCertPath() const
|
||||
{
|
||||
return tempDir_ / kSERVER_CERT_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getServerKeyPath() const
|
||||
{
|
||||
return tempDir_ / kSERVER_KEY_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getClientCertPath() const
|
||||
{
|
||||
return tempDir_ / kCLIENT_CERT_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getClientKeyPath() const
|
||||
{
|
||||
return tempDir_ / kCLIENT_KEY_FILENAME;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path
|
||||
getTempDir() const
|
||||
{
|
||||
return tempDir_;
|
||||
}
|
||||
|
||||
private:
|
||||
static void
|
||||
writeFile(std::filesystem::path const& path, std::string_view content)
|
||||
{
|
||||
std::ofstream file(path);
|
||||
if (!file)
|
||||
throw std::runtime_error("Failed to create file: " + path.string());
|
||||
file << content;
|
||||
if (!file)
|
||||
throw std::runtime_error("Failed to write file: " + path.string());
|
||||
}
|
||||
|
||||
std::filesystem::path tempDir_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
/**
|
||||
* Helper function to make a simple gRPC call to test connectivity.
|
||||
* Returns true if the call succeeded, false otherwise.
|
||||
*/
|
||||
bool
|
||||
makeTestGRPCCall(std::unique_ptr<org::xrpl::rpc::v1::XRPLedgerAPIService::Stub> const& stub)
|
||||
{
|
||||
grpc::ClientContext context;
|
||||
org::xrpl::rpc::v1::GetLedgerRequest const request;
|
||||
org::xrpl::rpc::v1::GetLedgerResponse response;
|
||||
|
||||
// Set a short deadline to avoid hanging on failed connections
|
||||
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(2));
|
||||
|
||||
grpc::Status const status = stub->GetLedger(&context, request, &response);
|
||||
return status.ok();
|
||||
}
|
||||
|
||||
class GRPCServerTLS_test : public beast::unit_test::suite, public TemporaryTLSCertificates
|
||||
{
|
||||
public:
|
||||
void
|
||||
testWithoutTLS()
|
||||
{
|
||||
testcase("GRPCServer without TLS");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create config without TLS settings
|
||||
auto cfg = envconfig(addGrpcConfig);
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
// Test 1: Plaintext client should connect successfully
|
||||
std::string const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
auto plaintextStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
|
||||
BEAST_EXPECT(makeTestGRPCCall(plaintextStub));
|
||||
}
|
||||
|
||||
void
|
||||
testWithValidTLS()
|
||||
{
|
||||
testcase("GRPCServer with valid TLS configuration (no mutual TLS)");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with just server cert and key (no client verification)
|
||||
auto cfg = envconfig(
|
||||
addGrpcConfigWithTLS, getServerCertPath().string(), getServerKeyPath().string());
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
std::string const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test 1: Plaintext client should FAIL against TLS server
|
||||
auto plaintextStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(plaintextStub));
|
||||
|
||||
// Test 2: TLS client with server CA should succeed
|
||||
grpc::SslCredentialsOptions sslOpts;
|
||||
sslOpts.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOpts)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStub));
|
||||
}
|
||||
|
||||
void
|
||||
testWithMutualTLS()
|
||||
{
|
||||
testcase("GRPCServer with mutual TLS (client verification enabled)");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with server cert, key, and CA for client verification
|
||||
auto cfg = envconfig(
|
||||
addGrpcConfigWithTLSAndClientCA,
|
||||
getServerCertPath().string(),
|
||||
getServerKeyPath().string(),
|
||||
getCACertPath().string());
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test 1: TLS client WITHOUT client certificate should FAIL (mTLS requires client cert)
|
||||
grpc::SslCredentialsOptions sslOptsNoClient;
|
||||
sslOptsNoClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStubNoClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsNoClient)));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(tlsStubNoClient));
|
||||
|
||||
// Test 2: TLS client WITH client certificate should succeed
|
||||
grpc::SslCredentialsOptions sslOptsWithClient;
|
||||
sslOptsWithClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_cert_chain = std::string(kCLIENT_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_private_key = std::string(kCLIENT_KEY_CONTENT);
|
||||
auto tlsStubWithClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsWithClient)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStubWithClient));
|
||||
}
|
||||
|
||||
void
|
||||
testWithMissingKey()
|
||||
{
|
||||
testcase("GRPCServer with cert but no key");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create config with only cert (missing key)
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
// Intentionally omit ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for incomplete TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithMissingCert()
|
||||
{
|
||||
testcase("GRPCServer with key but no cert");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create config with only key (missing cert)
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
// Intentionally omit ssl_cert
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for incomplete TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithClientCAButNoTLS()
|
||||
{
|
||||
testcase("GRPCServer with ssl_client_ca but without both ssl_cert and ssl_key");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test 1: ssl_client_ca specified without any TLS config
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
// Intentionally omit both ssl_cert and ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_client_ca without TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find(
|
||||
"ssl_client_ca requires both ssl_cert and ssl_key") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: ssl_client_ca with only ssl_cert (missing ssl_key)
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
// Intentionally omit ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_client_ca with only ssl_cert");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// This should fail with "Incomplete TLS configuration" first
|
||||
// because ssl_cert is specified without ssl_key
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: ssl_client_ca with only ssl_key (missing ssl_cert)
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
// Intentionally omit ssl_cert
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_client_ca with only ssl_key");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// This should fail with "Incomplete TLS configuration" first
|
||||
// because ssl_key is specified without ssl_cert
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithCertChainButNoTLS()
|
||||
{
|
||||
testcase("GRPCServer with ssl_cert_chain but without both ssl_cert and ssl_key");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test 1: ssl_cert_chain specified without any TLS config
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", getCACertPath().string());
|
||||
// Intentionally omit both ssl_cert and ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_cert_chain without TLS config");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find(
|
||||
"ssl_cert_chain requires both ssl_cert and ssl_key") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: ssl_cert_chain with only ssl_cert (missing ssl_key)
|
||||
{
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", getCACertPath().string());
|
||||
// Intentionally omit ssl_key
|
||||
|
||||
try
|
||||
{
|
||||
Env const env(*this, std::move(cfg));
|
||||
fail("Should have thrown exception for ssl_cert_chain with only ssl_cert");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// This should fail with "Incomplete TLS configuration" first
|
||||
// because ssl_cert is specified without ssl_key
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Incomplete TLS configuration") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithCertChain()
|
||||
{
|
||||
testcase("GRPCServer with ssl_cert_chain for intermediate CA certificates");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with server cert, key, and cert chain (intermediate CA)
|
||||
// In this test, we use the CA cert as a stand-in for an intermediate CA cert
|
||||
auto cfg = envconfig(
|
||||
addGrpcConfigWithTLSAndCertChain,
|
||||
getServerCertPath().string(),
|
||||
getServerKeyPath().string(),
|
||||
getCACertPath().string());
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server actually started by checking the port
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test: TLS client should be able to connect (no client cert required)
|
||||
grpc::SslCredentialsOptions sslOpts;
|
||||
sslOpts.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOpts)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStub));
|
||||
|
||||
// Insecure client should fail
|
||||
auto insecureStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(insecureStub));
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidCertFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent certificate file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", "/nonexistent/path/to/cert.pem");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidKeyFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent key file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", "/nonexistent/path/to/key.pem");
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidCertChainFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent cert chain file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", "/nonexistent/path/to/chain.pem");
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithInvalidClientCAFile()
|
||||
{
|
||||
testcase("GRPCServer with invalid/non-existent client CA file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", "/nonexistent/path/to/ca.pem");
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start - verify port is 0
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithEmptyClientCAFile()
|
||||
{
|
||||
testcase("GRPCServer with empty client CA file");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Create an empty file for client CA
|
||||
auto emptyCAPath = getTempDir() / "empty_ca.pem";
|
||||
std::ofstream emptyFile(emptyCAPath);
|
||||
emptyFile.close();
|
||||
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", emptyCAPath.string());
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Server should fail to start due to empty CA file
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
|
||||
}
|
||||
|
||||
void
|
||||
testWithBothCertChainAndClientCA()
|
||||
{
|
||||
testcase("GRPCServer with both cert chain and client CA (full mTLS with intermediates)");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Test with all TLS features enabled: cert, key, cert_chain, and client_ca
|
||||
auto cfg = envconfig();
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
|
||||
(*cfg)[SECTION_PORT_GRPC].set(
|
||||
"ssl_cert_chain", getCACertPath().string()); // Using CA as intermediate
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
|
||||
|
||||
Env env(*this, std::move(cfg));
|
||||
|
||||
// Verify the server started successfully
|
||||
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpcPort.has_value());
|
||||
BEAST_EXPECT(*grpcPort > 0);
|
||||
|
||||
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
|
||||
|
||||
// Test 1: TLS client WITHOUT client certificate should FAIL (mTLS requires client cert)
|
||||
grpc::SslCredentialsOptions sslOptsNoClient;
|
||||
sslOptsNoClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
auto tlsStubNoClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsNoClient)));
|
||||
BEAST_EXPECT(!makeTestGRPCCall(tlsStubNoClient));
|
||||
|
||||
// Test 2: TLS client WITH client certificate should succeed
|
||||
grpc::SslCredentialsOptions sslOptsWithClient;
|
||||
sslOptsWithClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_cert_chain = std::string(kCLIENT_CERT_CONTENT);
|
||||
sslOptsWithClient.pem_private_key = std::string(kCLIENT_KEY_CONTENT);
|
||||
auto tlsStubWithClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
|
||||
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsWithClient)));
|
||||
BEAST_EXPECT(makeTestGRPCCall(tlsStubWithClient));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testWithoutTLS();
|
||||
testWithValidTLS();
|
||||
testWithMutualTLS();
|
||||
testWithMissingKey();
|
||||
testWithMissingCert();
|
||||
testWithClientCAButNoTLS();
|
||||
testWithCertChainButNoTLS();
|
||||
testWithCertChain();
|
||||
testWithInvalidCertFile();
|
||||
testWithInvalidKeyFile();
|
||||
testWithInvalidCertChainFile();
|
||||
testWithInvalidClientCAFile();
|
||||
testWithEmptyClientCAFile();
|
||||
testWithBothCertChainAndClientCA();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(GRPCServerTLS, app, xrpl);
|
||||
|
||||
} // namespace test
|
||||
} // namespace xrpl
|
||||
@@ -1774,8 +1774,10 @@ class Invariants_test : public beast::unit_test::suite
|
||||
using namespace test::jtx;
|
||||
|
||||
bool const fixPDEnabled = features[fixPermissionedDomainInvariant];
|
||||
bool const fixS313Enabled = features[fixSecurity3_1_3];
|
||||
|
||||
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fix" : "");
|
||||
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fixPD" : "") +
|
||||
std::string(fixS313Enabled ? " fixS313" : "");
|
||||
|
||||
doInvariantCheck(
|
||||
Env(*this, features),
|
||||
@@ -1863,6 +1865,45 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
// empty sfAdditionalBooks (size 0)
|
||||
{
|
||||
Env env1(*this, features);
|
||||
|
||||
Account const A1{"A1"};
|
||||
Account const A2{"A2"};
|
||||
env1.fund(XRP(1000), A1, A2);
|
||||
env1.close();
|
||||
|
||||
[[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, A1, A2);
|
||||
env1.close();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env1),
|
||||
A1,
|
||||
A2,
|
||||
fixS313Enabled ? std::vector<std::string>{{"hybrid offer is malformed"}}
|
||||
: std::vector<std::string>{},
|
||||
[&pd1](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
Keylet const offerKey = keylet::offer(A2.id(), 10);
|
||||
auto sleOffer = std::make_shared<SLE>(offerKey);
|
||||
sleOffer->setAccountID(sfAccount, A2);
|
||||
sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10));
|
||||
sleOffer->setFieldAmount(sfTakerGets, XRP(1));
|
||||
sleOffer->setFlag(lsfHybrid);
|
||||
sleOffer->setFieldH256(sfDomainID, pd1);
|
||||
|
||||
STArray const bookArr; // empty array, size 0
|
||||
sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
|
||||
ac.view().insert(sleOffer);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttOFFER_CREATE, [&](STObject&) {}},
|
||||
fixS313Enabled
|
||||
? std::initializer_list<TER>{tecINVARIANT_FAILED, tecINVARIANT_FAILED}
|
||||
: std::initializer_list<TER>{tesSUCCESS, tesSUCCESS});
|
||||
}
|
||||
|
||||
// hybrid offer missing sfAdditionalBooks
|
||||
{
|
||||
Env env1(*this, features);
|
||||
@@ -4061,6 +4102,10 @@ public:
|
||||
testPermissionedDomainInvariants(defaultAmendments() - fixPermissionedDomainInvariant);
|
||||
testPermissionedDEX(defaultAmendments() | fixPermissionedDomainInvariant);
|
||||
testPermissionedDEX(defaultAmendments() - fixPermissionedDomainInvariant);
|
||||
testPermissionedDEX(
|
||||
(defaultAmendments() | fixPermissionedDomainInvariant) - fixSecurity3_1_3);
|
||||
testPermissionedDEX(
|
||||
defaultAmendments() - fixPermissionedDomainInvariant - fixSecurity3_1_3);
|
||||
testNoModifiedUnmodifiableFields();
|
||||
testValidPseudoAccounts();
|
||||
testValidLoanBroker();
|
||||
|
||||
@@ -5,17 +5,21 @@
|
||||
#include <test/jtx/noop.h>
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/misc/SHAMapStore.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/ToString.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -112,6 +116,73 @@ class LedgerMaster_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCompleteLedgerRange(FeatureBitset features)
|
||||
{
|
||||
// Note that this test is intentionally very similar to
|
||||
// SHAMapStore_test::testLedgerGaps, but has a different
|
||||
// focus.
|
||||
|
||||
testcase("Complete Ledger operations");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
auto const deleteInterval = 8;
|
||||
|
||||
Env env{*this, envconfig([](auto cfg) {
|
||||
return online_delete(std::move(cfg), deleteInterval);
|
||||
})};
|
||||
|
||||
auto const alice = Account("alice");
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
LedgerIndex minSeq = 2;
|
||||
LedgerIndex maxSeq = env.closed()->header().seq;
|
||||
auto& store = env.app().getSHAMapStore();
|
||||
store.rendezvous();
|
||||
LedgerIndex lastRotated = store.getLastRotated();
|
||||
BEAST_EXPECTS(maxSeq == 3, to_string(maxSeq));
|
||||
BEAST_EXPECTS(lm.getCompleteLedgers() == "2-3", lm.getCompleteLedgers());
|
||||
BEAST_EXPECTS(lastRotated == 3, to_string(lastRotated));
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
|
||||
|
||||
// Close enough ledgers to rotate a few times
|
||||
for (int i = 0; i < 24; ++i)
|
||||
{
|
||||
for (int t = 0; t < 3; ++t)
|
||||
{
|
||||
env(noop(alice));
|
||||
}
|
||||
env.close();
|
||||
store.rendezvous();
|
||||
|
||||
++maxSeq;
|
||||
|
||||
if (maxSeq == lastRotated + deleteInterval)
|
||||
{
|
||||
minSeq = lastRotated;
|
||||
lastRotated = maxSeq;
|
||||
}
|
||||
BEAST_EXPECTS(
|
||||
env.closed()->header().seq == maxSeq, to_string(env.closed()->header().seq));
|
||||
BEAST_EXPECTS(store.getLastRotated() == lastRotated, to_string(store.getLastRotated()));
|
||||
std::stringstream expectedRange;
|
||||
expectedRange << minSeq << "-" << maxSeq;
|
||||
BEAST_EXPECTS(lm.getCompleteLedgers() == expectedRange.str(), lm.getCompleteLedgers());
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -125,6 +196,7 @@ public:
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testTxnIdFromIndex(features);
|
||||
testCompleteLedgerRange(features);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3042,45 +3042,83 @@ class MPToken_test : public beast::unit_test::suite
|
||||
testcase("Mutate MPTRequireAuth");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
// test mutating RequireAuth flag on the issuance and its effect on payment authorization
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1,
|
||||
.flags = tfMPTRequireAuth,
|
||||
.mutableFlags = tmfMPTCanMutateRequireAuth});
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1,
|
||||
.flags = tfMPTRequireAuth,
|
||||
.mutableFlags = tmfMPTCanMutateRequireAuth});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
|
||||
// Pay to bob
|
||||
mptAlice.pay(alice, bob, 1000);
|
||||
// Pay to bob
|
||||
mptAlice.pay(alice, bob, 1000);
|
||||
|
||||
// Unauthorize bob
|
||||
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
||||
// Unauthorize bob
|
||||
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
||||
|
||||
// Can not pay to bob
|
||||
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
||||
// Can not pay to bob
|
||||
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
||||
|
||||
// Clear RequireAuth
|
||||
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth});
|
||||
// Clear RequireAuth
|
||||
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth});
|
||||
|
||||
// Can pay to bob
|
||||
mptAlice.pay(alice, bob, 1000);
|
||||
// Can pay to bob
|
||||
mptAlice.pay(alice, bob, 1000);
|
||||
|
||||
// Set RequireAuth again
|
||||
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
|
||||
// Set RequireAuth again
|
||||
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
|
||||
|
||||
// Can not pay to bob since he is not authorized
|
||||
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
||||
// Can not pay to bob since he is not authorized
|
||||
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
||||
|
||||
// Authorize bob again
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
// Authorize bob again
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
|
||||
// Can pay to bob again
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
// Can pay to bob again
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
}
|
||||
|
||||
// Cannot clear RequireAuth when a DomainID is set on the issuance
|
||||
{
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const credIssuer{"credIssuer"};
|
||||
pdomain::Credentials const credentials{
|
||||
{.issuer = credIssuer, .credType = "credential"}};
|
||||
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(1000), credIssuer);
|
||||
env.close();
|
||||
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId = pdomain::getNewDomain(env.meta());
|
||||
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({
|
||||
.ownerCount = 1,
|
||||
.flags = tfMPTRequireAuth,
|
||||
.mutableFlags = tmfMPTCanMutateRequireAuth,
|
||||
.domainID = domainId,
|
||||
});
|
||||
|
||||
// Clearing RequireAuth while a DomainID is present must be rejected,
|
||||
mptAlice.set({
|
||||
.account = alice,
|
||||
.mutableFlags = tmfMPTClearRequireAuth,
|
||||
.err = tecNO_PERMISSION,
|
||||
});
|
||||
|
||||
// Setting RequireAuth (already set) is still allowed, though it has no effect.
|
||||
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -28,6 +30,8 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
@@ -35,6 +39,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -1385,6 +1390,73 @@ class PermissionedDEX_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
|
||||
}
|
||||
|
||||
void
|
||||
testHybridMalformedOffer(FeatureBitset features)
|
||||
{
|
||||
bool const fixS313Enabled = features[fixSecurity3_1_3];
|
||||
|
||||
testcase << "Hybrid offer with empty AdditionalBooks"
|
||||
<< (fixS313Enabled ? " (fixSecurity3_1_3 enabled)"
|
||||
: " (fixSecurity3_1_3 disabled)");
|
||||
|
||||
// offerInDomain has two code paths gated by fixSecurity3_1_3:
|
||||
//
|
||||
// pre-fix: only rejects a hybrid offer when sfAdditionalBooks is
|
||||
// entirely absent — an empty array (size 0) passes through.
|
||||
// post-fix: also rejects a hybrid offer whose sfAdditionalBooks array
|
||||
// has size != 1 (i.e. 0 or >1 entries).
|
||||
//
|
||||
// We create a valid hybrid offer, then directly manipulate its SLE to
|
||||
// produce the size==0 case that cannot occur via normal transactions,
|
||||
// and verify that the two code paths produce the expected outcomes.
|
||||
//
|
||||
// Note: the PermissionedDEX invariant checker (ValidPermissionedDEX)
|
||||
// does not flag this malformation for ttPAYMENT — only for
|
||||
// ttOFFER_CREATE — so the without-fix payment completes as tesSUCCESS.
|
||||
|
||||
Env env(*this, features);
|
||||
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
|
||||
PermissionedDEX(env);
|
||||
|
||||
// Create a valid hybrid offer (sfAdditionalBooks has exactly 1 entry)
|
||||
auto const bobOfferSeq{env.seq(bob)};
|
||||
env(offer(bob, XRP(10), USD(10)), txflags(tfHybrid), domain(domainID));
|
||||
env.close();
|
||||
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
|
||||
|
||||
// Directly manipulate the offer SLE in the open ledger so that
|
||||
// sfAdditionalBooks is present but empty (size 0). This is the
|
||||
// malformed state that fixSecurity3_1_3 is designed to catch.
|
||||
auto const offerKey = keylet::offer(bob.id(), bobOfferSeq);
|
||||
env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) {
|
||||
auto const sle = view.read(offerKey);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto replacement = std::make_shared<SLE>(*sle, sle->key());
|
||||
replacement->setFieldArray(sfAdditionalBooks, STArray{});
|
||||
view.rawReplace(replacement);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (fixS313Enabled)
|
||||
{
|
||||
// post-fixSecurity3_1_3: offerInDomain rejects the malformed
|
||||
// offer (size == 0), so no valid domain offer is found.
|
||||
env(pay(alice, carol, USD(10)),
|
||||
path(~USD),
|
||||
sendmax(XRP(10)),
|
||||
domain(domainID),
|
||||
ter(tecPATH_PARTIAL));
|
||||
}
|
||||
else
|
||||
{
|
||||
// pre-fixSecurity3_1_3: offerInDomain only checks for a missing
|
||||
// sfAdditionalBooks field; size == 0 passes through, so the
|
||||
// malformed offer is crossed and the payment succeeds.
|
||||
env(pay(alice, carol, USD(10)), path(~USD), sendmax(XRP(10)), domain(domainID));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1406,6 +1478,8 @@ public:
|
||||
testHybridBookStep(all);
|
||||
testHybridInvalidOffer(all);
|
||||
testHybridOfferDirectories(all);
|
||||
testHybridMalformedOffer(all);
|
||||
testHybridMalformedOffer(all - fixSecurity3_1_3);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/noop.h>
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/main/NodeStoreScheduler.h>
|
||||
#include <xrpld/app/misc/SHAMapStore.h>
|
||||
@@ -29,7 +31,9 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -42,10 +46,8 @@ class SHAMapStore_test : public beast::unit_test::suite
|
||||
static auto
|
||||
onlineDelete(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg->LEDGER_HISTORY = deleteInterval;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("online_delete", std::to_string(deleteInterval));
|
||||
return cfg;
|
||||
using namespace jtx;
|
||||
return online_delete(std::move(cfg), deleteInterval);
|
||||
}
|
||||
|
||||
static auto
|
||||
@@ -585,6 +587,160 @@ public:
|
||||
BEAST_EXPECT(dbr->getName() == "3");
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerGaps()
|
||||
{
|
||||
// Note that this test is intentionally very similar to
|
||||
// LedgerMaster_test::testCompleteLedgerRange, but has a different
|
||||
// focus.
|
||||
|
||||
testcase("Wait for ledger gaps to fill in");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, envconfig(onlineDelete)};
|
||||
|
||||
auto failureMessage = [&](char const* label, auto expected, auto actual) {
|
||||
std::stringstream ss;
|
||||
ss << label << ": Expected: " << expected << ", Got: " << actual;
|
||||
return ss.str();
|
||||
};
|
||||
|
||||
auto const alice = Account("alice");
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
LedgerIndex minSeq = 2;
|
||||
LedgerIndex maxSeq = env.closed()->header().seq;
|
||||
auto& store = env.app().getSHAMapStore();
|
||||
store.rendezvous();
|
||||
LedgerIndex lastRotated = store.getLastRotated();
|
||||
BEAST_EXPECTS(maxSeq == 3, to_string(maxSeq));
|
||||
BEAST_EXPECTS(lm.getCompleteLedgers() == "2-3", lm.getCompleteLedgers());
|
||||
BEAST_EXPECTS(lastRotated == 3, to_string(lastRotated));
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
|
||||
|
||||
// Close enough ledgers to rotate a few times
|
||||
while (maxSeq < 20)
|
||||
{
|
||||
for (int t = 0; t < 3; ++t)
|
||||
{
|
||||
env(noop(alice));
|
||||
}
|
||||
env.close();
|
||||
store.rendezvous();
|
||||
|
||||
++maxSeq;
|
||||
|
||||
if (maxSeq + 1 == lastRotated + deleteInterval)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// The next ledger will trigger a rotation. Delete the
|
||||
// current ledger from LedgerMaster.
|
||||
std::this_thread::sleep_for(100ms);
|
||||
LedgerIndex const deleteSeq = maxSeq;
|
||||
while (!lm.haveLedger(deleteSeq))
|
||||
{
|
||||
std::this_thread::sleep_for(100ms);
|
||||
}
|
||||
lm.clearLedger(deleteSeq);
|
||||
|
||||
auto expectedRange = [](auto minSeq, auto deleteSeq, auto maxSeq) {
|
||||
std::stringstream expectedRange;
|
||||
expectedRange << minSeq << "-" << (deleteSeq - 1);
|
||||
if (deleteSeq + 1 == maxSeq)
|
||||
{
|
||||
expectedRange << "," << maxSeq;
|
||||
}
|
||||
else if (deleteSeq < maxSeq)
|
||||
{
|
||||
expectedRange << "," << (deleteSeq + 1) << "-" << maxSeq;
|
||||
}
|
||||
return expectedRange.str();
|
||||
};
|
||||
BEAST_EXPECTS(
|
||||
lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq),
|
||||
failureMessage(
|
||||
"Complete ledgers",
|
||||
expectedRange(minSeq, deleteSeq, maxSeq),
|
||||
lm.getCompleteLedgers()));
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 1);
|
||||
|
||||
// Close another ledger, which will trigger a rotation, but the
|
||||
// rotation will be stuck until the missing ledger is filled in.
|
||||
env.close();
|
||||
// DO NOT CALL rendezvous()! You'll end up with a deadlock.
|
||||
++maxSeq;
|
||||
|
||||
// Nothing has changed
|
||||
BEAST_EXPECTS(
|
||||
store.getLastRotated() == lastRotated,
|
||||
failureMessage("lastRotated", lastRotated, store.getLastRotated()));
|
||||
BEAST_EXPECTS(
|
||||
lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq),
|
||||
failureMessage(
|
||||
"Complete ledgers",
|
||||
expectedRange(minSeq, deleteSeq, maxSeq),
|
||||
lm.getCompleteLedgers()));
|
||||
|
||||
// Close 5 more ledgers, waiting one second in between to
|
||||
// simulate the ledger making progress while online delete waits
|
||||
// for the missing ledger to be filled in.
|
||||
// This ensures the healthWait check has time to run and
|
||||
// detect the gap.
|
||||
for (int l = 0; l < 5; ++l)
|
||||
{
|
||||
env.close();
|
||||
// DO NOT CALL rendezvous()! You'll end up with a deadlock.
|
||||
++maxSeq;
|
||||
// Nothing has changed
|
||||
BEAST_EXPECTS(
|
||||
store.getLastRotated() == lastRotated,
|
||||
failureMessage("lastRotated", lastRotated, store.getLastRotated()));
|
||||
BEAST_EXPECTS(
|
||||
lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq),
|
||||
failureMessage(
|
||||
"Complete Ledgers",
|
||||
expectedRange(minSeq, deleteSeq, maxSeq),
|
||||
lm.getCompleteLedgers()));
|
||||
std::this_thread::sleep_for(1s);
|
||||
}
|
||||
|
||||
// Put the missing ledger back in LedgerMaster
|
||||
lm.setLedgerRangePresent(deleteSeq, deleteSeq);
|
||||
|
||||
// Wait for the rotation to finish
|
||||
store.rendezvous();
|
||||
|
||||
minSeq = lastRotated;
|
||||
lastRotated = deleteSeq + 1;
|
||||
}
|
||||
BEAST_EXPECT(maxSeq != lastRotated + deleteInterval);
|
||||
BEAST_EXPECTS(
|
||||
env.closed()->header().seq == maxSeq,
|
||||
failureMessage("maxSeq", maxSeq, env.closed()->header().seq));
|
||||
BEAST_EXPECTS(
|
||||
store.getLastRotated() == lastRotated,
|
||||
failureMessage("lastRotated", lastRotated, store.getLastRotated()));
|
||||
std::stringstream expectedRange;
|
||||
expectedRange << minSeq << "-" << maxSeq;
|
||||
BEAST_EXPECTS(
|
||||
lm.getCompleteLedgers() == expectedRange.str(),
|
||||
failureMessage("CompleteLedgers", expectedRange.str(), lm.getCompleteLedgers()));
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
|
||||
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -592,6 +748,7 @@ public:
|
||||
testAutomatic();
|
||||
testCanDelete();
|
||||
testRotate();
|
||||
testLedgerGaps();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -57,6 +57,17 @@ envconfig(F&& modfunc, Args&&... args)
|
||||
return modfunc(envconfig(), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
/// @brief adjust config to enable online_delete
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
///
|
||||
/// @param deleteInterval how many new ledgers should be available before
|
||||
/// rotating. Defaults to 8, because the standalone minimum is 8.
|
||||
///
|
||||
/// @return unique_ptr to Config instance
|
||||
std::unique_ptr<Config>
|
||||
online_delete(std::unique_ptr<Config> cfg, std::uint32_t deleteInterval = 8);
|
||||
|
||||
/// @brief adjust config so no admin ports are enabled
|
||||
///
|
||||
/// this is intended for use with envconfig, as in
|
||||
@@ -107,6 +118,52 @@ std::unique_ptr<Config> addGrpcConfig(std::unique_ptr<Config>);
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithSecureGateway(std::unique_ptr<Config>, std::string const& secureGateway);
|
||||
|
||||
/// @brief add a grpc address, port and TLS certificate/key paths to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server with TLS enabled.
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
/// @param certPath path to SSL certificate file
|
||||
/// @param keyPath path to SSL private key file
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLS(
|
||||
std::unique_ptr<Config>,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath);
|
||||
|
||||
/// @brief add a grpc address, port and TLS certificate/key/client CA paths to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server with mutual TLS (client certificate verification) enabled.
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
/// @param certPath path to SSL certificate file
|
||||
/// @param keyPath path to SSL private key file
|
||||
/// @param clientCAPath path to SSL client CA certificate file for mTLS
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndClientCA(
|
||||
std::unique_ptr<Config>,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& clientCAPath);
|
||||
|
||||
/// @brief add a grpc address, port and TLS with server cert chain to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server with TLS enabled and intermediate CA certificates.
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
/// @param certPath path to SSL certificate file
|
||||
/// @param keyPath path to SSL private key file
|
||||
/// @param certChainPath path to SSL intermediate CA certificate(s) file
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndCertChain(
|
||||
std::unique_ptr<Config>,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& certChainPath);
|
||||
|
||||
std::unique_ptr<Config>
|
||||
makeConfig(
|
||||
std::map<std::string, std::string> extraTxQ = {},
|
||||
|
||||
@@ -48,7 +48,7 @@ class JSONRPCClient : public AbstractClient
|
||||
continue;
|
||||
ParsedPort pp;
|
||||
parse_Port(pp, cfg[name], log);
|
||||
if (pp.protocol.count("http") == 0)
|
||||
if (not pp.protocol.contains("http"))
|
||||
continue;
|
||||
using namespace boost::asio::ip;
|
||||
if (pp.ip && pp.ip->is_unspecified())
|
||||
@@ -91,12 +91,6 @@ public:
|
||||
stream_.connect(ep_);
|
||||
}
|
||||
|
||||
~JSONRPCClient() override
|
||||
{
|
||||
// stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
|
||||
// stream_.close();
|
||||
}
|
||||
|
||||
/*
|
||||
Return value is an Object type with up to three keys:
|
||||
status
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -60,6 +62,15 @@ setupConfigForUnitTests(Config& cfg)
|
||||
|
||||
namespace jtx {
|
||||
|
||||
std::unique_ptr<Config>
|
||||
online_delete(std::unique_ptr<Config> cfg, std::uint32_t deleteInterval)
|
||||
{
|
||||
cfg->LEDGER_HISTORY = deleteInterval;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("online_delete", std::to_string(deleteInterval));
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
no_admin(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
@@ -132,6 +143,49 @@ addGrpcConfigWithSecureGateway(std::unique_ptr<Config> cfg, std::string const& s
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLS(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath)
|
||||
{
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndClientCA(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& clientCAPath)
|
||||
{
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", clientCAPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfigWithTLSAndCertChain(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& certPath,
|
||||
std::string const& keyPath,
|
||||
std::string const& certChainPath)
|
||||
{
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
|
||||
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", certChainPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Config>
|
||||
makeConfig(
|
||||
std::map<std::string, std::string> extraTxQ,
|
||||
|
||||
@@ -105,7 +105,10 @@ public:
|
||||
failedSave(std::uint32_t seq, uint256 const& hash);
|
||||
|
||||
std::string
|
||||
getCompleteLedgers();
|
||||
getCompleteLedgers() const;
|
||||
|
||||
std::size_t
|
||||
missingFromCompleteLedgerRange(LedgerIndex first, LedgerIndex last) const;
|
||||
|
||||
/** Apply held transactions to the open ledger
|
||||
This is normally called as we close the ledger.
|
||||
@@ -322,7 +325,7 @@ private:
|
||||
// A set of transactions to replay during the next close
|
||||
std::unique_ptr<LedgerReplay> replayData;
|
||||
|
||||
std::recursive_mutex mCompleteLock;
|
||||
std::recursive_mutex mutable mCompleteLock;
|
||||
RangeSet<std::uint32_t> mCompleteLedgers;
|
||||
|
||||
// Publish thread is running.
|
||||
|
||||
@@ -1570,12 +1570,34 @@ LedgerMaster::getPublishedLedger()
|
||||
}
|
||||
|
||||
std::string
|
||||
LedgerMaster::getCompleteLedgers()
|
||||
LedgerMaster::getCompleteLedgers() const
|
||||
{
|
||||
std::lock_guard const sl(mCompleteLock);
|
||||
return to_string(mCompleteLedgers);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LedgerMaster::missingFromCompleteLedgerRange(LedgerIndex first, LedgerIndex last) const
|
||||
{
|
||||
// Make a copy of the range to avoid holding the lock
|
||||
auto const range = [&] {
|
||||
std::lock_guard const sl(mCompleteLock);
|
||||
return mCompleteLedgers;
|
||||
}();
|
||||
|
||||
std::size_t missing = 0;
|
||||
|
||||
for (LedgerIndex idx = first; idx <= last; ++idx)
|
||||
{
|
||||
if (!boost::icl::contains(range, idx))
|
||||
{
|
||||
++missing;
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::optional<NetClock::time_point>
|
||||
LedgerMaster::getCloseTimeBySeq(LedgerIndex ledgerIndex)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/beast/core/CurrentThreadName.h>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/icl/interval_set.hpp>
|
||||
|
||||
#include <grpc/grpc_security_constants.h>
|
||||
#include <grpcpp/completion_queue.h>
|
||||
#include <grpcpp/security/server_credentials.h>
|
||||
#include <grpcpp/server_builder.h>
|
||||
@@ -387,6 +389,48 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
|
||||
Throw<std::runtime_error>("Error parsing secure_gateway section");
|
||||
}
|
||||
}
|
||||
|
||||
// Read TLS certificate configuration (optional)
|
||||
sslCertPath_ = section.get("ssl_cert");
|
||||
sslKeyPath_ = section.get("ssl_key");
|
||||
sslCertChainPath_ = section.get("ssl_cert_chain");
|
||||
sslClientCAPath_ = section.get("ssl_client_ca");
|
||||
|
||||
// If cert or key is specified, both must be specified
|
||||
if (sslCertPath_.has_value() || sslKeyPath_.has_value())
|
||||
{
|
||||
if (!sslCertPath_.has_value() || !sslKeyPath_.has_value())
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Both ssl_cert and ssl_key must be specified for gRPC TLS";
|
||||
Throw<std::runtime_error>("Incomplete TLS configuration for gRPC");
|
||||
}
|
||||
JLOG(journal_.info()) << "gRPC TLS enabled with certificate: " << *sslCertPath_;
|
||||
}
|
||||
|
||||
// Validate TLS configuration consistency: ssl_cert_chain only makes sense when TLS is
|
||||
// enabled
|
||||
if (sslCertChainPath_.has_value() &&
|
||||
(!sslCertPath_.has_value() || !sslKeyPath_.has_value()))
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "ssl_cert_chain specified for gRPC without both ssl_cert and ssl_key; "
|
||||
<< "this is an invalid TLS configuration";
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid gRPC TLS configuration: ssl_cert_chain requires both ssl_cert and "
|
||||
"ssl_key");
|
||||
}
|
||||
|
||||
// Validate TLS configuration consistency: ssl_client_ca only makes sense when TLS is
|
||||
// enabled
|
||||
if (sslClientCAPath_.has_value() && (!sslCertPath_.has_value() || !sslKeyPath_.has_value()))
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "ssl_client_ca specified for gRPC without both ssl_cert and ssl_key; "
|
||||
<< "this is an invalid TLS configuration";
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid gRPC TLS configuration: ssl_client_ca requires both ssl_cert and ssl_key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +602,104 @@ GRPCServerImpl::setupListeners()
|
||||
return requests;
|
||||
}
|
||||
|
||||
std::shared_ptr<grpc::ServerCredentials>
|
||||
GRPCServerImpl::createServerCredentials()
|
||||
{
|
||||
if (not sslCertPath_.has_value() or not sslKeyPath_.has_value())
|
||||
{
|
||||
JLOG(journal_.info()) << "Configuring gRPC server without TLS";
|
||||
return grpc::InsecureServerCredentials();
|
||||
}
|
||||
|
||||
JLOG(journal_.info()) << "Configuring gRPC server with TLS";
|
||||
|
||||
try
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
grpc::SslServerCredentialsOptions sslOpts;
|
||||
grpc::SslServerCredentialsOptions::PemKeyCertPair keyCertPair;
|
||||
|
||||
std::string const certContents = getFileContents(ec, *sslCertPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error()) << "Failed to read gRPC SSL certificate file: " << *sslCertPath_
|
||||
<< " - " << ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string const keyContents = getFileContents(ec, *sslKeyPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error()) << "Failed to read gRPC SSL key file: " << *sslKeyPath_ << " - "
|
||||
<< ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
keyCertPair.private_key = keyContents;
|
||||
|
||||
// Read intermediate CA certificates for server certificate chain (optional)
|
||||
std::string certChainContents;
|
||||
if (sslCertChainPath_.has_value())
|
||||
{
|
||||
certChainContents = getFileContents(ec, *sslCertChainPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Failed to read gRPC SSL cert chain file: " << *sslCertChainPath_ << " - "
|
||||
<< ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Read CA certificate for client verification (mTLS, optional)
|
||||
if (sslClientCAPath_.has_value())
|
||||
{
|
||||
auto const clientCAContents = getFileContents(ec, *sslClientCAPath_);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Failed to read gRPC SSL client CA file: " << *sslClientCAPath_ << " - "
|
||||
<< ec.message(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (clientCAContents.empty())
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Empty/truncated gRPC SSL client CA file: " << *sslClientCAPath_
|
||||
<< " - failed to configure mutual TLS"; // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
sslOpts.pem_root_certs = clientCAContents;
|
||||
sslOpts.client_certificate_request =
|
||||
GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY;
|
||||
JLOG(journal_.info()) << "gRPC mutual TLS enabled - client certificates will be "
|
||||
"required and verified";
|
||||
}
|
||||
|
||||
// Combine server cert with intermediate CA certs for complete chain
|
||||
keyCertPair.cert_chain = certContents;
|
||||
if (!certChainContents.empty())
|
||||
{
|
||||
keyCertPair.cert_chain += '\n' + certChainContents;
|
||||
JLOG(journal_.info()) << "gRPC server certificate chain configured with "
|
||||
"intermediate CA certificates"; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
sslOpts.pem_key_cert_pairs.push_back(keyCertPair);
|
||||
|
||||
JLOG(journal_.info()) << "gRPC TLS credentials configured successfully"; // LCOV_EXCL_LINE
|
||||
return grpc::SslServerCredentials(sslOpts);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(journal_.error()) << "Exception while configuring gRPC TLS: "
|
||||
<< e.what(); // LCOV_EXCL_LINE
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
GRPCServerImpl::start()
|
||||
{
|
||||
@@ -565,24 +707,63 @@ GRPCServerImpl::start()
|
||||
if (serverAddress_.empty())
|
||||
return false;
|
||||
|
||||
JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_;
|
||||
// Determine TLS mode for logging
|
||||
bool const tlsEnabled = sslCertPath_.has_value() && sslKeyPath_.has_value();
|
||||
bool const mtlsEnabled = tlsEnabled && sslClientCAPath_.has_value();
|
||||
|
||||
std::string tlsMode = "without TLS";
|
||||
if (mtlsEnabled)
|
||||
{
|
||||
tlsMode = "with mutual TLS (mTLS)";
|
||||
}
|
||||
else if (tlsEnabled)
|
||||
{
|
||||
tlsMode = "with TLS";
|
||||
}
|
||||
|
||||
JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_ << " "
|
||||
<< tlsMode; // LCOV_EXCL_LINE
|
||||
|
||||
grpc::ServerBuilder builder;
|
||||
|
||||
// Listen on the given address without any authentication mechanism.
|
||||
// Actually binded port will be returned into "port" variable.
|
||||
int port = 0;
|
||||
builder.AddListeningPort(serverAddress_, grpc::InsecureServerCredentials(), &port);
|
||||
|
||||
// Create credentials (TLS or insecure) based on configuration
|
||||
auto credentials = createServerCredentials();
|
||||
if (!credentials)
|
||||
{
|
||||
JLOG(journal_.error()) << "Failed to create gRPC server credentials for " << serverAddress_
|
||||
<< " (TLS mode: " << tlsMode
|
||||
<< ") - server will not start"; // LCOV_EXCL_LINE
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add listening port with appropriate credentials
|
||||
builder.AddListeningPort(serverAddress_, credentials, &port);
|
||||
|
||||
// Register "service_" as the instance through which we'll communicate with
|
||||
// clients. In this case it corresponds to an *asynchronous* service.
|
||||
builder.RegisterService(&service_);
|
||||
|
||||
// Get hold of the completion queue used for the asynchronous communication
|
||||
// with the gRPC runtime.
|
||||
cq_ = builder.AddCompletionQueue();
|
||||
|
||||
// Finally assemble the server.
|
||||
server_ = builder.BuildAndStart();
|
||||
serverPort_ = static_cast<std::uint16_t>(port);
|
||||
|
||||
if (serverPort_ != 0u)
|
||||
{
|
||||
JLOG(journal_.info()) << "gRPC server started successfully on port " << serverPort_;
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(journal_.error())
|
||||
<< "Failed to start gRPC server at " << serverAddress_ << " (TLS mode: " << tlsMode
|
||||
<< "); Possible causes: address already in use, invalid address format, or permission "
|
||||
"denied"; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return static_cast<bool>(serverPort_);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ private:
|
||||
|
||||
std::vector<boost::asio::ip::address> secureGatewayIPs_;
|
||||
|
||||
// TLS certificate paths
|
||||
std::optional<std::string> sslCertPath_;
|
||||
std::optional<std::string> sslKeyPath_;
|
||||
std::optional<std::string> sslCertChainPath_; // Intermediate CA certs for server cert chain
|
||||
std::optional<std::string>
|
||||
sslClientCAPath_; // CA cert for client certificate verification (mTLS)
|
||||
|
||||
beast::Journal journal_;
|
||||
|
||||
// typedef for function to bind a listener
|
||||
@@ -124,6 +131,10 @@ public:
|
||||
getEndpoint() const;
|
||||
|
||||
private:
|
||||
// Create server credentials (TLS or insecure) based on configuration
|
||||
std::shared_ptr<grpc::ServerCredentials>
|
||||
createServerCredentials();
|
||||
|
||||
// Class encompassing the state and logic needed to serve a request.
|
||||
template <class Request, class Response>
|
||||
class CallData : public Processor,
|
||||
|
||||
@@ -292,6 +292,17 @@ SHAMapStoreImp::run()
|
||||
bool const readyToRotate = validatedSeq >= lastRotated + deleteInterval_ &&
|
||||
canDelete_ >= lastRotated - 1 && healthWait() == keepGoing;
|
||||
|
||||
JLOG(journal_.debug()) << "run: Setting lastGoodValidatedLedger_ to " << validatedSeq;
|
||||
|
||||
{
|
||||
// Note that this is set after the healthWait() check, so that we
|
||||
// don't start the rotation until the validated ledger is fully
|
||||
// processed. It is not guaranteed to be done at this point. It also
|
||||
// allows the testLedgerGaps unit test to work.
|
||||
std::unique_lock<std::mutex> const lock(mutex_);
|
||||
lastGoodValidatedLedger_ = validatedSeq;
|
||||
}
|
||||
|
||||
// will delete up to (not including) lastRotated
|
||||
if (readyToRotate)
|
||||
{
|
||||
@@ -299,7 +310,8 @@ SHAMapStoreImp::run()
|
||||
<< lastRotated << " deleteInterval " << deleteInterval_
|
||||
<< " canDelete_ " << canDelete_ << " state "
|
||||
<< app_.getOPs().strOperatingMode(false) << " age "
|
||||
<< ledgerMaster_->getValidatedLedgerAge().count() << 's';
|
||||
<< ledgerMaster_->getValidatedLedgerAge().count()
|
||||
<< "s. Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
|
||||
|
||||
clearPrior(lastRotated);
|
||||
if (healthWait() == stopping)
|
||||
@@ -359,7 +371,9 @@ SHAMapStoreImp::run()
|
||||
clearCaches(validatedSeq);
|
||||
});
|
||||
|
||||
JLOG(journal_.warn()) << "finished rotation " << validatedSeq;
|
||||
JLOG(journal_.warn()) << "finished rotation. validatedSeq: " << validatedSeq
|
||||
<< ", lastRotated: " << lastRotated
|
||||
<< ". Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,22 +613,41 @@ SHAMapStoreImp::clearPrior(LedgerIndex lastRotated)
|
||||
SHAMapStoreImp::HealthResult
|
||||
SHAMapStoreImp::healthWait()
|
||||
{
|
||||
auto index = ledgerMaster_->getValidLedgerIndex();
|
||||
auto age = ledgerMaster_->getValidatedLedgerAge();
|
||||
OperatingMode mode = netOPs_->getOperatingMode();
|
||||
std::unique_lock lock(mutex_);
|
||||
while (!stop_ && (mode != OperatingMode::FULL || age > ageThreshold_))
|
||||
|
||||
auto numMissing =
|
||||
ledgerMaster_->missingFromCompleteLedgerRange(lastGoodValidatedLedger_, index);
|
||||
while (!stop_ && (mode != OperatingMode::FULL || age > ageThreshold_ || numMissing > 0))
|
||||
{
|
||||
// this value shouldn't change, so grab it while we have the
|
||||
// lock
|
||||
auto const lowerBound = lastGoodValidatedLedger_;
|
||||
|
||||
lock.unlock();
|
||||
JLOG(journal_.warn()) << "Waiting " << recoveryWaitTime_.count()
|
||||
<< "s for node to stabilize. state: "
|
||||
<< app_.getOPs().strOperatingMode(mode, false) << ". age "
|
||||
<< age.count() << 's';
|
||||
|
||||
auto const stream =
|
||||
mode != OperatingMode::FULL || age > ageThreshold_ ? journal_.warn() : journal_.info();
|
||||
JLOG(stream) << "Waiting " << recoveryWaitTime_.count()
|
||||
<< "s for node to stabilize. state: "
|
||||
<< app_.getOPs().strOperatingMode(mode, false) << ". age " << age.count()
|
||||
<< "s. Missing ledgers: " << numMissing << ". Expect: " << lowerBound << "-"
|
||||
<< index << ". Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
|
||||
std::this_thread::sleep_for(recoveryWaitTime_);
|
||||
index = ledgerMaster_->getValidLedgerIndex();
|
||||
age = ledgerMaster_->getValidatedLedgerAge();
|
||||
mode = netOPs_->getOperatingMode();
|
||||
numMissing =
|
||||
lowerBound == 0 ? 0 : ledgerMaster_->missingFromCompleteLedgerRange(lowerBound, index);
|
||||
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
JLOG(journal_.debug()) << "healthWait: Setting lastGoodValidatedLedger_ to " << index;
|
||||
lastGoodValidatedLedger_ = index;
|
||||
|
||||
return stop_ ? stopping : keepGoing;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@ private:
|
||||
std::thread thread_;
|
||||
bool stop_ = false;
|
||||
bool healthy_ = true;
|
||||
// Used to prevent ledger gaps from forming during online deletion. Keeps
|
||||
// track of the last validated ledger that was processed without gaps. There
|
||||
// are no guarantees about gaps while online delete is not running. For
|
||||
// that, use advisory_delete and check for gaps externally.
|
||||
LedgerIndex lastGoodValidatedLedger_ = 0;
|
||||
mutable std::condition_variable cond_;
|
||||
mutable std::condition_variable rendezvous_;
|
||||
mutable std::mutex mutex_;
|
||||
@@ -83,11 +88,11 @@ private:
|
||||
std::uint32_t deleteBatch_ = 100;
|
||||
std::chrono::milliseconds backOff_{100};
|
||||
std::chrono::seconds ageThreshold_{60};
|
||||
/// If the node is out of sync during an online_delete healthWait()
|
||||
/// call, sleep the thread for this time, and continue checking until
|
||||
/// recovery.
|
||||
/// If the node is out of sync, or any recent ledgers are not
|
||||
/// available during an online_delete healthWait() call, sleep
|
||||
/// the thread for this time, and continue checking until recovery.
|
||||
/// See also: "recovery_wait_seconds" in xrpld-example.cfg
|
||||
std::chrono::seconds recoveryWaitTime_{5};
|
||||
std::chrono::seconds recoveryWaitTime_{1};
|
||||
|
||||
// these do not exist upon SHAMapStore creation, but do exist
|
||||
// as of run() or before
|
||||
@@ -204,6 +209,8 @@ private:
|
||||
enum HealthResult { stopping, keepGoing };
|
||||
[[nodiscard]] HealthResult
|
||||
healthWait();
|
||||
bool
|
||||
hasCompleteRange(LedgerIndex first, LedgerIndex last);
|
||||
|
||||
public:
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user