#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { namespace test { class ServerStatus_test : public beast::unit_test::suite, public beast::test::enable_yield_to { class myFields : public boost::beast::http::fields { }; static auto makeConfig(std::string const& proto, bool admin = true, bool credentials = false) { auto const section_name = boost::starts_with(proto, "h") ? "port_rpc" : "port_ws"; auto p = jtx::envconfig(); p->overwrite(section_name, "protocol", proto); if (!admin) p->overwrite(section_name, "admin", ""); if (credentials) { (*p)[section_name].set("admin_password", "p"); (*p)[section_name].set("admin_user", "u"); } p->overwrite( boost::starts_with(proto, "h") ? "port_ws" : "port_rpc", "protocol", boost::starts_with(proto, "h") ? "ws" : "http"); if (proto == "https") { // this port is here to allow the env to create its internal client, // which requires an http endpoint to talk to. In the connection // failure test, this endpoint should never be used (*p)["server"].append("port_alt"); (*p)["port_alt"].set("ip", getEnvLocalhostAddr()); (*p)["port_alt"].set("port", "7099"); (*p)["port_alt"].set("protocol", "http"); (*p)["port_alt"].set("admin", getEnvLocalhostAddr()); } return p; } static auto makeWSUpgrade(std::string const& host, uint16_t port) { using namespace boost::asio; using namespace boost::beast::http; request req; req.target("/"); req.version(11); req.insert("Host", host + ":" + std::to_string(port)); req.insert("User-Agent", "test"); req.method(boost::beast::http::verb::get); req.insert("Upgrade", "websocket"); { // not secure, but OK for a testing std::random_device rd; std::mt19937 e{rd()}; std::uniform_int_distribution<> d(0, 255); std::array key{}; for (auto& v : key) v = d(e); req.insert("Sec-WebSocket-Key", base64_encode(key.data(), key.size())); }; req.insert("Sec-WebSocket-Version", "13"); req.insert(boost::beast::http::field::connection, "upgrade"); return req; } static auto makeHTTPRequest( std::string const& host, uint16_t port, std::string const& body, myFields const& fields) { using namespace boost::asio; using namespace boost::beast::http; request req; req.target("/"); req.version(11); for (auto const& f : fields) req.insert(f.name(), f.value()); req.insert("Host", host + ":" + std::to_string(port)); req.insert("User-Agent", "test"); if (body.empty()) { req.method(boost::beast::http::verb::get); } else { req.method(boost::beast::http::verb::post); req.insert("Content-Type", "application/json; charset=UTF-8"); req.body() = body; } req.prepare_payload(); return req; } void doRequest( boost::asio::yield_context& yield, boost::beast::http::request const& req, std::string const& host, uint16_t port, bool secure, boost::beast::http::response& resp, boost::system::error_code& ec) { using namespace boost::asio; using namespace boost::beast::http; io_context& ios = get_io_context(); ip::tcp::resolver r{ios}; boost::beast::multi_buffer sb; auto it = r.async_resolve(host, std::to_string(port), yield[ec]); if (ec) return; resp.body().clear(); if (secure) { ssl::context ctx{ssl::context::sslv23}; ctx.set_verify_mode(ssl::verify_none); ssl::stream ss{ios, ctx}; async_connect(ss.next_layer(), it, yield[ec]); if (ec) return; ss.async_handshake(ssl::stream_base::client, yield[ec]); if (ec) return; boost::beast::http::async_write(ss, req, yield[ec]); if (ec) return; async_read(ss, sb, resp, yield[ec]); if (ec) return; } else { ip::tcp::socket sock{ios}; async_connect(sock, it, yield[ec]); if (ec) return; boost::beast::http::async_write(sock, req, yield[ec]); if (ec) return; async_read(sock, sb, resp, yield[ec]); if (ec) return; } return; } void doWSRequest( test::jtx::Env& env, boost::asio::yield_context& yield, bool secure, boost::beast::http::response& resp, boost::system::error_code& ec) { auto const port = env.app().config()["port_ws"].get("port"); auto ip = env.app().config()["port_ws"].get("ip"); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) doRequest(yield, makeWSUpgrade(*ip, *port), *ip, *port, secure, resp, ec); return; } void doHTTPRequest( test::jtx::Env& env, boost::asio::yield_context& yield, bool secure, boost::beast::http::response& resp, boost::system::error_code& ec, std::string const& body = "", myFields const& fields = {}) { auto const port = env.app().config()["port_rpc"].get("port"); auto const ip = env.app().config()["port_rpc"].get("ip"); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) doRequest(yield, makeHTTPRequest(*ip, *port, body, fields), *ip, *port, secure, resp, ec); return; } static auto makeAdminRequest( jtx::Env& env, std::string const& proto, std::string const& user, std::string const& password, bool subobject = false) { Json::Value jrr; Json::Value jp = Json::objectValue; if (!user.empty()) { jp["admin_user"] = user; if (subobject) { // special case of bad password..passed as object Json::Value jpi = Json::objectValue; jpi["admin_password"] = password; jp["admin_password"] = jpi; } else { jp["admin_password"] = password; } } if (boost::starts_with(proto, "h")) { auto jrc = makeJSONRPCClient(env.app().config()); jrr = jrc->invoke("ledger_accept", jp); } else { auto wsc = makeWSClient(env.app().config(), proto == "ws2"); jrr = wsc->invoke("ledger_accept", jp); } return jrr; } // ------------ // Test Cases // ------------ void testAdminRequest(std::string const& proto, bool admin, bool credentials) { testcase << "Admin request over " << proto << ", config " << (admin ? "enabled" : "disabled") << ", credentials " << (credentials ? "" : "not ") << "set"; using namespace jtx; Env env{*this, makeConfig(proto, admin, credentials)}; Json::Value jrr; auto const proto_ws = boost::starts_with(proto, "w"); // the set of checks we do are different depending // on how the admin config options are set if (admin && credentials) { auto const user = env.app().config()[proto_ws ? "port_ws" : "port_rpc"].get( "admin_user"); auto const password = env.app().config()[proto_ws ? "port_ws" : "port_rpc"].get( "admin_password"); // 1 - FAILS with wrong pass // NOLINTNEXTLINE(bugprone-unchecked-optional-access) jrr = makeAdminRequest(env, proto, *user, *password + "_")[jss::result]; BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission"); BEAST_EXPECT( jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command."); // 2 - FAILS with password in an object // NOLINTNEXTLINE(bugprone-unchecked-optional-access) jrr = makeAdminRequest(env, proto, *user, *password, true)[jss::result]; BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission"); BEAST_EXPECT( jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command."); // 3 - FAILS with wrong user // NOLINTNEXTLINE(bugprone-unchecked-optional-access) jrr = makeAdminRequest(env, proto, *user + "_", *password)[jss::result]; BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission"); BEAST_EXPECT( jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command."); // 4 - FAILS no credentials jrr = makeAdminRequest(env, proto, "", "")[jss::result]; BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission"); BEAST_EXPECT( jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command."); // 5 - SUCCEEDS with proper credentials // NOLINTNEXTLINE(bugprone-unchecked-optional-access) jrr = makeAdminRequest(env, proto, *user, *password)[jss::result]; BEAST_EXPECT(jrr["status"] == "success"); } else if (admin) { // 1 - SUCCEEDS with proper credentials jrr = makeAdminRequest(env, proto, "u", "p")[jss::result]; BEAST_EXPECT(jrr["status"] == "success"); // 2 - SUCCEEDS without proper credentials jrr = makeAdminRequest(env, proto, "", "")[jss::result]; BEAST_EXPECT(jrr["status"] == "success"); } else { // 1 - FAILS - admin disabled jrr = makeAdminRequest(env, proto, "", "")[jss::result]; BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission"); BEAST_EXPECT( jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command."); } } void testWSClientToHttpServer(boost::asio::yield_context& yield) { testcase("WS client to http server fails"); using namespace jtx; Env env{*this, envconfig([](std::unique_ptr cfg) { cfg->section("port_ws").set("protocol", "http,https"); return cfg; })}; // non-secure request { boost::system::error_code ec; boost::beast::http::response resp; doWSRequest(env, yield, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::unauthorized); } // secure request { boost::system::error_code ec; boost::beast::http::response resp; doWSRequest(env, yield, true, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::unauthorized); } } void testStatusRequest(boost::asio::yield_context& yield) { testcase("Status request"); using namespace jtx; Env env{*this, envconfig([](std::unique_ptr cfg) { cfg->section("port_rpc").set("protocol", "ws2,wss2"); cfg->section("port_ws").set("protocol", "http"); return cfg; })}; // non-secure request { boost::system::error_code ec; boost::beast::http::response resp; doHTTPRequest(env, yield, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); } // secure request { boost::system::error_code ec; boost::beast::http::response resp; doHTTPRequest(env, yield, true, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); } } void testTruncatedWSUpgrade(boost::asio::yield_context& yield) { testcase("Partial WS upgrade request"); using namespace jtx; using namespace boost::asio; using namespace boost::beast::http; Env env{*this, envconfig([](std::unique_ptr cfg) { cfg->section("port_ws").set("protocol", "ws2"); return cfg; })}; auto const port = env.app().config()["port_ws"].get("port"); auto const ip = env.app().config()["port_ws"].get("ip"); boost::system::error_code ec; response resp; auto req = makeWSUpgrade(*ip, *port); // NOLINT(bugprone-unchecked-optional-access) // truncate the request message to near the value of the version header auto req_string = boost::lexical_cast(req); req_string.erase(req_string.find_last_of("13"), std::string::npos); io_context& ios = get_io_context(); ip::tcp::resolver r{ios}; boost::beast::multi_buffer sb; auto it = r.async_resolve( *ip, std::to_string(*port), yield[ec]); // NOLINT(bugprone-unchecked-optional-access) if (!BEAST_EXPECTS(!ec, ec.message())) return; ip::tcp::socket sock{ios}; async_connect(sock, it, yield[ec]); if (!BEAST_EXPECTS(!ec, ec.message())) return; async_write(sock, boost::asio::buffer(req_string), yield[ec]); if (!BEAST_EXPECTS(!ec, ec.message())) return; // since we've sent an incomplete request, the server will // keep trying to read until it gives up (by timeout) async_read(sock, sb, resp, yield[ec]); BEAST_EXPECT(ec); } void testCantConnect( std::string const& client_protocol, std::string const& server_protocol, boost::asio::yield_context& yield) { // The essence of this test is to have a client and server configured // out-of-phase with respect to ssl (secure client and insecure server // or vice-versa) testcase << "Connect fails: " << client_protocol << " client to " << server_protocol << " server"; using namespace jtx; Env env{*this, makeConfig(server_protocol)}; boost::beast::http::response resp; boost::system::error_code ec; if (boost::starts_with(client_protocol, "h")) { doHTTPRequest(env, yield, client_protocol == "https", resp, ec); BEAST_EXPECT(ec); } else { doWSRequest( env, yield, client_protocol == "wss" || client_protocol == "wss2", resp, ec); BEAST_EXPECT(ec); } } void testAuth(bool secure, boost::asio::yield_context& yield) { testcase << "Server with authorization, " << (secure ? "secure" : "non-secure"); using namespace test::jtx; Env env{*this, envconfig([secure](std::unique_ptr cfg) { (*cfg)["port_rpc"].set("user", "me"); (*cfg)["port_rpc"].set("password", "secret"); (*cfg)["port_rpc"].set("protocol", secure ? "https" : "http"); if (secure) (*cfg)["port_ws"].set("protocol", "http,ws"); return cfg; })}; Json::Value jr; jr[jss::method] = "server_info"; boost::beast::http::response resp; boost::system::error_code ec; doHTTPRequest(env, yield, secure, resp, ec, to_string(jr)); BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden); myFields auth; auth.insert("Authorization", ""); doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth); BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden); auth.set("Authorization", "Basic NOT-VALID"); doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth); BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden); auth.set("Authorization", "Basic " + base64_encode("me:badpass")); doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth); BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const user = env.app().config().section("port_rpc").get("user").value(); auto const pass = // NOLINTNEXTLINE(bugprone-unchecked-optional-access) env.app().config().section("port_rpc").get("password").value(); // try with the correct user/pass, but not encoded auth.set("Authorization", "Basic " + user + ":" + pass); doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth); BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden); // finally if we use the correct user/pass encoded, we should get a 200 auth.set("Authorization", "Basic " + base64_encode(user + ":" + pass)); doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth); BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); BEAST_EXPECT(!resp.body().empty()); } void testLimit(boost::asio::yield_context& yield, int limit) { testcase << "Server with connection limit of " << limit; using namespace test::jtx; using namespace boost::asio; using namespace boost::beast::http; Env env{*this, envconfig([&](std::unique_ptr cfg) { (*cfg)["port_rpc"].set("limit", std::to_string(limit)); return cfg; })}; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const port = env.app().config()["port_rpc"].get("port").value(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const ip = env.app().config()["port_rpc"].get("ip").value(); boost::system::error_code ec; io_context& ios = get_io_context(); ip::tcp::resolver r{ios}; Json::Value jr; jr[jss::method] = "server_info"; auto it = r.async_resolve(ip, std::to_string(port), yield[ec]); BEAST_EXPECT(!ec); std::vector> clients; int connectionCount{1}; // starts at 1 because the Env already has one // for JSONRPCCLient // for nonzero limits, go one past the limit, although failures happen // at the limit, so this really leads to the last two clients failing. // for zero limit, pick an arbitrary nonzero number of clients - all // should connect fine. int const testTo = (limit == 0) ? 50 : limit + 1; while (connectionCount < testTo) { clients.emplace_back( std::make_pair(ip::tcp::socket{ios}, boost::beast::multi_buffer{})); async_connect(clients.back().first, it, yield[ec]); BEAST_EXPECT(!ec); auto req = makeHTTPRequest(ip, port, to_string(jr), {}); async_write(clients.back().first, req, yield[ec]); BEAST_EXPECT(!ec); ++connectionCount; } int readCount = 0; for (auto& [soc, buf] : clients) { boost::beast::http::response resp; async_read(soc, buf, resp, yield[ec]); ++readCount; // expect the reads to fail for the clients that connected at or // above the limit. If limit is 0, all reads should succeed BEAST_EXPECT((limit == 0 || readCount < limit - 1) ? (!ec) : bool(ec)); } } void testWSHandoff(boost::asio::yield_context& yield) { testcase("Connection with WS handoff"); using namespace test::jtx; Env env{*this, envconfig([](std::unique_ptr cfg) { (*cfg)["port_ws"].set("protocol", "wss"); return cfg; })}; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const port = env.app().config()["port_ws"].get("port").value(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const ip = env.app().config()["port_ws"].get("ip").value(); boost::beast::http::response resp; boost::system::error_code ec; doRequest(yield, makeWSUpgrade(ip, port), ip, port, true, resp, ec); BEAST_EXPECT(resp.result() == boost::beast::http::status::switching_protocols); BEAST_EXPECT(resp.contains("Upgrade") && resp["Upgrade"] == "websocket"); BEAST_EXPECT(resp.contains("Connection") && boost::iequals(resp["Connection"], "upgrade")); } void testNoRPC(boost::asio::yield_context& yield) { testcase("Connection to port with no RPC enabled"); using namespace test::jtx; Env env{*this}; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const port = env.app().config()["port_ws"].get("port").value(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const ip = env.app().config()["port_ws"].get("ip").value(); boost::beast::http::response resp; boost::system::error_code ec; // body content is required here to avoid being // detected as a status request doRequest(yield, makeHTTPRequest(ip, port, "foo", {}), ip, port, false, resp, ec); BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden); BEAST_EXPECT(resp.body() == "Forbidden\r\n"); } void testWSRequests(boost::asio::yield_context& yield) { testcase("WS client sends assorted input"); using namespace test::jtx; using namespace boost::asio; using namespace boost::beast::http; Env env{*this}; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const port = env.app().config()["port_ws"].get("port").value(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const ip = env.app().config()["port_ws"].get("ip").value(); boost::system::error_code ec; io_context& ios = get_io_context(); ip::tcp::resolver r{ios}; auto it = r.async_resolve(ip, std::to_string(port), yield[ec]); if (!BEAST_EXPECT(!ec)) return; ip::tcp::socket sock{ios}; async_connect(sock, it, yield[ec]); if (!BEAST_EXPECT(!ec)) return; boost::beast::websocket::stream ws{sock}; ws.handshake(ip + ":" + std::to_string(port), "/"); // helper lambda, used below auto sendAndParse = [&](std::string const& req) -> Json::Value { ws.async_write_some(true, buffer(req), yield[ec]); if (!BEAST_EXPECT(!ec)) return Json::objectValue; boost::beast::multi_buffer sb; ws.async_read(sb, yield[ec]); if (!BEAST_EXPECT(!ec)) return Json::objectValue; Json::Value resp; Json::Reader jr; if (!BEAST_EXPECT(jr.parse( boost::lexical_cast(boost::beast::make_printable(sb.data())), resp))) return Json::objectValue; sb.consume(sb.size()); return resp; }; { // send invalid json auto resp = sendAndParse("NOT JSON"); BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "jsonInvalid"); BEAST_EXPECT(!resp.isMember(jss::status)); } { // send incorrect json (method and command fields differ) Json::Value jv; jv[jss::command] = "foo"; jv[jss::method] = "bar"; auto resp = sendAndParse(to_string(jv)); BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "missingCommand"); BEAST_EXPECT(resp.isMember(jss::status) && resp[jss::status] == "error"); } { // send a ping (not an error) Json::Value jv; jv[jss::command] = "ping"; auto resp = sendAndParse(to_string(jv)); BEAST_EXPECT(resp.isMember(jss::status) && resp[jss::status] == "success"); BEAST_EXPECT( resp.isMember(jss::result) && resp[jss::result].isMember(jss::role) && resp[jss::result][jss::role] == "admin"); } } void testAmendmentWarning(boost::asio::yield_context& yield) { testcase("Status request over WS and RPC with/without Amendment Warning"); using namespace jtx; using namespace boost::asio; using namespace boost::beast::http; Env env{ *this, validator( envconfig([](std::unique_ptr cfg) { cfg->section("port_rpc").set("protocol", "http"); return cfg; }), "")}; env.close(); // advance the ledger so that server status // sees a published ledger -- without this, we get a status // failure message about no published ledgers env.app().getLedgerMaster().tryAdvance(); // make an RPC server info request and look for // amendment warning status auto si = env.rpc("server_info")[jss::result]; BEAST_EXPECT(si.isMember(jss::info)); BEAST_EXPECT(!si[jss::info].isMember(jss::amendment_blocked)); BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true); BEAST_EXPECT(!si.isMember(jss::warnings)); // make an RPC server state request and look for // amendment warning status si = env.rpc("server_state")[jss::result]; BEAST_EXPECT(si.isMember(jss::state)); BEAST_EXPECT(!si[jss::state].isMember(jss::amendment_blocked)); BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true); BEAST_EXPECT(!si[jss::state].isMember(jss::warnings)); auto const port_ws = env.app().config()["port_ws"].get("port"); auto const ip_ws = env.app().config()["port_ws"].get("ip"); boost::system::error_code ec; response resp; doRequest( yield, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) makeHTTPRequest(*ip_ws, *port_ws, "", {}), // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *ip_ws, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *port_ws, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos); // mark the Network as having an Amendment Warning, but won't fail env.app().getOPs().setAmendmentWarned(); env.app().getOPs().beginConsensus(env.closed()->header().hash, {}); // consensus doesn't change BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true); // RPC request server_info again, now unsupported majority should be // returned si = env.rpc("server_info")[jss::result]; BEAST_EXPECT(si.isMember(jss::info)); BEAST_EXPECT(!si[jss::info].isMember(jss::amendment_blocked)); BEAST_EXPECT( si[jss::info].isMember(jss::warnings) && si[jss::info][jss::warnings].isArray() && si[jss::info][jss::warnings].size() == 1 && si[jss::info][jss::warnings][0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY); // RPC request server_state again, now unsupported majority should be // returned si = env.rpc("server_state")[jss::result]; BEAST_EXPECT(si.isMember(jss::state)); BEAST_EXPECT(!si[jss::state].isMember(jss::amendment_blocked)); BEAST_EXPECT( si[jss::state].isMember(jss::warnings) && si[jss::state][jss::warnings].isArray() && si[jss::state][jss::warnings].size() == 1 && si[jss::state][jss::warnings][0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY); // but status does not indicate a problem doRequest( yield, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) makeHTTPRequest(*ip_ws, *port_ws, "", {}), // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *ip_ws, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *port_ws, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos); // with ELB_SUPPORT, status still does not indicate a problem env.app().config().ELB_SUPPORT = true; doRequest( yield, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) makeHTTPRequest(*ip_ws, *port_ws, "", {}), // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *ip_ws, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *port_ws, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos); } void testAmendmentBlock(boost::asio::yield_context& yield) { testcase("Status request over WS and RPC with/without Amendment Block"); using namespace jtx; using namespace boost::asio; using namespace boost::beast::http; Env env{ *this, validator( envconfig([](std::unique_ptr cfg) { cfg->section("port_rpc").set("protocol", "http"); return cfg; }), "")}; env.close(); // advance the ledger so that server status // sees a published ledger -- without this, we get a status // failure message about no published ledgers env.app().getLedgerMaster().tryAdvance(); // make an RPC server info request and look for // amendment_blocked status auto si = env.rpc("server_info")[jss::result]; BEAST_EXPECT(si.isMember(jss::info)); BEAST_EXPECT(!si[jss::info].isMember(jss::amendment_blocked)); BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true); BEAST_EXPECT(!si.isMember(jss::warnings)); // make an RPC server state request and look for // amendment_blocked status si = env.rpc("server_state")[jss::result]; BEAST_EXPECT(si.isMember(jss::state)); BEAST_EXPECT(!si[jss::state].isMember(jss::amendment_blocked)); BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true); BEAST_EXPECT(!si[jss::state].isMember(jss::warnings)); auto const port_ws = env.app().config()["port_ws"].get("port"); auto const ip_ws = env.app().config()["port_ws"].get("ip"); boost::system::error_code ec; response resp; doRequest( yield, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) makeHTTPRequest(*ip_ws, *port_ws, "", {}), // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *ip_ws, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *port_ws, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos); // mark the Network as Amendment Blocked, but still won't fail until // ELB is enabled (next step) env.app().getOPs().setAmendmentBlocked(); env.app().getOPs().beginConsensus(env.closed()->header().hash, {}); // consensus now sees validation disabled BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == false); // RPC request server_info again, now AB should be returned si = env.rpc("server_info")[jss::result]; BEAST_EXPECT(si.isMember(jss::info)); BEAST_EXPECT( si[jss::info].isMember(jss::amendment_blocked) && si[jss::info][jss::amendment_blocked] == true); BEAST_EXPECT( si[jss::info].isMember(jss::warnings) && si[jss::info][jss::warnings].isArray() && si[jss::info][jss::warnings].size() == 1 && si[jss::info][jss::warnings][0u][jss::id].asInt() == warnRPC_AMENDMENT_BLOCKED); // RPC request server_state again, now AB should be returned si = env.rpc("server_state")[jss::result]; BEAST_EXPECT( si[jss::state].isMember(jss::amendment_blocked) && si[jss::state][jss::amendment_blocked] == true); BEAST_EXPECT( si[jss::state].isMember(jss::warnings) && si[jss::state][jss::warnings].isArray() && si[jss::state][jss::warnings].size() == 1 && si[jss::state][jss::warnings][0u][jss::id].asInt() == warnRPC_AMENDMENT_BLOCKED); // but status does not indicate because it still relies on ELB // being enabled doRequest( yield, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) makeHTTPRequest(*ip_ws, *port_ws, "", {}), // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *ip_ws, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *port_ws, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::ok); BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos); env.app().config().ELB_SUPPORT = true; doRequest( yield, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) makeHTTPRequest(*ip_ws, *port_ws, "", {}), // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *ip_ws, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) *port_ws, false, resp, ec); if (!BEAST_EXPECTS(!ec, ec.message())) return; BEAST_EXPECT(resp.result() == boost::beast::http::status::internal_server_error); BEAST_EXPECT(resp.body().find("cannot accept clients:") != std::string::npos); BEAST_EXPECT(resp.body().find("Server version too old") != std::string::npos); } void testRPCRequests(boost::asio::yield_context& yield) { testcase("RPC client sends assorted input"); using namespace test::jtx; Env env{*this}; boost::system::error_code ec; { boost::beast::http::response resp; doHTTPRequest(env, yield, false, resp, ec, "{}"); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n"); } { boost::beast::http::response resp; Json::Value jv; jv["invalid"] = 1; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Null method\r\n"); } { boost::beast::http::response resp; Json::Value jv(Json::arrayValue); jv.append("invalid"); doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n"); } { boost::beast::http::response resp; Json::Value jv(Json::arrayValue); Json::Value j; j["invalid"] = 1; jv.append(j); doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n"); } { boost::beast::http::response resp; Json::Value jv; jv[jss::method] = "batch"; jv[jss::params] = 2; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Malformed batch request\r\n"); } { boost::beast::http::response resp; Json::Value jv; jv[jss::method] = "batch"; jv[jss::params] = Json::objectValue; jv[jss::params]["invalid"] = 3; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Malformed batch request\r\n"); } Json::Value jv; { boost::beast::http::response resp; jv[jss::method] = Json::nullValue; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "Null method\r\n"); } { boost::beast::http::response resp; jv[jss::method] = 1; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "method is not string\r\n"); } { boost::beast::http::response resp; jv[jss::method] = ""; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "method is empty\r\n"); } { boost::beast::http::response resp; jv[jss::method] = "some_method"; jv[jss::params] = "params"; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "params unparsable\r\n"); } { boost::beast::http::response resp; jv[jss::params] = Json::arrayValue; jv[jss::params][0u] = "not an object"; doHTTPRequest(env, yield, false, resp, ec, to_string(jv)); BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request); BEAST_EXPECT(resp.body() == "params unparsable\r\n"); } } void testStatusNotOkay(boost::asio::yield_context& yield) { testcase("Server status not okay"); using namespace test::jtx; Env env{*this, envconfig([](std::unique_ptr cfg) { cfg->ELB_SUPPORT = true; return cfg; })}; // raise the fee so that the server is considered overloaded env.app().getFeeTrack().raiseLocalFee(); boost::beast::http::response resp; boost::system::error_code ec; doHTTPRequest(env, yield, false, resp, ec); BEAST_EXPECT(resp.result() == boost::beast::http::status::internal_server_error); std::regex const body{"Server cannot accept clients"}; BEAST_EXPECT(std::regex_search(resp.body(), body)); } public: void run() override { for (auto it : {"http", "ws", "ws2"}) { testAdminRequest(it, true, true); testAdminRequest(it, true, false); testAdminRequest(it, false, false); } yield_to([&](boost::asio::yield_context& yield) { testWSClientToHttpServer(yield); testStatusRequest(yield); testTruncatedWSUpgrade(yield); // these are secure/insecure protocol pairs, i.e. for // each item, the second value is the secure or insecure equivalent testCantConnect("ws", "wss", yield); testCantConnect("ws2", "wss2", yield); testCantConnect("http", "https", yield); testCantConnect("wss", "ws", yield); testCantConnect("wss2", "ws2", yield); testCantConnect("https", "http", yield); testAmendmentWarning(yield); testAmendmentBlock(yield); testAuth(false, yield); testAuth(true, yield); testLimit(yield, 5); testLimit(yield, 0); testWSHandoff(yield); testNoRPC(yield); testWSRequests(yield); testRPCRequests(yield); testStatusNotOkay(yield); }); } }; BEAST_DEFINE_TESTSUITE(ServerStatus, server, xrpl); } // namespace test } // namespace xrpl