Files
xahaud/src/test/jtx/impl/JSONRPCClient.cpp
T
Niq DudfieldandAlex Kremer 21fab66a17 build: bump boost to 1.91.0 and fix beast/asio timeout bugs (#812)
Cherry-picking XRPLF/rippled 1506e65558

Co-authored-by: Alex Kremer <[email protected]>
2026-10-01 15:32:18 +10:00

217 lines
6.8 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2016 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/JSONRPCClient.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/Port.h>
#include <boost/asio.hpp>
#include <boost/beast/http/dynamic_body.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/write.hpp>
#include <string>
namespace ripple {
namespace test {
class JSONRPCClient : public AbstractClient
{
static boost::asio::ip::tcp::endpoint
getEndpoint(BasicConfig const& cfg)
{
auto& log = std::cerr;
ParsedPort common;
parse_Port(common, cfg["server"], log);
for (auto const& name : cfg.section("server").values())
{
if (!cfg.exists(name))
continue;
ParsedPort pp;
parse_Port(pp, cfg[name], log);
if (pp.protocol.count("http") == 0)
continue;
using namespace boost::asio::ip;
if (pp.ip && pp.ip->is_unspecified())
*pp.ip = pp.ip->is_v6() ? address{address_v6::loopback()}
: address{address_v4::loopback()};
if (!pp.port)
Throw<std::runtime_error>("Use fixConfigPorts with auto ports");
return {*pp.ip, *pp.port};
}
Throw<std::runtime_error>("Missing HTTP port");
return {}; // Silence compiler control paths return value warning
}
template <class ConstBufferSequence>
static std::string
buffer_string(ConstBufferSequence const& b)
{
using namespace boost::asio;
std::string s;
s.resize(buffer_size(b));
buffer_copy(buffer(&s[0], s.size()), b);
return s;
}
boost::asio::ip::tcp::endpoint ep_;
boost::asio::io_context ios_;
boost::asio::ip::tcp::socket stream_;
boost::beast::multi_buffer bin_;
boost::beast::multi_buffer bout_;
unsigned rpc_version_;
unsigned reconnects_ = 0;
// Errors meaning the server closed the keep-alive connection, rather
// than a failed request, so the request can be sent again on a fresh one.
static bool
droppedConnection(boost::system::error_code const& ec)
{
namespace error = boost::asio::error;
return ec == boost::beast::http::error::end_of_stream ||
ec == error::eof || ec == error::connection_reset ||
ec == error::connection_aborted || ec == error::broken_pipe ||
ec == error::not_connected;
}
// Replace the dropped connection, discarding any bytes buffered from it.
void
reconnect()
{
boost::system::error_code ec;
stream_.close(ec);
bin_.clear();
stream_.connect(ep_);
++reconnects_;
}
public:
explicit JSONRPCClient(Config const& cfg, unsigned rpc_version)
: ep_(getEndpoint(cfg)), stream_(ios_), rpc_version_(rpc_version)
{
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
error
result
*/
Json::Value
invoke(std::string const& cmd, Json::Value const& params) override
{
using namespace boost::beast::http;
using namespace boost::asio;
using namespace std::string_literals;
request<string_body> req;
req.method(boost::beast::http::verb::post);
req.target("/");
req.version(11);
req.insert("Content-Type", "application/json; charset=UTF-8");
{
std::ostringstream ostr;
ostr << ep_;
req.insert("Host", ostr.str());
}
{
Json::Value jr;
jr[jss::method] = cmd;
if (rpc_version_ == 2)
{
jr[jss::jsonrpc] = "2.0";
jr[jss::ripplerpc] = "2.0";
jr[jss::id] = 5;
}
if (params)
{
Json::Value& ja = jr[jss::params] = Json::arrayValue;
ja.append(params);
}
req.body() = to_string(jr);
}
req.prepare_payload();
// The connection is opened at construction and kept alive, but the
// server closes it when the first request takes longer than
// BaseHTTPPeer::timeoutSecondsLocal to arrive (test setup on a loaded
// machine can), or when it idles past timeoutSeconds between requests.
// If the server has closed it, reconnect and send the request once
// more.
response<dynamic_body> res;
auto writeAndRead = [&] {
write(stream_, req);
read(stream_, bin_, res);
};
try
{
writeAndRead();
}
catch (boost::system::system_error const& e)
{
if (!droppedConnection(e.code()))
throw;
reconnect();
res = {};
writeAndRead();
}
Json::Reader jr;
Json::Value jv;
jr.parse(buffer_string(res.body().data()), jv);
if (jv["result"].isMember("error"))
jv["error"] = jv["result"]["error"];
if (jv["result"].isMember("status"))
jv["status"] = jv["result"]["status"];
return jv;
}
unsigned
version() const override
{
return rpc_version_;
}
unsigned
reconnects() const override
{
return reconnects_;
}
};
std::unique_ptr<AbstractClient>
makeJSONRPCClient(Config const& cfg, unsigned rpc_version)
{
return std::make_unique<JSONRPCClient>(cfg, rpc_version);
}
} // namespace test
} // namespace ripple