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