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