mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-07 18:56:47 +00:00
519 lines
15 KiB
C++
519 lines
15 KiB
C++
#include <test/jtx/CaptureLogs.h>
|
|
#include <test/jtx/Env.h>
|
|
#include <test/jtx/envconfig.h>
|
|
#include <test/unit_test/SuiteJournal.h>
|
|
|
|
#include <xrpld/core/Config.h>
|
|
#include <xrpld/core/ConfigSections.h>
|
|
|
|
#include <xrpl/beast/rfc2616.h>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/server/Handoff.h>
|
|
#include <xrpl/server/Port.h>
|
|
#include <xrpl/server/Server.h>
|
|
#include <xrpl/server/Session.h>
|
|
#include <xrpl/server/WSSession.h>
|
|
#include <xrpl/server/detail/ServerImpl.h>
|
|
|
|
#include <boost/asio/buffer.hpp>
|
|
#include <boost/asio/executor_work_guard.hpp>
|
|
#include <boost/asio/io_context.hpp>
|
|
#include <boost/asio/ip/address.hpp>
|
|
#include <boost/asio/ip/tcp.hpp>
|
|
#include <boost/asio/streambuf.hpp>
|
|
#include <boost/beast/core/tcp_stream.hpp>
|
|
#include <boost/beast/ssl/ssl_stream.hpp>
|
|
#include <boost/system/detail/error_code.hpp>
|
|
|
|
#include <chrono>
|
|
#include <exception>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <ostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl::test {
|
|
|
|
using socket_type = boost::beast::tcp_stream;
|
|
using stream_type = boost::beast::ssl_stream<socket_type>;
|
|
|
|
class Server_test : public beast::unit_test::Suite
|
|
{
|
|
public:
|
|
class TestThread
|
|
{
|
|
private:
|
|
boost::asio::io_context ioContext_;
|
|
std::optional<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>>
|
|
work_;
|
|
std::thread thread_;
|
|
|
|
public:
|
|
TestThread()
|
|
: work_(std::in_place, boost::asio::make_work_guard(ioContext_))
|
|
, thread_([&]() { this->ioContext_.run(); })
|
|
{
|
|
}
|
|
|
|
~TestThread()
|
|
{
|
|
work_.reset();
|
|
thread_.join();
|
|
}
|
|
|
|
boost::asio::io_context&
|
|
getIoContext()
|
|
{
|
|
return ioContext_;
|
|
}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
class TestSink : public beast::Journal::Sink
|
|
{
|
|
beast::unit_test::Suite& suite_;
|
|
|
|
public:
|
|
explicit TestSink(beast::unit_test::Suite& suite)
|
|
: Sink(beast::Severity::Warning, false), suite_(suite)
|
|
{
|
|
}
|
|
|
|
void
|
|
write(beast::Severity level, std::string const& text) override
|
|
{
|
|
if (level < threshold())
|
|
return;
|
|
|
|
suite_.log << text << std::endl;
|
|
}
|
|
|
|
void
|
|
writeAlways(beast::Severity level, std::string const& text) override
|
|
{
|
|
suite_.log << text << std::endl;
|
|
}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
struct TestHandler
|
|
{
|
|
static bool
|
|
onAccept(Session& session, boost::asio::ip::tcp::endpoint endpoint)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static Handoff
|
|
onHandoff(
|
|
Session& session,
|
|
std::unique_ptr<stream_type> const& bundle,
|
|
http_request_type const& request,
|
|
boost::asio::ip::tcp::endpoint remoteAddress)
|
|
{
|
|
return Handoff{};
|
|
}
|
|
|
|
static Handoff
|
|
onHandoff(
|
|
Session& session,
|
|
http_request_type const& request,
|
|
boost::asio::ip::tcp::endpoint remoteAddress)
|
|
{
|
|
return Handoff{};
|
|
}
|
|
|
|
static void
|
|
onRequest(Session& session)
|
|
{
|
|
session.write(std::string("Hello, world!\n"));
|
|
if (beast::rfc2616::isKeepAlive(session.request()))
|
|
{
|
|
session.complete();
|
|
}
|
|
else
|
|
{
|
|
session.close(true);
|
|
}
|
|
}
|
|
|
|
void
|
|
onWSMessage(
|
|
std::shared_ptr<WSSession> session,
|
|
std::vector<boost::asio::const_buffer> const&)
|
|
{
|
|
}
|
|
|
|
void
|
|
onClose(Session& session, boost::system::error_code const&)
|
|
{
|
|
}
|
|
|
|
void
|
|
onStopped(Server& server)
|
|
{
|
|
}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
// Connect to an address
|
|
template <class Socket>
|
|
bool
|
|
connect(Socket& s, typename Socket::endpoint_type const& ep)
|
|
{
|
|
try
|
|
{
|
|
s.connect(ep);
|
|
pass();
|
|
return true;
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
fail(e.what());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Write a string to the stream
|
|
template <class SyncWriteStream>
|
|
bool
|
|
write(SyncWriteStream& s, std::string const& text)
|
|
{
|
|
try
|
|
{
|
|
boost::asio::write(s, boost::asio::buffer(text));
|
|
pass();
|
|
return true;
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
fail(e.what());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Expect that reading the stream produces a matching string
|
|
template <class SyncReadStream>
|
|
bool
|
|
expectRead(SyncReadStream& s, std::string const& match)
|
|
{
|
|
boost::asio::streambuf b(1000); // limit on read
|
|
try
|
|
{
|
|
auto const n = boost::asio::read_until(s, b, '\n');
|
|
if (BEAST_EXPECT(n == match.size()))
|
|
{
|
|
std::string got;
|
|
got.resize(n);
|
|
boost::asio::buffer_copy(boost::asio::buffer(&got[0], n), b.data());
|
|
return BEAST_EXPECT(got == match);
|
|
}
|
|
}
|
|
catch (std::length_error const& e)
|
|
{
|
|
fail(e.what());
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
fail(e.what());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
testRequest(boost::asio::ip::tcp::endpoint const& ep)
|
|
{
|
|
boost::asio::io_context ios;
|
|
using socket = boost::asio::ip::tcp::socket;
|
|
socket s(ios);
|
|
|
|
if (!connect(s, ep))
|
|
return;
|
|
|
|
if (!write(
|
|
s,
|
|
"GET / HTTP/1.1\r\n"
|
|
"Connection: close\r\n"
|
|
"\r\n"))
|
|
return;
|
|
|
|
if (!expectRead(s, "Hello, world!\n"))
|
|
return;
|
|
|
|
boost::system::error_code ec;
|
|
s.shutdown(socket::shutdown_both, ec); // NOLINT(bugprone-unused-return-value)
|
|
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
}
|
|
|
|
void
|
|
testKeepalive(boost::asio::ip::tcp::endpoint const& ep)
|
|
{
|
|
boost::asio::io_context ios;
|
|
using socket = boost::asio::ip::tcp::socket;
|
|
socket s(ios);
|
|
|
|
if (!connect(s, ep))
|
|
return;
|
|
|
|
if (!write(
|
|
s,
|
|
"GET / HTTP/1.1\r\n"
|
|
"Connection: Keep-Alive\r\n"
|
|
"\r\n"))
|
|
return;
|
|
|
|
if (!expectRead(s, "Hello, world!\n"))
|
|
return;
|
|
|
|
if (!write(
|
|
s,
|
|
"GET / HTTP/1.1\r\n"
|
|
"Connection: close\r\n"
|
|
"\r\n"))
|
|
return;
|
|
|
|
if (!expectRead(s, "Hello, world!\n"))
|
|
return;
|
|
|
|
boost::system::error_code ec;
|
|
s.shutdown(socket::shutdown_both, ec); // NOLINT(bugprone-unused-return-value)
|
|
}
|
|
|
|
void
|
|
basicTests()
|
|
{
|
|
testcase("Basic client/server");
|
|
TestSink sink{*this};
|
|
TestThread thread;
|
|
sink.threshold(beast::Severity::All);
|
|
beast::Journal const journal{sink};
|
|
TestHandler handler;
|
|
auto s = makeServer(handler, thread.getIoContext(), journal);
|
|
std::vector<Port> serverPort(1);
|
|
serverPort.back().ip = boost::asio::ip::make_address(getEnvLocalhostAddr()),
|
|
serverPort.back().port = 0;
|
|
serverPort.back().protocol.insert("http");
|
|
auto eps = s->ports(serverPort);
|
|
testRequest(eps.begin()->second);
|
|
testKeepalive(eps.begin()->second);
|
|
// s->close();
|
|
s = nullptr;
|
|
pass();
|
|
}
|
|
|
|
void
|
|
stressTest()
|
|
{
|
|
testcase("stress test");
|
|
struct NullHandler
|
|
{
|
|
static bool
|
|
onAccept(Session& session, boost::asio::ip::tcp::endpoint endpoint)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static Handoff
|
|
onHandoff(
|
|
Session& session,
|
|
std::unique_ptr<stream_type> const& bundle,
|
|
http_request_type const& request,
|
|
boost::asio::ip::tcp::endpoint remoteAddress)
|
|
{
|
|
return Handoff{};
|
|
}
|
|
|
|
static Handoff
|
|
onHandoff(
|
|
Session& session,
|
|
http_request_type const& request,
|
|
boost::asio::ip::tcp::endpoint remoteAddress)
|
|
{
|
|
return Handoff{};
|
|
}
|
|
|
|
void
|
|
onRequest(Session& session)
|
|
{
|
|
}
|
|
|
|
void
|
|
onWSMessage(
|
|
std::shared_ptr<WSSession> session,
|
|
std::vector<boost::asio::const_buffer> const& buffers)
|
|
{
|
|
}
|
|
|
|
void
|
|
onClose(Session& session, boost::system::error_code const&)
|
|
{
|
|
}
|
|
|
|
void
|
|
onStopped(Server& server)
|
|
{
|
|
}
|
|
};
|
|
|
|
using beast::Severity;
|
|
SuiteJournal journal("Server_test", *this);
|
|
|
|
NullHandler h;
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
TestThread thread;
|
|
auto s = makeServer(h, thread.getIoContext(), journal);
|
|
std::vector<Port> serverPort(1);
|
|
serverPort.back().ip = boost::asio::ip::make_address(getEnvLocalhostAddr()),
|
|
serverPort.back().port = 0;
|
|
serverPort.back().protocol.insert("http");
|
|
s->ports(serverPort);
|
|
}
|
|
pass();
|
|
}
|
|
|
|
void
|
|
testBadConfig()
|
|
{
|
|
testcase("Server config - invalid options");
|
|
using namespace test::jtx;
|
|
|
|
std::string messages;
|
|
|
|
except([&] {
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
(*cfg).deprecatedClearSection("port_rpc");
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(messages.find("Missing 'ip' in [port_rpc]") != std::string::npos);
|
|
|
|
except([&] {
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
(*cfg).deprecatedClearSection("port_rpc");
|
|
(*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(messages.find("Missing 'port' in [port_rpc]") != std::string::npos);
|
|
|
|
except([&] {
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
(*cfg).deprecatedClearSection("port_rpc");
|
|
(*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
|
|
(*cfg)["port_rpc"].set("port", "0");
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(
|
|
messages.find("Invalid value '0' for key 'port' in [port_rpc]") == std::string::npos);
|
|
|
|
except([&] {
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
(*cfg)["server"].set("port", "0");
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(
|
|
messages.find("Invalid value '0' for key 'port' in [server]") != std::string::npos);
|
|
|
|
except([&] {
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
(*cfg).deprecatedClearSection("port_rpc");
|
|
(*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
|
|
(*cfg)["port_rpc"].set("port", "8081");
|
|
(*cfg)["port_rpc"].set("protocol", "");
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(messages.find("Missing 'protocol' in [port_rpc]") != std::string::npos);
|
|
|
|
except([&] // this creates a standard test config without the server
|
|
// section
|
|
{
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
cfg = std::make_unique<Config>();
|
|
cfg->overwrite(ConfigSection::nodeDatabase(), "type", "memory");
|
|
cfg->overwrite(ConfigSection::nodeDatabase(), "path", "main");
|
|
cfg->deprecatedClearSection(ConfigSection::importNodeDatabase());
|
|
cfg->legacy("database_path", "");
|
|
cfg->setupControl(true, true, true);
|
|
(*cfg)["port_peer"].set("ip", getEnvLocalhostAddr());
|
|
(*cfg)["port_peer"].set("port", "8080");
|
|
(*cfg)["port_peer"].set("protocol", "peer");
|
|
(*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
|
|
(*cfg)["port_rpc"].set("port", "8081");
|
|
(*cfg)["port_rpc"].set("protocol", "http,ws2");
|
|
(*cfg)["port_rpc"].set("admin", getEnvLocalhostAddr());
|
|
(*cfg)["port_ws"].set("ip", getEnvLocalhostAddr());
|
|
(*cfg)["port_ws"].set("port", "8082");
|
|
(*cfg)["port_ws"].set("protocol", "ws");
|
|
(*cfg)["port_ws"].set("admin", getEnvLocalhostAddr());
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(messages.find("Required section [server] is missing") != std::string::npos);
|
|
|
|
except([&] // this creates a standard test config without some of the
|
|
// port sections
|
|
{
|
|
Env const env{
|
|
*this,
|
|
envconfig([](std::unique_ptr<Config> cfg) {
|
|
cfg = std::make_unique<Config>();
|
|
cfg->overwrite(ConfigSection::nodeDatabase(), "type", "memory");
|
|
cfg->overwrite(ConfigSection::nodeDatabase(), "path", "main");
|
|
cfg->deprecatedClearSection(ConfigSection::importNodeDatabase());
|
|
cfg->legacy("database_path", "");
|
|
cfg->setupControl(true, true, true);
|
|
(*cfg)["server"].append("port_peer");
|
|
(*cfg)["server"].append("port_rpc");
|
|
(*cfg)["server"].append("port_ws");
|
|
return cfg;
|
|
}),
|
|
std::make_unique<CaptureLogs>(&messages)};
|
|
});
|
|
BEAST_EXPECT(messages.find("Missing section: [port_peer]") != std::string::npos);
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
basicTests();
|
|
stressTest();
|
|
testBadConfig();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Server, server, xrpl);
|
|
|
|
} // namespace xrpl::test
|