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