rippled
Loading...
Searching...
No Matches
ServerStatus_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/JSONRPCClient.h>
3#include <test/jtx/WSClient.h>
4#include <test/jtx/envconfig.h>
5
6#include <xrpld/app/ledger/LedgerMaster.h>
7#include <xrpld/app/misc/LoadFeeTrack.h>
8#include <xrpld/app/misc/NetworkOPs.h>
9#include <xrpld/rpc/ServerHandler.h>
10
11#include <xrpl/basics/base64.h>
12#include <xrpl/beast/test/yield_to.h>
13#include <xrpl/json/json_reader.h>
14
15#include <boost/algorithm/string/predicate.hpp>
16#include <boost/asio.hpp>
17#include <boost/asio/io_context.hpp>
18#include <boost/asio/ssl.hpp>
19#include <boost/beast/core/multi_buffer.hpp>
20#include <boost/beast/http.hpp>
21
22#include <algorithm>
23#include <array>
24#include <random>
25#include <regex>
26
27namespace xrpl {
28namespace test {
29
31{
32 class myFields : public boost::beast::http::fields
33 {
34 };
35
36 auto
37 makeConfig(std::string const& proto, bool admin = true, bool credentials = false)
38 {
39 auto const section_name = boost::starts_with(proto, "h") ? "port_rpc" : "port_ws";
40 auto p = jtx::envconfig();
41
42 p->overwrite(section_name, "protocol", proto);
43 if (!admin)
44 p->overwrite(section_name, "admin", "");
45
46 if (credentials)
47 {
48 (*p)[section_name].set("admin_password", "p");
49 (*p)[section_name].set("admin_user", "u");
50 }
51
52 p->overwrite(
53 boost::starts_with(proto, "h") ? "port_ws" : "port_rpc",
54 "protocol",
55 boost::starts_with(proto, "h") ? "ws" : "http");
56
57 if (proto == "https")
58 {
59 // this port is here to allow the env to create its internal client,
60 // which requires an http endpoint to talk to. In the connection
61 // failure test, this endpoint should never be used
62 (*p)["server"].append("port_alt");
63 (*p)["port_alt"].set("ip", getEnvLocalhostAddr());
64 (*p)["port_alt"].set("port", "7099");
65 (*p)["port_alt"].set("protocol", "http");
66 (*p)["port_alt"].set("admin", getEnvLocalhostAddr());
67 }
68
69 return p;
70 }
71
72 auto
73 makeWSUpgrade(std::string const& host, uint16_t port)
74 {
75 using namespace boost::asio;
76 using namespace boost::beast::http;
77 request<string_body> req;
78
79 req.target("/");
80 req.version(11);
81 req.insert("Host", host + ":" + std::to_string(port));
82 req.insert("User-Agent", "test");
83 req.method(boost::beast::http::verb::get);
84 req.insert("Upgrade", "websocket");
85 {
86 // not secure, but OK for a testing
88 std::mt19937 e{rd()};
91 for (auto& v : key)
92 v = d(e);
93 req.insert("Sec-WebSocket-Key", base64_encode(key.data(), key.size()));
94 };
95 req.insert("Sec-WebSocket-Version", "13");
96 req.insert(boost::beast::http::field::connection, "upgrade");
97 return req;
98 }
99
100 auto
101 makeHTTPRequest(std::string const& host, uint16_t port, std::string const& body, myFields const& fields)
102 {
103 using namespace boost::asio;
104 using namespace boost::beast::http;
105 request<string_body> req;
106
107 req.target("/");
108 req.version(11);
109 for (auto const& f : fields)
110 req.insert(f.name(), f.value());
111 req.insert("Host", host + ":" + std::to_string(port));
112 req.insert("User-Agent", "test");
113 if (body.empty())
114 {
115 req.method(boost::beast::http::verb::get);
116 }
117 else
118 {
119 req.method(boost::beast::http::verb::post);
120 req.insert("Content-Type", "application/json; charset=UTF-8");
121 req.body() = body;
122 }
123 req.prepare_payload();
124
125 return req;
126 }
127
128 void
130 boost::asio::yield_context& yield,
131 boost::beast::http::request<boost::beast::http::string_body>&& req,
132 std::string const& host,
133 uint16_t port,
134 bool secure,
135 boost::beast::http::response<boost::beast::http::string_body>& resp,
136 boost::system::error_code& ec)
137 {
138 using namespace boost::asio;
139 using namespace boost::beast::http;
140 io_context& ios = get_io_context();
141 ip::tcp::resolver r{ios};
142 boost::beast::multi_buffer sb;
143
144 auto it = r.async_resolve(host, std::to_string(port), yield[ec]);
145 if (ec)
146 return;
147
148 resp.body().clear();
149 if (secure)
150 {
151 ssl::context ctx{ssl::context::sslv23};
152 ctx.set_verify_mode(ssl::verify_none);
153 ssl::stream<ip::tcp::socket> ss{ios, ctx};
154 async_connect(ss.next_layer(), it, yield[ec]);
155 if (ec)
156 return;
157 ss.async_handshake(ssl::stream_base::client, yield[ec]);
158 if (ec)
159 return;
160 boost::beast::http::async_write(ss, req, yield[ec]);
161 if (ec)
162 return;
163 async_read(ss, sb, resp, yield[ec]);
164 if (ec)
165 return;
166 }
167 else
168 {
169 ip::tcp::socket sock{ios};
170 async_connect(sock, it, yield[ec]);
171 if (ec)
172 return;
173 boost::beast::http::async_write(sock, req, yield[ec]);
174 if (ec)
175 return;
176 async_read(sock, sb, resp, yield[ec]);
177 if (ec)
178 return;
179 }
180
181 return;
182 }
183
184 void
186 test::jtx::Env& env,
187 boost::asio::yield_context& yield,
188 bool secure,
189 boost::beast::http::response<boost::beast::http::string_body>& resp,
190 boost::system::error_code& ec)
191 {
192 auto const port = env.app().config()["port_ws"].get<std::uint16_t>("port");
193 auto ip = env.app().config()["port_ws"].get<std::string>("ip");
194 doRequest(yield, makeWSUpgrade(*ip, *port), *ip, *port, secure, resp, ec);
195 return;
196 }
197
198 void
200 test::jtx::Env& env,
201 boost::asio::yield_context& yield,
202 bool secure,
203 boost::beast::http::response<boost::beast::http::string_body>& resp,
204 boost::system::error_code& ec,
205 std::string const& body = "",
206 myFields const& fields = {})
207 {
208 auto const port = env.app().config()["port_rpc"].get<std::uint16_t>("port");
209 auto const ip = env.app().config()["port_rpc"].get<std::string>("ip");
210 doRequest(yield, makeHTTPRequest(*ip, *port, body, fields), *ip, *port, secure, resp, ec);
211 return;
212 }
213
214 auto
216 jtx::Env& env,
217 std::string const& proto,
218 std::string const& user,
219 std::string const& password,
220 bool subobject = false)
221 {
222 Json::Value jrr;
223
225 if (!user.empty())
226 {
227 jp["admin_user"] = user;
228 if (subobject)
229 {
230 // special case of bad password..passed as object
232 jpi["admin_password"] = password;
233 jp["admin_password"] = jpi;
234 }
235 else
236 {
237 jp["admin_password"] = password;
238 }
239 }
240
241 if (boost::starts_with(proto, "h"))
242 {
243 auto jrc = makeJSONRPCClient(env.app().config());
244 jrr = jrc->invoke("ledger_accept", jp);
245 }
246 else
247 {
248 auto wsc = makeWSClient(env.app().config(), proto == "ws2");
249 jrr = wsc->invoke("ledger_accept", jp);
250 }
251
252 return jrr;
253 }
254
255 // ------------
256 // Test Cases
257 // ------------
258
259 void
260 testAdminRequest(std::string const& proto, bool admin, bool credentials)
261 {
262 testcase << "Admin request over " << proto << ", config " << (admin ? "enabled" : "disabled")
263 << ", credentials " << (credentials ? "" : "not ") << "set";
264 using namespace jtx;
265 Env env{*this, makeConfig(proto, admin, credentials)};
266
267 Json::Value jrr;
268 auto const proto_ws = boost::starts_with(proto, "w");
269
270 // the set of checks we do are different depending
271 // on how the admin config options are set
272
273 if (admin && credentials)
274 {
275 auto const user = env.app().config()[proto_ws ? "port_ws" : "port_rpc"].get<std::string>("admin_user");
276
277 auto const password =
278 env.app().config()[proto_ws ? "port_ws" : "port_rpc"].get<std::string>("admin_password");
279
280 // 1 - FAILS with wrong pass
281 jrr = makeAdminRequest(env, proto, *user, *password + "_")[jss::result];
282 BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission");
283 BEAST_EXPECT(
284 jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command.");
285
286 // 2 - FAILS with password in an object
287 jrr = makeAdminRequest(env, proto, *user, *password, true)[jss::result];
288 BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission");
289 BEAST_EXPECT(
290 jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command.");
291
292 // 3 - FAILS with wrong user
293 jrr = makeAdminRequest(env, proto, *user + "_", *password)[jss::result];
294 BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission");
295 BEAST_EXPECT(
296 jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command.");
297
298 // 4 - FAILS no credentials
299 jrr = makeAdminRequest(env, proto, "", "")[jss::result];
300 BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission");
301 BEAST_EXPECT(
302 jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command.");
303
304 // 5 - SUCCEEDS with proper credentials
305 jrr = makeAdminRequest(env, proto, *user, *password)[jss::result];
306 BEAST_EXPECT(jrr["status"] == "success");
307 }
308 else if (admin)
309 {
310 // 1 - SUCCEEDS with proper credentials
311 jrr = makeAdminRequest(env, proto, "u", "p")[jss::result];
312 BEAST_EXPECT(jrr["status"] == "success");
313
314 // 2 - SUCCEEDS without proper credentials
315 jrr = makeAdminRequest(env, proto, "", "")[jss::result];
316 BEAST_EXPECT(jrr["status"] == "success");
317 }
318 else
319 {
320 // 1 - FAILS - admin disabled
321 jrr = makeAdminRequest(env, proto, "", "")[jss::result];
322 BEAST_EXPECT(jrr["error"] == proto_ws ? "forbidden" : "noPermission");
323 BEAST_EXPECT(
324 jrr["error_message"] == proto_ws ? "Bad credentials." : "You don't have permission for this command.");
325 }
326 }
327
328 void
329 testWSClientToHttpServer(boost::asio::yield_context& yield)
330 {
331 testcase("WS client to http server fails");
332 using namespace jtx;
333 Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
334 cfg->section("port_ws").set("protocol", "http,https");
335 return cfg;
336 })};
337
338 // non-secure request
339 {
340 boost::system::error_code ec;
341 boost::beast::http::response<boost::beast::http::string_body> resp;
342 doWSRequest(env, yield, false, resp, ec);
343 if (!BEAST_EXPECTS(!ec, ec.message()))
344 return;
345 BEAST_EXPECT(resp.result() == boost::beast::http::status::unauthorized);
346 }
347
348 // secure request
349 {
350 boost::system::error_code ec;
351 boost::beast::http::response<boost::beast::http::string_body> resp;
352 doWSRequest(env, yield, true, resp, ec);
353 if (!BEAST_EXPECTS(!ec, ec.message()))
354 return;
355 BEAST_EXPECT(resp.result() == boost::beast::http::status::unauthorized);
356 }
357 }
358
359 void
360 testStatusRequest(boost::asio::yield_context& yield)
361 {
362 testcase("Status request");
363 using namespace jtx;
364 Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
365 cfg->section("port_rpc").set("protocol", "ws2,wss2");
366 cfg->section("port_ws").set("protocol", "http");
367 return cfg;
368 })};
369
370 // non-secure request
371 {
372 boost::system::error_code ec;
373 boost::beast::http::response<boost::beast::http::string_body> resp;
374 doHTTPRequest(env, yield, false, resp, ec);
375 if (!BEAST_EXPECTS(!ec, ec.message()))
376 return;
377 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
378 }
379
380 // secure request
381 {
382 boost::system::error_code ec;
383 boost::beast::http::response<boost::beast::http::string_body> resp;
384 doHTTPRequest(env, yield, true, resp, ec);
385 if (!BEAST_EXPECTS(!ec, ec.message()))
386 return;
387 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
388 }
389 }
390
391 void
392 testTruncatedWSUpgrade(boost::asio::yield_context& yield)
393 {
394 testcase("Partial WS upgrade request");
395 using namespace jtx;
396 using namespace boost::asio;
397 using namespace boost::beast::http;
398 Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
399 cfg->section("port_ws").set("protocol", "ws2");
400 return cfg;
401 })};
402
403 auto const port = env.app().config()["port_ws"].get<std::uint16_t>("port");
404 auto const ip = env.app().config()["port_ws"].get<std::string>("ip");
405
406 boost::system::error_code ec;
407 response<string_body> resp;
408 auto req = makeWSUpgrade(*ip, *port);
409
410 // truncate the request message to near the value of the version header
411 auto req_string = boost::lexical_cast<std::string>(req);
412 req_string.erase(req_string.find_last_of("13"), std::string::npos);
413
414 io_context& ios = get_io_context();
415 ip::tcp::resolver r{ios};
416 boost::beast::multi_buffer sb;
417
418 auto it = r.async_resolve(*ip, std::to_string(*port), yield[ec]);
419 if (!BEAST_EXPECTS(!ec, ec.message()))
420 return;
421
422 ip::tcp::socket sock{ios};
423 async_connect(sock, it, yield[ec]);
424 if (!BEAST_EXPECTS(!ec, ec.message()))
425 return;
426 async_write(sock, boost::asio::buffer(req_string), yield[ec]);
427 if (!BEAST_EXPECTS(!ec, ec.message()))
428 return;
429 // since we've sent an incomplete request, the server will
430 // keep trying to read until it gives up (by timeout)
431 async_read(sock, sb, resp, yield[ec]);
432 BEAST_EXPECT(ec);
433 }
434
435 void
437 std::string const& client_protocol,
438 std::string const& server_protocol,
439 boost::asio::yield_context& yield)
440 {
441 // The essence of this test is to have a client and server configured
442 // out-of-phase with respect to ssl (secure client and insecure server
443 // or vice-versa)
444 testcase << "Connect fails: " << client_protocol << " client to " << server_protocol << " server";
445 using namespace jtx;
446 Env env{*this, makeConfig(server_protocol)};
447
448 boost::beast::http::response<boost::beast::http::string_body> resp;
449 boost::system::error_code ec;
450 if (boost::starts_with(client_protocol, "h"))
451 {
452 doHTTPRequest(env, yield, client_protocol == "https", resp, ec);
453 BEAST_EXPECT(ec);
454 }
455 else
456 {
457 doWSRequest(env, yield, client_protocol == "wss" || client_protocol == "wss2", resp, ec);
458 BEAST_EXPECT(ec);
459 }
460 }
461
462 void
463 testAuth(bool secure, boost::asio::yield_context& yield)
464 {
465 testcase << "Server with authorization, " << (secure ? "secure" : "non-secure");
466
467 using namespace test::jtx;
468 Env env{*this, envconfig([secure](std::unique_ptr<Config> cfg) {
469 (*cfg)["port_rpc"].set("user", "me");
470 (*cfg)["port_rpc"].set("password", "secret");
471 (*cfg)["port_rpc"].set("protocol", secure ? "https" : "http");
472 if (secure)
473 (*cfg)["port_ws"].set("protocol", "http,ws");
474 return cfg;
475 })};
476
477 Json::Value jr;
478 jr[jss::method] = "server_info";
479 boost::beast::http::response<boost::beast::http::string_body> resp;
480 boost::system::error_code ec;
481 doHTTPRequest(env, yield, secure, resp, ec, to_string(jr));
482 BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden);
483
485 auth.insert("Authorization", "");
486 doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
487 BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden);
488
489 auth.set("Authorization", "Basic NOT-VALID");
490 doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
491 BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden);
492
493 auth.set("Authorization", "Basic " + base64_encode("me:badpass"));
494 doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
495 BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden);
496
497 auto const user = env.app().config().section("port_rpc").get<std::string>("user").value();
498 auto const pass = env.app().config().section("port_rpc").get<std::string>("password").value();
499
500 // try with the correct user/pass, but not encoded
501 auth.set("Authorization", "Basic " + user + ":" + pass);
502 doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
503 BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden);
504
505 // finally if we use the correct user/pass encoded, we should get a 200
506 auth.set("Authorization", "Basic " + base64_encode(user + ":" + pass));
507 doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
508 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
509 BEAST_EXPECT(!resp.body().empty());
510 }
511
512 void
513 testLimit(boost::asio::yield_context& yield, int limit)
514 {
515 testcase << "Server with connection limit of " << limit;
516
517 using namespace test::jtx;
518 using namespace boost::asio;
519 using namespace boost::beast::http;
520 Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
521 (*cfg)["port_rpc"].set("limit", std::to_string(limit));
522 return cfg;
523 })};
524
525 auto const port = env.app().config()["port_rpc"].get<std::uint16_t>("port").value();
526 auto const ip = env.app().config()["port_rpc"].get<std::string>("ip").value();
527
528 boost::system::error_code ec;
529 io_context& ios = get_io_context();
530 ip::tcp::resolver r{ios};
531
532 Json::Value jr;
533 jr[jss::method] = "server_info";
534
535 auto it = r.async_resolve(ip, std::to_string(port), yield[ec]);
536 BEAST_EXPECT(!ec);
537
539 int connectionCount{1}; // starts at 1 because the Env already has one
540 // for JSONRPCCLient
541
542 // for nonzero limits, go one past the limit, although failures happen
543 // at the limit, so this really leads to the last two clients failing.
544 // for zero limit, pick an arbitrary nonzero number of clients - all
545 // should connect fine.
546
547 int testTo = (limit == 0) ? 50 : limit + 1;
548 while (connectionCount < testTo)
549 {
550 clients.emplace_back(std::make_pair(ip::tcp::socket{ios}, boost::beast::multi_buffer{}));
551 async_connect(clients.back().first, it, yield[ec]);
552 BEAST_EXPECT(!ec);
553 auto req = makeHTTPRequest(ip, port, to_string(jr), {});
554 async_write(clients.back().first, req, yield[ec]);
555 BEAST_EXPECT(!ec);
556 ++connectionCount;
557 }
558
559 int readCount = 0;
560 for (auto& [soc, buf] : clients)
561 {
562 boost::beast::http::response<boost::beast::http::string_body> resp;
563 async_read(soc, buf, resp, yield[ec]);
564 ++readCount;
565 // expect the reads to fail for the clients that connected at or
566 // above the limit. If limit is 0, all reads should succeed
567 BEAST_EXPECT((limit == 0 || readCount < limit - 1) ? (!ec) : bool(ec));
568 }
569 }
570
571 void
572 testWSHandoff(boost::asio::yield_context& yield)
573 {
574 testcase("Connection with WS handoff");
575
576 using namespace test::jtx;
577 Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
578 (*cfg)["port_ws"].set("protocol", "wss");
579 return cfg;
580 })};
581
582 auto const port = env.app().config()["port_ws"].get<std::uint16_t>("port").value();
583 auto const ip = env.app().config()["port_ws"].get<std::string>("ip").value();
584 boost::beast::http::response<boost::beast::http::string_body> resp;
585 boost::system::error_code ec;
586 doRequest(yield, makeWSUpgrade(ip, port), ip, port, true, resp, ec);
587 BEAST_EXPECT(resp.result() == boost::beast::http::status::switching_protocols);
588 BEAST_EXPECT(resp.find("Upgrade") != resp.end() && resp["Upgrade"] == "websocket");
589 BEAST_EXPECT(resp.find("Connection") != resp.end() && boost::iequals(resp["Connection"], "upgrade"));
590 }
591
592 void
593 testNoRPC(boost::asio::yield_context& yield)
594 {
595 testcase("Connection to port with no RPC enabled");
596
597 using namespace test::jtx;
598 Env env{*this};
599
600 auto const port = env.app().config()["port_ws"].get<std::uint16_t>("port").value();
601 auto const ip = env.app().config()["port_ws"].get<std::string>("ip").value();
602 boost::beast::http::response<boost::beast::http::string_body> resp;
603 boost::system::error_code ec;
604 // body content is required here to avoid being
605 // detected as a status request
606 doRequest(yield, makeHTTPRequest(ip, port, "foo", {}), ip, port, false, resp, ec);
607 BEAST_EXPECT(resp.result() == boost::beast::http::status::forbidden);
608 BEAST_EXPECT(resp.body() == "Forbidden\r\n");
609 }
610
611 void
612 testWSRequests(boost::asio::yield_context& yield)
613 {
614 testcase("WS client sends assorted input");
615
616 using namespace test::jtx;
617 using namespace boost::asio;
618 using namespace boost::beast::http;
619 Env env{*this};
620
621 auto const port = env.app().config()["port_ws"].get<std::uint16_t>("port").value();
622 auto const ip = env.app().config()["port_ws"].get<std::string>("ip").value();
623 boost::system::error_code ec;
624
625 io_context& ios = get_io_context();
626 ip::tcp::resolver r{ios};
627
628 auto it = r.async_resolve(ip, std::to_string(port), yield[ec]);
629 if (!BEAST_EXPECT(!ec))
630 return;
631
632 ip::tcp::socket sock{ios};
633 async_connect(sock, it, yield[ec]);
634 if (!BEAST_EXPECT(!ec))
635 return;
636
637 boost::beast::websocket::stream<boost::asio::ip::tcp::socket&> ws{sock};
638 ws.handshake(ip + ":" + std::to_string(port), "/");
639
640 // helper lambda, used below
641 auto sendAndParse = [&](std::string const& req) -> Json::Value {
642 ws.async_write_some(true, buffer(req), yield[ec]);
643 if (!BEAST_EXPECT(!ec))
644 return Json::objectValue;
645
646 boost::beast::multi_buffer sb;
647 ws.async_read(sb, yield[ec]);
648 if (!BEAST_EXPECT(!ec))
649 return Json::objectValue;
650
651 Json::Value resp;
652 Json::Reader jr;
653 if (!BEAST_EXPECT(
654 jr.parse(boost::lexical_cast<std::string>(boost::beast::make_printable(sb.data())), resp)))
655 return Json::objectValue;
656 sb.consume(sb.size());
657 return resp;
658 };
659
660 { // send invalid json
661 auto resp = sendAndParse("NOT JSON");
662 BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "jsonInvalid");
663 BEAST_EXPECT(!resp.isMember(jss::status));
664 }
665
666 { // send incorrect json (method and command fields differ)
667 Json::Value jv;
668 jv[jss::command] = "foo";
669 jv[jss::method] = "bar";
670 auto resp = sendAndParse(to_string(jv));
671 BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "missingCommand");
672 BEAST_EXPECT(resp.isMember(jss::status) && resp[jss::status] == "error");
673 }
674
675 { // send a ping (not an error)
676 Json::Value jv;
677 jv[jss::command] = "ping";
678 auto resp = sendAndParse(to_string(jv));
679 BEAST_EXPECT(resp.isMember(jss::status) && resp[jss::status] == "success");
680 BEAST_EXPECT(
681 resp.isMember(jss::result) && resp[jss::result].isMember(jss::role) &&
682 resp[jss::result][jss::role] == "admin");
683 }
684 }
685
686 void
687 testAmendmentWarning(boost::asio::yield_context& yield)
688 {
689 testcase("Status request over WS and RPC with/without Amendment Warning");
690 using namespace jtx;
691 using namespace boost::asio;
692 using namespace boost::beast::http;
693 Env env{
694 *this,
695 validator(
697 cfg->section("port_rpc").set("protocol", "http");
698 return cfg;
699 }),
700 "")};
701
702 env.close();
703
704 // advance the ledger so that server status
705 // sees a published ledger -- without this, we get a status
706 // failure message about no published ledgers
708
709 // make an RPC server info request and look for
710 // amendment warning status
711 auto si = env.rpc("server_info")[jss::result];
712 BEAST_EXPECT(si.isMember(jss::info));
713 BEAST_EXPECT(!si[jss::info].isMember(jss::amendment_blocked));
714 BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true);
715 BEAST_EXPECT(!si.isMember(jss::warnings));
716
717 // make an RPC server state request and look for
718 // amendment warning status
719 si = env.rpc("server_state")[jss::result];
720 BEAST_EXPECT(si.isMember(jss::state));
721 BEAST_EXPECT(!si[jss::state].isMember(jss::amendment_blocked));
722 BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true);
723 BEAST_EXPECT(!si[jss::state].isMember(jss::warnings));
724
725 auto const port_ws = env.app().config()["port_ws"].get<std::uint16_t>("port");
726 auto const ip_ws = env.app().config()["port_ws"].get<std::string>("ip");
727
728 boost::system::error_code ec;
729 response<string_body> resp;
730
731 doRequest(yield, makeHTTPRequest(*ip_ws, *port_ws, "", {}), *ip_ws, *port_ws, false, resp, ec);
732
733 if (!BEAST_EXPECTS(!ec, ec.message()))
734 return;
735 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
736 BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos);
737
738 // mark the Network as having an Amendment Warning, but won't fail
739 env.app().getOPs().setAmendmentWarned();
740 env.app().getOPs().beginConsensus(env.closed()->header().hash, {});
741
742 // consensus doesn't change
743 BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true);
744
745 // RPC request server_info again, now unsupported majority should be
746 // returned
747 si = env.rpc("server_info")[jss::result];
748 BEAST_EXPECT(si.isMember(jss::info));
749 BEAST_EXPECT(!si[jss::info].isMember(jss::amendment_blocked));
750 BEAST_EXPECT(
751 si[jss::info].isMember(jss::warnings) && si[jss::info][jss::warnings].isArray() &&
752 si[jss::info][jss::warnings].size() == 1 &&
753 si[jss::info][jss::warnings][0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
754
755 // RPC request server_state again, now unsupported majority should be
756 // returned
757 si = env.rpc("server_state")[jss::result];
758 BEAST_EXPECT(si.isMember(jss::state));
759 BEAST_EXPECT(!si[jss::state].isMember(jss::amendment_blocked));
760 BEAST_EXPECT(
761 si[jss::state].isMember(jss::warnings) && si[jss::state][jss::warnings].isArray() &&
762 si[jss::state][jss::warnings].size() == 1 &&
763 si[jss::state][jss::warnings][0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
764
765 // but status does not indicate a problem
766 doRequest(yield, makeHTTPRequest(*ip_ws, *port_ws, "", {}), *ip_ws, *port_ws, false, resp, ec);
767
768 if (!BEAST_EXPECTS(!ec, ec.message()))
769 return;
770 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
771 BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos);
772
773 // with ELB_SUPPORT, status still does not indicate a problem
774 env.app().config().ELB_SUPPORT = true;
775
776 doRequest(yield, makeHTTPRequest(*ip_ws, *port_ws, "", {}), *ip_ws, *port_ws, false, resp, ec);
777
778 if (!BEAST_EXPECTS(!ec, ec.message()))
779 return;
780 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
781 BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos);
782 }
783
784 void
785 testAmendmentBlock(boost::asio::yield_context& yield)
786 {
787 testcase("Status request over WS and RPC with/without Amendment Block");
788 using namespace jtx;
789 using namespace boost::asio;
790 using namespace boost::beast::http;
791 Env env{
792 *this,
793 validator(
795 cfg->section("port_rpc").set("protocol", "http");
796 return cfg;
797 }),
798 "")};
799
800 env.close();
801
802 // advance the ledger so that server status
803 // sees a published ledger -- without this, we get a status
804 // failure message about no published ledgers
806
807 // make an RPC server info request and look for
808 // amendment_blocked status
809 auto si = env.rpc("server_info")[jss::result];
810 BEAST_EXPECT(si.isMember(jss::info));
811 BEAST_EXPECT(!si[jss::info].isMember(jss::amendment_blocked));
812 BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true);
813 BEAST_EXPECT(!si.isMember(jss::warnings));
814
815 // make an RPC server state request and look for
816 // amendment_blocked status
817 si = env.rpc("server_state")[jss::result];
818 BEAST_EXPECT(si.isMember(jss::state));
819 BEAST_EXPECT(!si[jss::state].isMember(jss::amendment_blocked));
820 BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == true);
821 BEAST_EXPECT(!si[jss::state].isMember(jss::warnings));
822
823 auto const port_ws = env.app().config()["port_ws"].get<std::uint16_t>("port");
824 auto const ip_ws = env.app().config()["port_ws"].get<std::string>("ip");
825
826 boost::system::error_code ec;
827 response<string_body> resp;
828
829 doRequest(yield, makeHTTPRequest(*ip_ws, *port_ws, "", {}), *ip_ws, *port_ws, false, resp, ec);
830
831 if (!BEAST_EXPECTS(!ec, ec.message()))
832 return;
833 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
834 BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos);
835
836 // mark the Network as Amendment Blocked, but still won't fail until
837 // ELB is enabled (next step)
839 env.app().getOPs().beginConsensus(env.closed()->header().hash, {});
840
841 // consensus now sees validation disabled
842 BEAST_EXPECT(env.app().getOPs().getConsensusInfo()["validating"] == false);
843
844 // RPC request server_info again, now AB should be returned
845 si = env.rpc("server_info")[jss::result];
846 BEAST_EXPECT(si.isMember(jss::info));
847 BEAST_EXPECT(si[jss::info].isMember(jss::amendment_blocked) && si[jss::info][jss::amendment_blocked] == true);
848 BEAST_EXPECT(
849 si[jss::info].isMember(jss::warnings) && si[jss::info][jss::warnings].isArray() &&
850 si[jss::info][jss::warnings].size() == 1 &&
851 si[jss::info][jss::warnings][0u][jss::id].asInt() == warnRPC_AMENDMENT_BLOCKED);
852
853 // RPC request server_state again, now AB should be returned
854 si = env.rpc("server_state")[jss::result];
855 BEAST_EXPECT(si[jss::state].isMember(jss::amendment_blocked) && si[jss::state][jss::amendment_blocked] == true);
856 BEAST_EXPECT(
857 si[jss::state].isMember(jss::warnings) && si[jss::state][jss::warnings].isArray() &&
858 si[jss::state][jss::warnings].size() == 1 &&
859 si[jss::state][jss::warnings][0u][jss::id].asInt() == warnRPC_AMENDMENT_BLOCKED);
860
861 // but status does not indicate because it still relies on ELB
862 // being enabled
863 doRequest(yield, makeHTTPRequest(*ip_ws, *port_ws, "", {}), *ip_ws, *port_ws, false, resp, ec);
864
865 if (!BEAST_EXPECTS(!ec, ec.message()))
866 return;
867 BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
868 BEAST_EXPECT(resp.body().find("connectivity is working.") != std::string::npos);
869
870 env.app().config().ELB_SUPPORT = true;
871
872 doRequest(yield, makeHTTPRequest(*ip_ws, *port_ws, "", {}), *ip_ws, *port_ws, false, resp, ec);
873
874 if (!BEAST_EXPECTS(!ec, ec.message()))
875 return;
876 BEAST_EXPECT(resp.result() == boost::beast::http::status::internal_server_error);
877 BEAST_EXPECT(resp.body().find("cannot accept clients:") != std::string::npos);
878 BEAST_EXPECT(resp.body().find("Server version too old") != std::string::npos);
879 }
880
881 void
882 testRPCRequests(boost::asio::yield_context& yield)
883 {
884 testcase("RPC client sends assorted input");
885
886 using namespace test::jtx;
887 Env env{*this};
888
889 boost::system::error_code ec;
890 {
891 boost::beast::http::response<boost::beast::http::string_body> resp;
892 doHTTPRequest(env, yield, false, resp, ec, "{}");
893 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
894 BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
895 }
896
897 {
898 boost::beast::http::response<boost::beast::http::string_body> resp;
899 Json::Value jv;
900 jv["invalid"] = 1;
901 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
902 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
903 BEAST_EXPECT(resp.body() == "Null method\r\n");
904 }
905
906 {
907 boost::beast::http::response<boost::beast::http::string_body> resp;
909 jv.append("invalid");
910 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
911 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
912 BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
913 }
914
915 {
916 boost::beast::http::response<boost::beast::http::string_body> resp;
918 Json::Value j;
919 j["invalid"] = 1;
920 jv.append(j);
921 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
922 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
923 BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
924 }
925
926 {
927 boost::beast::http::response<boost::beast::http::string_body> resp;
928 Json::Value jv;
929 jv[jss::method] = "batch";
930 jv[jss::params] = 2;
931 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
932 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
933 BEAST_EXPECT(resp.body() == "Malformed batch request\r\n");
934 }
935
936 {
937 boost::beast::http::response<boost::beast::http::string_body> resp;
938 Json::Value jv;
939 jv[jss::method] = "batch";
940 jv[jss::params] = Json::objectValue;
941 jv[jss::params]["invalid"] = 3;
942 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
943 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
944 BEAST_EXPECT(resp.body() == "Malformed batch request\r\n");
945 }
946
947 Json::Value jv;
948 {
949 boost::beast::http::response<boost::beast::http::string_body> resp;
950 jv[jss::method] = Json::nullValue;
951 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
952 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
953 BEAST_EXPECT(resp.body() == "Null method\r\n");
954 }
955
956 {
957 boost::beast::http::response<boost::beast::http::string_body> resp;
958 jv[jss::method] = 1;
959 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
960 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
961 BEAST_EXPECT(resp.body() == "method is not string\r\n");
962 }
963
964 {
965 boost::beast::http::response<boost::beast::http::string_body> resp;
966 jv[jss::method] = "";
967 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
968 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
969 BEAST_EXPECT(resp.body() == "method is empty\r\n");
970 }
971
972 {
973 boost::beast::http::response<boost::beast::http::string_body> resp;
974 jv[jss::method] = "some_method";
975 jv[jss::params] = "params";
976 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
977 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
978 BEAST_EXPECT(resp.body() == "params unparsable\r\n");
979 }
980
981 {
982 boost::beast::http::response<boost::beast::http::string_body> resp;
983 jv[jss::params] = Json::arrayValue;
984 jv[jss::params][0u] = "not an object";
985 doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
986 BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
987 BEAST_EXPECT(resp.body() == "params unparsable\r\n");
988 }
989 }
990
991 void
992 testStatusNotOkay(boost::asio::yield_context& yield)
993 {
994 testcase("Server status not okay");
995
996 using namespace test::jtx;
997 Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
998 cfg->ELB_SUPPORT = true;
999 return cfg;
1000 })};
1001
1002 // raise the fee so that the server is considered overloaded
1003 env.app().getFeeTrack().raiseLocalFee();
1004
1005 boost::beast::http::response<boost::beast::http::string_body> resp;
1006 boost::system::error_code ec;
1007 doHTTPRequest(env, yield, false, resp, ec);
1008 BEAST_EXPECT(resp.result() == boost::beast::http::status::internal_server_error);
1009 std::regex body{"Server cannot accept clients"};
1010 BEAST_EXPECT(std::regex_search(resp.body(), body));
1011 }
1012
1013public:
1014 void
1015 run() override
1016 {
1017 for (auto it : {"http", "ws", "ws2"})
1018 {
1019 testAdminRequest(it, true, true);
1020 testAdminRequest(it, true, false);
1021 testAdminRequest(it, false, false);
1022 }
1023
1024 yield_to([&](boost::asio::yield_context& yield) {
1026 testStatusRequest(yield);
1028
1029 // these are secure/insecure protocol pairs, i.e. for
1030 // each item, the second value is the secure or insecure equivalent
1031 testCantConnect("ws", "wss", yield);
1032 testCantConnect("ws2", "wss2", yield);
1033 testCantConnect("http", "https", yield);
1034 testCantConnect("wss", "ws", yield);
1035 testCantConnect("wss2", "ws2", yield);
1036 testCantConnect("https", "http", yield);
1037
1038 testAmendmentWarning(yield);
1039 testAmendmentBlock(yield);
1040 testAuth(false, yield);
1041 testAuth(true, yield);
1042 testLimit(yield, 5);
1043 testLimit(yield, 0);
1044 testWSHandoff(yield);
1045 testNoRPC(yield);
1046 testWSRequests(yield);
1047 testRPCRequests(yield);
1048 testStatusNotOkay(yield);
1049 });
1050 }
1051};
1052
1053BEAST_DEFINE_TESTSUITE(ServerStatus, server, xrpl);
1054
1055} // namespace test
1056} // namespace xrpl
T back(T... args)
Unserialize a JSON document into a Value.
Definition json_reader.h:18
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:131
Value & append(Value const &value)
Append value to array at the end.
Mix-in to support tests using asio coroutines.
Definition yield_to.h:29
boost::asio::io_context & get_io_context()
Return the io_context associated with the object.
Definition yield_to.h:60
void yield_to(F0 &&f0, FN &&... fn)
Run one or more functions, each in a coroutine.
Definition yield_to.h:99
A testsuite class.
Definition suite.h:52
void pass()
Record a successful test condition.
Definition suite.h:495
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:148
virtual Config & config()=0
Section & section(std::string const &name)
Returns the section with the given name.
bool ELB_SUPPORT
Definition Config.h:120
virtual void setAmendmentBlocked()=0
virtual bool beginConsensus(uint256 const &netLCL, std::unique_ptr< std::stringstream > const &clog)=0
virtual Json::Value getConsensusInfo()=0
virtual void setAmendmentWarned()=0
std::optional< T > get(std::string const &name) const
virtual NetworkOPs & getOPs()=0
virtual LoadFeeTrack & getFeeTrack()=0
virtual LedgerMaster & getLedgerMaster()=0
void testWSClientToHttpServer(boost::asio::yield_context &yield)
auto makeAdminRequest(jtx::Env &env, std::string const &proto, std::string const &user, std::string const &password, bool subobject=false)
void doWSRequest(test::jtx::Env &env, boost::asio::yield_context &yield, bool secure, boost::beast::http::response< boost::beast::http::string_body > &resp, boost::system::error_code &ec)
auto makeConfig(std::string const &proto, bool admin=true, bool credentials=false)
void testAmendmentBlock(boost::asio::yield_context &yield)
auto makeHTTPRequest(std::string const &host, uint16_t port, std::string const &body, myFields const &fields)
void testNoRPC(boost::asio::yield_context &yield)
void testWSHandoff(boost::asio::yield_context &yield)
void testTruncatedWSUpgrade(boost::asio::yield_context &yield)
void testAdminRequest(std::string const &proto, bool admin, bool credentials)
void testRPCRequests(boost::asio::yield_context &yield)
void testStatusNotOkay(boost::asio::yield_context &yield)
void run() override
Runs the suite.
void testAmendmentWarning(boost::asio::yield_context &yield)
void testAuth(bool secure, boost::asio::yield_context &yield)
void testLimit(boost::asio::yield_context &yield, int limit)
void testWSRequests(boost::asio::yield_context &yield)
void doHTTPRequest(test::jtx::Env &env, boost::asio::yield_context &yield, bool secure, boost::beast::http::response< boost::beast::http::string_body > &resp, boost::system::error_code &ec, std::string const &body="", myFields const &fields={})
auto makeWSUpgrade(std::string const &host, uint16_t port)
void testCantConnect(std::string const &client_protocol, std::string const &server_protocol, boost::asio::yield_context &yield)
void testStatusRequest(boost::asio::yield_context &yield)
void doRequest(boost::asio::yield_context &yield, boost::beast::http::request< boost::beast::http::string_body > &&req, std::string const &host, uint16_t port, bool secure, boost::beast::http::response< boost::beast::http::string_body > &resp, boost::system::error_code &ec)
A transaction testing environment.
Definition Env.h:98
Application & app()
Definition Env.h:230
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:97
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:91
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:749
T data(T... args)
T emplace_back(T... args)
T empty(T... args)
T make_pair(T... args)
@ nullValue
'null' value
Definition json_value.h:20
@ arrayValue
array value (ordered list)
Definition json_value.h:26
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:27
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:35
std::unique_ptr< Config > validator(std::unique_ptr< Config >, std::string const &)
adjust configuration with params needed to be a validator
Definition envconfig.cpp:94
char const * getEnvLocalhostAddr()
Definition envconfig.h:17
std::unique_ptr< AbstractClient > makeJSONRPCClient(Config const &cfg, unsigned rpc_version)
Returns a client using JSON-RPC over HTTP/S.
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpc_version, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:285
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:598
std::string base64_encode(std::uint8_t const *data, std::size_t len)
@ warnRPC_AMENDMENT_BLOCKED
Definition ErrorCodes.h:154
@ warnRPC_UNSUPPORTED_MAJORITY
Definition ErrorCodes.h:153
T regex_search(T... args)
T size(T... args)
T to_string(T... args)