#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::test { using socket_type = boost::beast::tcp_stream; using stream_type = boost::beast::ssl_stream; class Server_test : public beast::unit_test::Suite { public: class TestThread { private: boost::asio::io_context ioContext_; std::optional> 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 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 session, std::vector const&) { } void onClose(Session& session, boost::system::error_code const&) { } void onStopped(Server& server) { } }; //-------------------------------------------------------------------------- // Connect to an address template 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 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 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 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 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 session, std::vector 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 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 cfg) { (*cfg).deprecatedClearSection("port_rpc"); return cfg; }), std::make_unique(&messages)}; }); BEAST_EXPECT(messages.find("Missing 'ip' in [port_rpc]") != std::string::npos); except([&] { Env const env{ *this, envconfig([](std::unique_ptr cfg) { (*cfg).deprecatedClearSection("port_rpc"); (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr()); return cfg; }), std::make_unique(&messages)}; }); BEAST_EXPECT(messages.find("Missing 'port' in [port_rpc]") != std::string::npos); except([&] { Env const env{ *this, envconfig([](std::unique_ptr cfg) { (*cfg).deprecatedClearSection("port_rpc"); (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr()); (*cfg)["port_rpc"].set("port", "0"); return cfg; }), std::make_unique(&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 cfg) { (*cfg)["server"].set("port", "0"); return cfg; }), std::make_unique(&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 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(&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 cfg) { cfg = std::make_unique(); 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(&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 cfg) { cfg = std::make_unique(); 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(&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