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