Files
rippled/src/xrpld/app/main/GRPCServer.cpp
Pratik Mankawde 3715b7a2a3 fix(telemetry): correct RPC and gRPC span status reporting
Six related defects in the RPC/gRPC span surface, all cases where a failure
was recorded as success or an attribute was missing on an error path.

GRPCServer: the non-exception branch set the span Ok unconditionally, then
sent a possibly-failed grpc::Status. The handler can return a non-OK status
without throwing, so every failed call traced as successful. Status now
follows result.second, with the error message as the span description.

ServerHandler: eight per-item error branches appended an error reply without
recording that the request failed. Batch responses and ripplerpc < 3.0 always
carry HTTP 200, so those failures were invisible and an entirely failed batch
ended its span as successful. Added an appendItemError() helper next to the
existing httpReplyError() lambda and routed all eight sites through it, so the
flag cannot be forgotten at a new call site.

ServerHandler: the early-return validation paths set the span error but not the
rpc_status attribute. Added it to httpReplyError() so every such path gets it.

RPCHandler: the fillHandler error path set only command and rpc_status, while
callMethod sets command, version and rpc_role. Error spans were therefore not
filterable by API version or role. The error path now mirrors that set.

RPCHandler: resolveCommandSpanName() checked only that command/method were
present, not that they agreed, while fillHandler rejects a mismatch as
rpcUNKNOWN_COMMAND. A request supplying both with different values was labelled
with one of the two names, misattributing the error to a command that never
dispatched. It now mirrors fillHandler's rule and collapses to "unknown".

ServerHandler: processRequest returned bool solely so the caller could set its
span status. Telemetry should read state, not shape the signature of the code it
observes, so the signature returns to void and rpc.process sets its own status
from spanHadError. The enclosing rpc.http_request span now leaves status unset:
the OTel spec has instrumentation leave status unset unless the operation itself
errored, and reserves Ok for an operator asserting verified success.
2026-07-29 16:02:19 +01:00

855 lines
29 KiB
C++

#include <xrpld/app/main/GRPCServer.h>
#include <xrpld/app/ledger/LedgerMaster.h> // IWYU pragma: keep
#include <xrpld/app/main/Application.h>
#include <xrpld/app/main/GrpcSpanNames.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/core/CurrentThreadName.h>
#include <xrpl/beast/net/IPAddressConversion.h>
#include <xrpl/beast/net/IPEndpoint.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/config/Constants.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/server/InfoSub.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <boost/algorithm/string/trim.hpp>
#include <boost/asio/ip/address.hpp>
#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>
#include <grpcpp/support/status.h>
#include <org/xrpl/rpc/v1/get_ledger.pb.h>
#include <org/xrpl/rpc/v1/get_ledger_data.pb.h>
#include <org/xrpl/rpc/v1/get_ledger_diff.pb.h>
#include <org/xrpl/rpc/v1/get_ledger_entry.pb.h>
#include <org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace xrpl {
namespace {
// helper function. converts string to endpoint. handles ipv4 and ipv6, with or
// without port, with or without prepended scheme
std::optional<boost::asio::ip::tcp::endpoint>
getEndpoint(std::string const& peer)
{
try
{
std::size_t const first = peer.find_first_of(':');
std::size_t const last = peer.find_last_of(':');
std::string peerClean(peer);
if (first != last)
{
peerClean = peer.substr(first + 1);
}
std::optional<beast::IP::Endpoint> endpoint =
beast::IP::Endpoint::fromStringChecked(peerClean);
if (endpoint)
return beast::IP::toAsioEndpoint(endpoint.value());
}
catch (std::exception const&) // NOLINT(bugprone-empty-catch)
{
}
return {};
}
} // namespace
template <class Request, class Response>
GRPCServerImpl::CallData<Request, Response>::CallData(
org::xrpl::rpc::v1::XRPLedgerAPIService::AsyncService& service,
grpc::ServerCompletionQueue& cq,
Application& app,
BindListener<Request, Response> bindListener,
Handler<Request, Response> handler,
Forward<Request, Response> forward,
RPC::Condition requiredCondition,
Resource::Charge loadType,
std::vector<boost::asio::ip::address> const& secureGatewayIPs,
std::string_view name)
: service_(service)
, cq_(cq)
, finished_(false)
, app_(app)
, responder_(&ctx_)
, bindListener_(std::move(bindListener))
, handler_(std::move(handler))
, forward_(std::move(forward))
, requiredCondition_(requiredCondition)
, loadType_(std::move(loadType))
, secureGatewayIPs_(secureGatewayIPs)
, name_(name)
{
// Bind a listener. When a request is received, "this" will be returned
// from CompletionQueue::Next
bindListener_(service_, &ctx_, &request_, &responder_, &cq_, &cq_, this);
}
template <class Request, class Response>
std::shared_ptr<Processor>
GRPCServerImpl::CallData<Request, Response>::clone()
{
return std::make_shared<CallData<Request, Response>>(
service_,
cq_,
app_,
bindListener_,
handler_,
forward_,
requiredCondition_,
loadType_,
secureGatewayIPs_,
name_);
}
template <class Request, class Response>
void
GRPCServerImpl::CallData<Request, Response>::process()
{
// sanity check
BOOST_ASSERT(!finished_);
std::shared_ptr<CallData<Request, Response>> const thisShared = this->shared_from_this();
// Need to set finished to true before processing the response,
// because as soon as the response is posted to the completion
// queue (via responder_.Finish(...) or responder_.FinishWithError(...)),
// the CallData object is returned as a tag in handleRpcs().
// handleRpcs() checks the finished variable, and if true, destroys
// the object. Setting finished to true before calling process
// ensures that finished is always true when this CallData object
// is returned as a tag in handleRpcs(), after sending the response
finished_ = true;
auto coro = app_.getJobQueue().postCoro(
JobType::JtRpc, "gRPC-Client", [thisShared](std::shared_ptr<JobQueue::Coro> coro) {
thisShared->process(coro);
});
// If coro is null, then the JobQueue has already been shutdown
if (!coro)
{
grpc::Status const status{grpc::StatusCode::INTERNAL, "Job Queue is already stopped"};
responder_.FinishWithError(status, this);
}
}
template <class Request, class Response>
void
GRPCServerImpl::CallData<Request, Response>::process(std::shared_ptr<JobQueue::Coro> coro)
{
using namespace telemetry;
auto span = SpanGuard::span(TraceCategory::Rpc, grpc_span::prefix::grpc, name_);
span.setAttribute(grpc_span::attr::method, name_);
try
{
auto usage = getUsage();
bool const isUnlimited = clientIsUnlimited();
if (!isUnlimited && usage.disconnect(app_.getJournal("gRPCServer")))
{
span.setAttribute(
grpc_span::attr::grpcStatus, grpc_span::val::error); // LCOV_EXCL_LINE
span.setError(grpc_span::val::resourceExhausted); // LCOV_EXCL_LINE
grpc::Status const status{
grpc::StatusCode::RESOURCE_EXHAUSTED, "usage balance exceeds threshold"};
responder_.FinishWithError(status, this);
}
else
{
auto loadType = getLoadType();
usage.charge(loadType);
auto role = getRole(isUnlimited);
span.setAttribute(
grpc_span::attr::grpcRole,
role == Role::ADMIN ? std::string_view{grpc_span::val::admin}
: std::string_view{grpc_span::val::user});
{
std::stringstream toLog;
toLog << "role = " << (int)role;
toLog << " address = ";
if (auto clientIp = getClientIpAddress())
toLog << clientIp.value();
toLog << " user = ";
if (auto user = getUser())
toLog << user.value();
toLog << " isUnlimited = " << isUnlimited;
JLOG(app_.getJournal("GRPCServer::Calldata").debug()) << toLog.str();
}
RPC::GRPCContext<Request> context{
{app_.getJournal("gRPCServer"),
app_,
loadType,
app_.getOPs(),
app_.getLedgerMaster(),
usage,
role,
coro,
InfoSub::pointer(),
kApiVersion},
request_};
// Make sure we can currently handle the rpc
ErrorCodeI const conditionMetRes = RPC::conditionMet(requiredCondition_, context);
if (conditionMetRes != RpcSuccess)
{
RPC::ErrorInfo const errorInfo = RPC::getErrorInfo(conditionMetRes);
span.setAttribute(
grpc_span::attr::grpcStatus, grpc_span::val::error); // LCOV_EXCL_LINE
span.setError(errorInfo.token.cStr()); // LCOV_EXCL_LINE
grpc::Status const status{
grpc::StatusCode::FAILED_PRECONDITION, errorInfo.message.cStr()};
responder_.FinishWithError(status, this);
}
else
{
std::pair<Response, grpc::Status> result = handler_(context);
setIsUnlimited(result.first, isUnlimited);
// The handler can return a non-OK status without throwing, so
// the span status must follow result.second rather than assume
// success — otherwise every failed call traces as OK.
if (result.second.ok())
{
span.setAttribute(grpc_span::attr::grpcStatus, grpc_span::val::success);
span.setOk();
}
else
{
span.setAttribute(grpc_span::attr::grpcStatus, grpc_span::val::error);
span.setError(result.second.error_message());
}
responder_.Finish(result.first, result.second, this);
}
}
}
catch (std::exception const& ex)
{
span.setAttribute(grpc_span::attr::grpcStatus, grpc_span::val::error); // LCOV_EXCL_LINE
span.recordException(ex); // LCOV_EXCL_LINE
grpc::Status const status{grpc::StatusCode::INTERNAL, ex.what()};
responder_.FinishWithError(status, this);
}
}
template <class Request, class Response>
bool
GRPCServerImpl::CallData<Request, Response>::isFinished()
{
return finished_;
}
template <class Request, class Response>
Resource::Charge
GRPCServerImpl::CallData<Request, Response>::getLoadType()
{
return loadType_;
}
template <class Request, class Response>
Role
GRPCServerImpl::CallData<Request, Response>::getRole(bool isUnlimited)
{
if (isUnlimited)
{
return Role::IDENTIFIED;
}
return Role::USER;
}
template <class Request, class Response>
std::optional<std::string>
GRPCServerImpl::CallData<Request, Response>::getUser()
{
if (auto descriptor = Request::GetDescriptor()->FindFieldByName("user"))
{
std::string user = Request::GetReflection()->GetString(request_, descriptor);
if (!user.empty())
{
return user;
}
}
return {};
}
template <class Request, class Response>
std::optional<boost::asio::ip::address>
GRPCServerImpl::CallData<Request, Response>::getClientIpAddress()
{
auto endpoint = getClientEndpoint();
if (endpoint)
return endpoint->address();
return {};
}
template <class Request, class Response>
std::optional<boost::asio::ip::tcp::endpoint>
GRPCServerImpl::CallData<Request, Response>::getClientEndpoint()
{
return xrpl::getEndpoint(ctx_.peer());
}
template <class Request, class Response>
bool
GRPCServerImpl::CallData<Request, Response>::clientIsUnlimited()
{
if (!getUser())
return false;
auto clientIp = getClientIpAddress();
if (clientIp)
{
for (auto& ip : secureGatewayIPs_)
{
if (ip == clientIp)
return true;
}
}
return false;
}
template <class Request, class Response>
void
GRPCServerImpl::CallData<Request, Response>::setIsUnlimited(Response& response, bool isUnlimited)
{
if (isUnlimited)
{
if (auto descriptor = Response::GetDescriptor()->FindFieldByName("is_unlimited"))
{
Response::GetReflection()->SetBool(&response, descriptor, true);
}
}
}
template <class Request, class Response>
Resource::Consumer
GRPCServerImpl::CallData<Request, Response>::getUsage()
{
auto endpoint = getClientEndpoint();
if (endpoint)
return app_.getResourceManager().newInboundEndpoint(beast::IP::fromAsio(endpoint.value()));
Throw<std::runtime_error>("Failed to get client endpoint");
}
GRPCServerImpl::GRPCServerImpl(Application& app)
: app_(app), journal_(app_.getJournal("gRPC Server"))
{
// if present, get endpoint from config
if (app_.config().exists(Sections::kPortGrpc))
{
Section const& section = app_.config().section(Sections::kPortGrpc);
auto const optIp = section.get(Keys::kIp);
if (!optIp)
return;
auto const optPort = section.get(Keys::kPort);
if (!optPort)
return;
try
{
boost::asio::ip::tcp::endpoint const endpoint(
boost::asio::ip::make_address(*optIp), std::stoi(*optPort));
std::stringstream ss;
ss << endpoint;
serverAddress_ = ss.str();
}
catch (std::exception const&)
{
JLOG(journal_.error()) << "Error setting grpc server address";
Throw<std::runtime_error>("Error setting grpc server address");
}
auto const optSecureGateway = section.get(Keys::kSecureGateway);
if (optSecureGateway)
{
try
{
std::stringstream ss{*optSecureGateway};
std::string ip;
while (std::getline(ss, ip, ','))
{
boost::algorithm::trim(ip);
auto const addr = boost::asio::ip::make_address(ip);
if (addr.is_unspecified())
{
JLOG(journal_.error()) << "Can't pass unspecified IP in "
<< "secure_gateway section of port_grpc";
Throw<std::runtime_error>("Unspecified IP in secure_gateway section");
}
secureGatewayIPs_.emplace_back(addr);
}
}
catch (std::exception const&)
{
JLOG(journal_.error()) << "Error parsing secure gateway IPs for grpc server";
Throw<std::runtime_error>("Error parsing secure_gateway section");
}
}
// Read TLS certificate configuration (optional)
sslCertPath_ = section.get(Keys::kSslCert);
sslKeyPath_ = section.get(Keys::kSslKey);
sslCertChainPath_ = section.get(Keys::kSslCertChain);
sslClientCAPath_ = section.get(Keys::kSslClientCa);
// 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");
}
}
}
void
GRPCServerImpl::shutdown()
{
JLOG(journal_.debug()) << "Shutting down";
// The below call cancels all "listeners" (CallData objects that are waiting
// for a request, as opposed to processing a request), and blocks until all
// requests being processed are completed. CallData objects in the midst of
// processing requests need to actually send data back to the client, via
// responder_.Finish(...) or responder_.FinishWithError(...), for this call
// to unblock. Each cancelled listener is returned via cq_.Next(...) with ok
// set to false
server_->Shutdown();
JLOG(journal_.debug()) << "Server has been shutdown";
// Always shutdown the completion queue after the server. This call allows
// cq_.Next() to return false, once all events posted to the completion
// queue have been processed. See handleRpcs() for more details.
cq_->Shutdown();
JLOG(journal_.debug()) << "Completion Queue has been shutdown";
}
void
GRPCServerImpl::handleRpcs()
{
// This collection should really be an unordered_set. However, to delete
// from the unordered_set, we need a shared_ptr, but cq_.Next() (see below
// while loop) sets the tag to a raw pointer.
std::vector<std::shared_ptr<Processor>> requests = setupListeners();
auto erase = [&requests](Processor* ptr) {
auto it = std::ranges::find_if(
requests, [ptr](std::shared_ptr<Processor>& sPtr) { return sPtr.get() == ptr; });
BOOST_ASSERT(it != requests.end());
it->swap(requests.back());
requests.pop_back();
};
void* tag = nullptr; // uniquely identifies a request.
bool ok = false;
// Block waiting to read the next event from the completion queue. The
// event is uniquely identified by its tag, which in this case is the
// memory address of a CallData instance.
// The return value of Next should always be checked. This return value
// tells us whether there is any kind of event or cq_ is shutting down.
// When cq_.Next(...) returns false, all work has been completed and the
// loop can exit. When the server is shutdown, each CallData object that is
// listening for a request is forcibly cancelled, and is returned by
// cq_->Next() with ok set to false. Then, each CallData object processing
// a request must complete (by sending data to the client), each of which
// will be returned from cq_->Next() with ok set to true. After all
// cancelled listeners and all CallData objects processing requests are
// returned via cq_->Next(), cq_->Next() will return false, causing the
// loop to exit.
while (cq_->Next(&tag, &ok))
{
auto ptr = static_cast<Processor*>(tag);
JLOG(journal_.trace()) << "Processing CallData object."
<< " ptr = " << ptr << " ok = " << ok;
if (!ok)
{
JLOG(journal_.debug()) << "Request listener cancelled. "
<< "Destroying object";
erase(ptr);
}
else
{
if (!ptr->isFinished())
{
JLOG(journal_.debug()) << "Received new request. Processing";
// ptr is now processing a request, so create a new CallData
// object to handle additional requests
auto cloned = ptr->clone();
requests.push_back(cloned);
// process the request
ptr->process();
}
else
{
JLOG(journal_.debug()) << "Sent response. Destroying object";
erase(ptr);
}
}
}
JLOG(journal_.debug()) << "Completion Queue drained";
}
// create a CallData instance for each RPC
std::vector<std::shared_ptr<Processor>>
GRPCServerImpl::setupListeners()
{
using RPC::Condition;
std::vector<std::shared_ptr<Processor>> requests;
auto addToRequests = [&requests](auto callData) { requests.push_back(std::move(callData)); };
{
using cd =
CallData<org::xrpl::rpc::v1::GetLedgerRequest, org::xrpl::rpc::v1::GetLedgerResponse>;
addToRequests(
std::make_shared<cd>(
service_,
*cq_,
app_,
&org::xrpl::rpc::v1::XRPLedgerAPIService::AsyncService::RequestGetLedger,
doLedgerGrpc,
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedger,
Condition::NoCondition,
Resource::kFeeMediumBurdenRpc,
secureGatewayIPs_,
"GetLedger"));
}
{
using cd = CallData<
org::xrpl::rpc::v1::GetLedgerDataRequest,
org::xrpl::rpc::v1::GetLedgerDataResponse>;
addToRequests(
std::make_shared<cd>(
service_,
*cq_,
app_,
&org::xrpl::rpc::v1::XRPLedgerAPIService::AsyncService::RequestGetLedgerData,
doLedgerDataGrpc,
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerData,
Condition::NoCondition,
Resource::kFeeMediumBurdenRpc,
secureGatewayIPs_,
"GetLedgerData"));
}
{
using cd = CallData<
org::xrpl::rpc::v1::GetLedgerDiffRequest,
org::xrpl::rpc::v1::GetLedgerDiffResponse>;
addToRequests(
std::make_shared<cd>(
service_,
*cq_,
app_,
&org::xrpl::rpc::v1::XRPLedgerAPIService::AsyncService::RequestGetLedgerDiff,
doLedgerDiffGrpc,
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerDiff,
Condition::NoCondition,
Resource::kFeeMediumBurdenRpc,
secureGatewayIPs_,
"GetLedgerDiff"));
}
{
using cd = CallData<
org::xrpl::rpc::v1::GetLedgerEntryRequest,
org::xrpl::rpc::v1::GetLedgerEntryResponse>;
addToRequests(
std::make_shared<cd>(
service_,
*cq_,
app_,
&org::xrpl::rpc::v1::XRPLedgerAPIService::AsyncService::RequestGetLedgerEntry,
doLedgerEntryGrpc,
&org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerEntry,
Condition::NoCondition,
Resource::kFeeMediumBurdenRpc,
secureGatewayIPs_,
"GetLedgerEntry"));
}
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()
{
// if config does not specify a grpc server address, don't start
if (serverAddress_.empty())
return false;
// 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;
int port = 0;
// 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_);
}
boost::asio::ip::tcp::endpoint
GRPCServerImpl::getEndpoint() const
{
std::string const addr = serverAddress_.substr(0, serverAddress_.find_last_of(':'));
return boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(addr), serverPort_);
}
bool
GRPCServer::start()
{
// Start the server and setup listeners
if (running_ = impl_.start(); running_)
{
thread_ = std::thread([this]() {
// Start the event loop and begin handling requests
beast::setCurrentThreadName("xrpld: grpc");
this->impl_.handleRpcs();
});
}
return running_;
}
void
GRPCServer::stop()
{
if (running_)
{
impl_.shutdown();
thread_.join();
running_ = false;
}
}
GRPCServer::~GRPCServer()
{
XRPL_ASSERT(!running_, "xrpl::GRPCServer::~GRPCServer : is not running");
}
boost::asio::ip::tcp::endpoint
GRPCServer::getEndpoint() const
{
return impl_.getEndpoint();
}
} // namespace xrpl