rippled
Loading...
Searching...
No Matches
ServerHandler.cpp
1#include <xrpld/app/main/Application.h>
2#include <xrpld/app/misc/NetworkOPs.h>
3#include <xrpld/core/ConfigSections.h>
4#include <xrpld/overlay/Overlay.h>
5#include <xrpld/rpc/RPCHandler.h>
6#include <xrpld/rpc/Role.h>
7#include <xrpld/rpc/ServerHandler.h>
8#include <xrpld/rpc/detail/Tuning.h>
9#include <xrpld/rpc/detail/WSInfoSub.h>
10#include <xrpld/rpc/json_body.h>
11
12#include <xrpl/basics/Log.h>
13#include <xrpl/basics/base64.h>
14#include <xrpl/basics/contract.h>
15#include <xrpl/basics/make_SSLContext.h>
16#include <xrpl/beast/net/IPAddressConversion.h>
17#include <xrpl/beast/rfc2616.h>
18#include <xrpl/core/JobQueue.h>
19#include <xrpl/json/json_reader.h>
20#include <xrpl/json/to_string.h>
21#include <xrpl/protocol/ApiVersion.h>
22#include <xrpl/protocol/ErrorCodes.h>
23#include <xrpl/protocol/RPCErr.h>
24#include <xrpl/resource/Fees.h>
25#include <xrpl/resource/ResourceManager.h>
26#include <xrpl/server/Server.h>
27#include <xrpl/server/SimpleWriter.h>
28#include <xrpl/server/detail/JSONRPCUtil.h>
29
30#include <boost/algorithm/string.hpp>
31#include <boost/beast/http/fields.hpp>
32#include <boost/beast/http/string_body.hpp>
33
34#include <algorithm>
35#include <memory>
36#include <stdexcept>
37
38namespace xrpl {
39
40class Peer;
41class LedgerMaster;
42class Transaction;
43class ValidatorKeys;
44class CanonicalTXSet;
45
46static bool
48{
49 return request.version() >= 11 && request.target() == "/" &&
50 request.body().size() == 0 &&
51 request.method() == boost::beast::http::verb::get;
52}
53
54static Handoff
56 http_request_type const& request,
57 boost::beast::http::status status)
58{
59 using namespace boost::beast::http;
60 Handoff handoff;
61 response<string_body> msg;
62 msg.version(request.version());
63 msg.result(status);
64 msg.insert("Server", BuildInfo::getFullVersionString());
65 msg.insert("Content-Type", "text/html");
66 msg.insert("Connection", "close");
67 msg.body() = "Invalid protocol.";
68 msg.prepare_payload();
70 return handoff;
71}
72
73// VFALCO TODO Rewrite to use boost::beast::http::fields
74static bool
76{
77 if (port.user.empty() || port.password.empty())
78 return true;
79
80 auto const it = h.find("authorization");
81 if ((it == h.end()) || (it->second.substr(0, 6) != "Basic "))
82 return false;
83 std::string strUserPass64 = it->second.substr(6);
84 boost::trim(strUserPass64);
85 std::string strUserPass = base64_decode(strUserPass64);
86 std::string::size_type nColon = strUserPass.find(":");
87 if (nColon == std::string::npos)
88 return false;
89 std::string strUser = strUserPass.substr(0, nColon);
90 std::string strPassword = strUserPass.substr(nColon + 1);
91 return strUser == port.user && strPassword == port.password;
92}
93
96 Application& app,
97 boost::asio::io_context& io_context,
98 JobQueue& jobQueue,
99 NetworkOPs& networkOPs,
100 Resource::Manager& resourceManager,
102 : app_(app)
103 , m_resourceManager(resourceManager)
104 , m_journal(app_.journal("Server"))
105 , m_networkOPs(networkOPs)
106 , m_server(make_Server(*this, io_context, app_.journal("Server")))
107 , m_jobQueue(jobQueue)
108{
109 auto const& group(cm.group("rpc"));
110 rpc_requests_ = group->make_counter("requests");
111 rpc_size_ = group->make_event("size");
112 rpc_time_ = group->make_event("time");
113}
114
116{
117 m_server = nullptr;
118}
119
120void
122{
123 setup_ = setup;
124 endpoints_ = m_server->ports(setup.ports);
125
126 // fix auto ports
127 for (auto& port : setup_.ports)
128 {
129 if (auto it = endpoints_.find(port.name); it != endpoints_.end())
130 {
131 auto const endpointPort = it->second.port();
132 if (!port.port)
133 port.port = endpointPort;
134
135 if (!setup_.client.port &&
136 (port.protocol.count("http") > 0 ||
137 port.protocol.count("https") > 0))
138 setup_.client.port = endpointPort;
139
140 if (!setup_.overlay.port() && (port.protocol.count("peer") > 0))
141 setup_.overlay.port(endpointPort);
142 }
143 }
144}
145
146//------------------------------------------------------------------------------
147
148void
150{
151 m_server->close();
152 {
154 condition_.wait(lock, [this] { return stopped_; });
155 }
156}
157
158//------------------------------------------------------------------------------
159
160bool
162 Session& session,
163 boost::asio::ip::tcp::endpoint endpoint)
164{
165 auto const& port = session.port();
166
167 auto const c = [this, &port]() {
169 return ++count_[port];
170 }();
171
172 if (port.limit && c >= port.limit)
173 {
174 JLOG(m_journal.trace())
175 << port.name << " is full; dropping " << endpoint;
176 return false;
177 }
178
179 return true;
180}
181
184 Session& session,
186 http_request_type&& request,
187 boost::asio::ip::tcp::endpoint const& remote_address)
188{
189 using namespace boost::beast;
190 auto const& p{session.port().protocol};
191 bool const is_ws{
192 p.count("ws") > 0 || p.count("ws2") > 0 || p.count("wss") > 0 ||
193 p.count("wss2") > 0};
194
195 if (websocket::is_upgrade(request))
196 {
197 if (!is_ws)
198 return statusRequestResponse(request, http::status::unauthorized);
199
201 try
202 {
203 ws = session.websocketUpgrade();
204 }
205 catch (std::exception const& e)
206 {
207 JLOG(m_journal.error())
208 << "Exception upgrading websocket: " << e.what() << "\n";
210 request, http::status::internal_server_error);
211 }
212
214 auto const beast_remote_address =
216 is->getConsumer() = requestInboundEndpoint(
218 beast_remote_address,
221 session.port(),
222 Json::Value(),
223 beast_remote_address,
224 is->user()),
225 is->user(),
226 is->forwarded_for());
227 ws->appDefined = std::move(is);
228 ws->run();
229
230 Handoff handoff;
231 handoff.moved = true;
232 return handoff;
233 }
234
235 if (bundle && p.count("peer") > 0)
236 return app_.overlay().onHandoff(
237 std::move(bundle), std::move(request), remote_address);
238
239 if (is_ws && isStatusRequest(request))
240 return statusResponse(request);
241
242 // Otherwise pass to legacy onRequest or websocket
243 return {};
244}
245
246static inline Json::Output
248{
249 return [&](boost::beast::string_view const& b) {
250 session.write(b.data(), b.size());
251 };
252}
253
255build_map(boost::beast::http::fields const& h)
256{
258 for (auto const& e : h)
259 {
260 // key cannot be a std::string_view because it needs to be used in
261 // map and along with iterators
262 std::string key(e.name_string());
263 std::transform(key.begin(), key.end(), key.begin(), [](auto kc) {
264 return std::tolower(static_cast<unsigned char>(kc));
265 });
266 c[key] = e.value();
267 }
268 return c;
269}
270
271template <class ConstBufferSequence>
272static std::string
273buffers_to_string(ConstBufferSequence const& bs)
274{
275 using boost::asio::buffer_size;
276 std::string s;
277 s.reserve(buffer_size(bs));
278 // Use auto&& so the right thing happens whether bs returns a copy or
279 // a reference
280 for (auto&& b : bs)
281 s.append(static_cast<char const*>(b.data()), buffer_size(b));
282 return s;
283}
284
285void
287{
288 // Make sure RPC is enabled on the port
289 if (session.port().protocol.count("http") == 0 &&
290 session.port().protocol.count("https") == 0)
291 {
292 HTTPReply(403, "Forbidden", makeOutput(session), app_.journal("RPC"));
293 session.close(true);
294 return;
295 }
296
297 // Check user/password authorization
298 if (!authorized(session.port(), build_map(session.request())))
299 {
300 HTTPReply(403, "Forbidden", makeOutput(session), app_.journal("RPC"));
301 session.close(true);
302 return;
303 }
304
305 std::shared_ptr<Session> detachedSession = session.detach();
306 auto const postResult = m_jobQueue.postCoro(
308 "RPC-Client",
309 [this, detachedSession](std::shared_ptr<JobQueue::Coro> coro) {
310 processSession(detachedSession, coro);
311 });
312 if (postResult == nullptr)
313 {
314 // The coroutine was rejected, probably because we're shutting down.
315 HTTPReply(
316 503,
317 "Service Unavailable",
318 makeOutput(*detachedSession),
319 app_.journal("RPC"));
320 detachedSession->close(true);
321 return;
322 }
323}
324
325void
329{
330 Json::Value jv;
331 auto const size = boost::asio::buffer_size(buffers);
332 if (size > RPC::Tuning::maxRequestSize ||
333 !Json::Reader{}.parse(jv, buffers) || !jv.isObject())
334 {
336 jvResult[jss::type] = jss::error;
337 jvResult[jss::error] = "jsonInvalid";
338 jvResult[jss::value] = buffers_to_string(buffers);
339 boost::beast::multi_buffer sb;
340 Json::stream(jvResult, [&sb](auto const p, auto const n) {
341 sb.commit(boost::asio::buffer_copy(
342 sb.prepare(n), boost::asio::buffer(p, n)));
343 });
344 JLOG(m_journal.trace()) << "Websocket sending '" << jvResult << "'";
345 session->send(
346 std::make_shared<StreambufWSMsg<decltype(sb)>>(std::move(sb)));
347 session->complete();
348 return;
349 }
350
351 JLOG(m_journal.trace()) << "Websocket received '" << jv << "'";
352
353 auto const postResult = m_jobQueue.postCoro(
355 "WS-Client",
356 [this, session, jv = std::move(jv)](
358 auto const jr = this->processSession(session, coro, jv);
359 auto const s = to_string(jr);
360 auto const n = s.length();
361 boost::beast::multi_buffer sb(n);
362 sb.commit(boost::asio::buffer_copy(
363 sb.prepare(n), boost::asio::buffer(s.c_str(), n)));
364 session->send(
365 std::make_shared<StreambufWSMsg<decltype(sb)>>(std::move(sb)));
366 session->complete();
367 });
368 if (postResult == nullptr)
369 {
370 // The coroutine was rejected, probably because we're shutting down.
371 session->close({boost::beast::websocket::going_away, "Shutting Down"});
372 }
373}
374
375void
376ServerHandler::onClose(Session& session, boost::system::error_code const&)
377{
379 --count_[session.port()];
380}
381
382void
389
390//------------------------------------------------------------------------------
391
392template <class T>
393void
395 Json::Value const& request,
396 T const& duration,
397 beast::Journal& journal)
398{
399 using namespace std::chrono_literals;
400 auto const level = (duration >= 10s) ? journal.error()
401 : (duration >= 1s) ? journal.warn()
402 : journal.debug();
403
404 JLOG(level) << "RPC request processing duration = "
405 << std::chrono::duration_cast<std::chrono::microseconds>(
406 duration)
407 .count()
408 << " microseconds. request = " << request;
409}
410
413 std::shared_ptr<WSSession> const& session,
415 Json::Value const& jv)
416{
417 auto is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
418 if (is->getConsumer().disconnect(m_journal))
419 {
420 session->close(
421 {boost::beast::websocket::policy_error, "threshold exceeded"});
422 // FIX: This rpcError is not delivered since the session
423 // was just closed.
424 return rpcError(rpcSLOW_DOWN);
425 }
426
427 // Requests without "command" are invalid.
430 try
431 {
432 auto apiVersion =
434 if (apiVersion == RPC::apiInvalidVersion ||
435 (!jv.isMember(jss::command) && !jv.isMember(jss::method)) ||
436 (jv.isMember(jss::command) && !jv[jss::command].isString()) ||
437 (jv.isMember(jss::method) && !jv[jss::method].isString()) ||
438 (jv.isMember(jss::command) && jv.isMember(jss::method) &&
439 jv[jss::command].asString() != jv[jss::method].asString()))
440 {
441 jr[jss::type] = jss::response;
442 jr[jss::status] = jss::error;
443 jr[jss::error] = apiVersion == RPC::apiInvalidVersion
444 ? jss::invalid_API_version
445 : jss::missingCommand;
446 jr[jss::request] = jv;
447 if (jv.isMember(jss::id))
448 jr[jss::id] = jv[jss::id];
449 if (jv.isMember(jss::jsonrpc))
450 jr[jss::jsonrpc] = jv[jss::jsonrpc];
451 if (jv.isMember(jss::ripplerpc))
452 jr[jss::ripplerpc] = jv[jss::ripplerpc];
453 if (jv.isMember(jss::api_version))
454 jr[jss::api_version] = jv[jss::api_version];
455
456 is->getConsumer().charge(Resource::feeMalformedRPC);
457 return jr;
458 }
459
460 auto required = RPC::roleRequired(
461 apiVersion,
463 jv.isMember(jss::command) ? jv[jss::command].asString()
464 : jv[jss::method].asString());
465 auto role = requestRole(
466 required,
467 session->port(),
468 jv,
469 beast::IP::from_asio(session->remote_endpoint().address()),
470 is->user());
471 if (Role::FORBID == role)
472 {
473 loadType = Resource::feeMalformedRPC;
474 jr[jss::result] = rpcError(rpcFORBIDDEN);
475 }
476 else
477 {
478 RPC::JsonContext context{
479 {app_.journal("RPCHandler"),
480 app_,
481 loadType,
482 app_.getOPs(),
484 is->getConsumer(),
485 role,
486 coro,
487 is,
488 apiVersion},
489 jv,
490 {is->user(), is->forwarded_for()}};
491
492 auto start = std::chrono::system_clock::now();
493 RPC::doCommand(context, jr[jss::result]);
495 logDuration(jv, end - start, m_journal);
496 }
497 }
498 catch (std::exception const& ex)
499 {
500 // LCOV_EXCL_START
501 jr[jss::result] = RPC::make_error(rpcINTERNAL);
502 JLOG(m_journal.error())
503 << "Exception while processing WS: " << ex.what() << "\n"
504 << "Input JSON: " << Json::Compact{Json::Value{jv}};
505 // LCOV_EXCL_STOP
506 }
507
508 is->getConsumer().charge(loadType);
509 if (is->getConsumer().warn())
510 jr[jss::warning] = jss::load;
511
512 // Currently we will simply unwrap errors returned by the RPC
513 // API, in the future maybe we can make the responses
514 // consistent.
515 //
516 // Regularize result. This is duplicate code.
517 if (jr[jss::result].isMember(jss::error))
518 {
519 jr = jr[jss::result];
520 jr[jss::status] = jss::error;
521
522 auto rq = jv;
523
524 if (rq.isObject())
525 {
526 if (rq.isMember(jss::passphrase.c_str()))
527 rq[jss::passphrase.c_str()] = "<masked>";
528 if (rq.isMember(jss::secret.c_str()))
529 rq[jss::secret.c_str()] = "<masked>";
530 if (rq.isMember(jss::seed.c_str()))
531 rq[jss::seed.c_str()] = "<masked>";
532 if (rq.isMember(jss::seed_hex.c_str()))
533 rq[jss::seed_hex.c_str()] = "<masked>";
534 }
535
536 jr[jss::request] = rq;
537 }
538 else
539 {
540 if (jr[jss::result].isMember("forwarded") &&
541 jr[jss::result]["forwarded"])
542 jr = jr[jss::result];
543 jr[jss::status] = jss::success;
544 }
545
546 if (jv.isMember(jss::id))
547 jr[jss::id] = jv[jss::id];
548 if (jv.isMember(jss::jsonrpc))
549 jr[jss::jsonrpc] = jv[jss::jsonrpc];
550 if (jv.isMember(jss::ripplerpc))
551 jr[jss::ripplerpc] = jv[jss::ripplerpc];
552 if (jv.isMember(jss::api_version))
553 jr[jss::api_version] = jv[jss::api_version];
554
555 jr[jss::type] = jss::response;
556 return jr;
557}
558
559// Run as a coroutine.
560void
562 std::shared_ptr<Session> const& session,
564{
566 session->port(),
567 buffers_to_string(session->request().body().data()),
568 session->remoteAddress().at_port(0),
569 makeOutput(*session),
570 coro,
571 forwardedFor(session->request()),
572 [&] {
573 auto const iter = session->request().find("X-User");
574 if (iter != session->request().end())
575 return iter->value();
576 return boost::beast::string_view{};
577 }());
578
579 if (beast::rfc2616::is_keep_alive(session->request()))
580 session->complete();
581 else
582 session->close(true);
583}
584
585static Json::Value
587{
589 sub["code"] = code;
590 sub["message"] = std::move(message);
592 r["error"] = sub;
593 return r;
594}
595
596Json::Int constexpr method_not_found = -32601;
597Json::Int constexpr server_overloaded = -32604;
598Json::Int constexpr forbidden = -32605;
599Json::Int constexpr wrong_version = -32606;
600
601void
602ServerHandler::processRequest(
603 Port const& port,
604 std::string const& request,
605 beast::IP::Endpoint const& remoteIPAddress,
606 Output&& output,
608 std::string_view forwardedFor,
609 std::string_view user)
610{
611 auto rpcJ = app_.journal("RPC");
612
613 Json::Value jsonOrig;
614 {
615 Json::Reader reader;
616 if ((request.size() > RPC::Tuning::maxRequestSize) ||
617 !reader.parse(request, jsonOrig) || !jsonOrig ||
618 !jsonOrig.isObject())
619 {
620 HTTPReply(
621 400,
622 "Unable to parse request: " +
624 output,
625 rpcJ);
626 return;
627 }
628 }
629
630 bool batch = false;
631 unsigned size = 1;
632 if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == "batch")
633 {
634 batch = true;
635 if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
636 {
637 HTTPReply(400, "Malformed batch request", output, rpcJ);
638 return;
639 }
640 size = jsonOrig[jss::params].size();
641 }
642
644 auto const start(std::chrono::high_resolution_clock::now());
645 for (unsigned i = 0; i < size; ++i)
646 {
647 Json::Value const& jsonRPC =
648 batch ? jsonOrig[jss::params][i] : jsonOrig;
649
650 if (!jsonRPC.isObject())
651 {
653 r[jss::request] = jsonRPC;
654 r[jss::error] =
655 make_json_error(method_not_found, "Method not found");
656 reply.append(r);
657 continue;
658 }
659
660 unsigned apiVersion = RPC::apiVersionIfUnspecified;
661 if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
662 jsonRPC[jss::params].size() > 0 &&
663 jsonRPC[jss::params][0u].isObject())
664 {
665 apiVersion = RPC::getAPIVersionNumber(
666 jsonRPC[jss::params][Json::UInt(0)],
667 app_.config().BETA_RPC_API);
668 }
669
670 if (apiVersion == RPC::apiVersionIfUnspecified && batch)
671 {
672 // for batch request, api_version may be at a different level
673 apiVersion =
674 RPC::getAPIVersionNumber(jsonRPC, app_.config().BETA_RPC_API);
675 }
676
677 if (apiVersion == RPC::apiInvalidVersion)
678 {
679 if (!batch)
680 {
681 HTTPReply(400, jss::invalid_API_version.c_str(), output, rpcJ);
682 return;
683 }
685 r[jss::request] = jsonRPC;
686 r[jss::error] = make_json_error(
687 wrong_version, jss::invalid_API_version.c_str());
688 reply.append(r);
689 continue;
690 }
691
692 /* ------------------------------------------------------------------ */
693 auto role = Role::FORBID;
694 auto required = Role::FORBID;
695 if (jsonRPC.isMember(jss::method) && jsonRPC[jss::method].isString())
696 required = RPC::roleRequired(
697 apiVersion,
698 app_.config().BETA_RPC_API,
699 jsonRPC[jss::method].asString());
700
701 if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
702 jsonRPC[jss::params].size() > 0 &&
703 jsonRPC[jss::params][Json::UInt(0)].isObjectOrNull())
704 {
705 role = requestRole(
706 required,
707 port,
708 jsonRPC[jss::params][Json::UInt(0)],
709 remoteIPAddress,
710 user);
711 }
712 else
713 {
714 role = requestRole(
715 required, port, Json::objectValue, remoteIPAddress, user);
716 }
717
718 Resource::Consumer usage;
719 if (isUnlimited(role))
720 {
721 usage = m_resourceManager.newUnlimitedEndpoint(remoteIPAddress);
722 }
723 else
724 {
725 usage = m_resourceManager.newInboundEndpoint(
726 remoteIPAddress, role == Role::PROXY, forwardedFor);
727 if (usage.disconnect(m_journal))
728 {
729 if (!batch)
730 {
731 HTTPReply(503, "Server is overloaded", output, rpcJ);
732 return;
733 }
734 Json::Value r = jsonRPC;
735 r[jss::error] =
736 make_json_error(server_overloaded, "Server is overloaded");
737 reply.append(r);
738 continue;
739 }
740 }
741
742 if (role == Role::FORBID)
743 {
744 usage.charge(Resource::feeMalformedRPC);
745 if (!batch)
746 {
747 HTTPReply(403, "Forbidden", output, rpcJ);
748 return;
749 }
750 Json::Value r = jsonRPC;
751 r[jss::error] = make_json_error(forbidden, "Forbidden");
752 reply.append(r);
753 continue;
754 }
755
756 if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull())
757 {
758 usage.charge(Resource::feeMalformedRPC);
759 if (!batch)
760 {
761 HTTPReply(400, "Null method", output, rpcJ);
762 return;
763 }
764 Json::Value r = jsonRPC;
765 r[jss::error] = make_json_error(method_not_found, "Null method");
766 reply.append(r);
767 continue;
768 }
769
770 Json::Value const& method = jsonRPC[jss::method];
771 if (!method.isString())
772 {
773 usage.charge(Resource::feeMalformedRPC);
774 if (!batch)
775 {
776 HTTPReply(400, "method is not string", output, rpcJ);
777 return;
778 }
779 Json::Value r = jsonRPC;
780 r[jss::error] =
781 make_json_error(method_not_found, "method is not string");
782 reply.append(r);
783 continue;
784 }
785
786 std::string strMethod = method.asString();
787 if (strMethod.empty())
788 {
789 usage.charge(Resource::feeMalformedRPC);
790 if (!batch)
791 {
792 HTTPReply(400, "method is empty", output, rpcJ);
793 return;
794 }
795 Json::Value r = jsonRPC;
796 r[jss::error] =
797 make_json_error(method_not_found, "method is empty");
798 reply.append(r);
799 continue;
800 }
801
802 // Extract request parameters from the request Json as `params`.
803 //
804 // If the field "params" is empty, `params` is an empty object.
805 //
806 // Otherwise, that field must be an array of length 1 (why?)
807 // and we take that first entry and validate that it's an object.
808 Json::Value params;
809 if (!batch)
810 {
811 params = jsonRPC[jss::params];
812 if (!params)
814
815 else if (!params.isArray() || params.size() != 1)
816 {
817 usage.charge(Resource::feeMalformedRPC);
818 HTTPReply(400, "params unparsable", output, rpcJ);
819 return;
820 }
821 else
822 {
823 params = std::move(params[0u]);
824 if (!params.isObjectOrNull())
825 {
826 usage.charge(Resource::feeMalformedRPC);
827 HTTPReply(400, "params unparsable", output, rpcJ);
828 return;
829 }
830 }
831 }
832 else // batch
833 {
834 params = jsonRPC;
835 }
836
837 std::string ripplerpc = "1.0";
838 if (params.isMember(jss::ripplerpc))
839 {
840 if (!params[jss::ripplerpc].isString())
841 {
842 usage.charge(Resource::feeMalformedRPC);
843 if (!batch)
844 {
845 HTTPReply(400, "ripplerpc is not a string", output, rpcJ);
846 return;
847 }
848
849 Json::Value r = jsonRPC;
850 r[jss::error] = make_json_error(
851 method_not_found, "ripplerpc is not a string");
852 reply.append(r);
853 continue;
854 }
855 ripplerpc = params[jss::ripplerpc].asString();
856 }
857
862 if (role != Role::IDENTIFIED && role != Role::PROXY)
863 {
865 user.remove_suffix(user.size());
866 }
867
868 JLOG(m_journal.debug()) << "Query: " << strMethod << params;
869
870 // Provide the JSON-RPC method as the field "command" in the request.
871 params[jss::command] = strMethod;
872 JLOG(m_journal.trace())
873 << "doRpcCommand:" << strMethod << ":" << params;
874
875 Resource::Charge loadType = Resource::feeReferenceRPC;
876
877 RPC::JsonContext context{
878 {m_journal,
879 app_,
880 loadType,
881 m_networkOPs,
882 app_.getLedgerMaster(),
883 usage,
884 role,
885 coro,
887 apiVersion},
888 params,
889 {user, forwardedFor}};
890 Json::Value result;
891
892 auto start = std::chrono::system_clock::now();
893
894 try
895 {
896 RPC::doCommand(context, result);
897 }
898 catch (std::exception const& ex)
899 {
900 // LCOV_EXCL_START
901 result = RPC::make_error(rpcINTERNAL);
902 JLOG(m_journal.error()) << "Internal error : " << ex.what()
903 << " when processing request: "
904 << Json::Compact{Json::Value{params}};
905 // LCOV_EXCL_STOP
906 }
907
909
910 logDuration(params, end - start, m_journal);
911
912 usage.charge(loadType);
913 if (usage.warn())
914 result[jss::warning] = jss::load;
915
917 if (ripplerpc >= "2.0")
918 {
919 if (result.isMember(jss::error))
920 {
921 result[jss::status] = jss::error;
922 result["code"] = result[jss::error_code];
923 result["message"] = result[jss::error_message];
924 result.removeMember(jss::error_message);
925 JLOG(m_journal.debug()) << "rpcError: " << result[jss::error]
926 << ": " << result[jss::error_message];
927 r[jss::error] = std::move(result);
928 }
929 else
930 {
931 result[jss::status] = jss::success;
932 r[jss::result] = std::move(result);
933 }
934 }
935 else
936 {
937 // Always report "status". On an error report the request as
938 // received.
939 if (result.isMember(jss::error))
940 {
941 auto rq = params;
942
943 if (rq.isObject())
944 { // But mask potentially sensitive information.
945 if (rq.isMember(jss::passphrase.c_str()))
946 rq[jss::passphrase.c_str()] = "<masked>";
947 if (rq.isMember(jss::secret.c_str()))
948 rq[jss::secret.c_str()] = "<masked>";
949 if (rq.isMember(jss::seed.c_str()))
950 rq[jss::seed.c_str()] = "<masked>";
951 if (rq.isMember(jss::seed_hex.c_str()))
952 rq[jss::seed_hex.c_str()] = "<masked>";
953 }
954
955 result[jss::status] = jss::error;
956 result[jss::request] = rq;
957
958 JLOG(m_journal.debug()) << "rpcError: " << result[jss::error]
959 << ": " << result[jss::error_message];
960 }
961 else
962 {
963 result[jss::status] = jss::success;
964 }
965 r[jss::result] = std::move(result);
966 }
967
968 if (params.isMember(jss::jsonrpc))
969 r[jss::jsonrpc] = params[jss::jsonrpc];
970 if (params.isMember(jss::ripplerpc))
971 r[jss::ripplerpc] = params[jss::ripplerpc];
972 if (params.isMember(jss::id))
973 r[jss::id] = params[jss::id];
974 if (batch)
975 reply.append(std::move(r));
976 else
977 reply = std::move(r);
978
979 if (reply.isMember(jss::result) &&
980 reply[jss::result].isMember(jss::result))
981 {
982 reply = reply[jss::result];
983 if (reply.isMember(jss::status))
984 {
985 reply[jss::result][jss::status] = reply[jss::status];
986 reply.removeMember(jss::status);
987 }
988 }
989 }
990
991 // If we're returning an error_code, use that to determine the HTTP status.
992 int const httpStatus = [&reply]() {
993 // This feature is enabled with ripplerpc version 3.0 and above.
994 // Before ripplerpc version 3.0 always return 200.
995 if (reply.isMember(jss::ripplerpc) &&
996 reply[jss::ripplerpc].isString() &&
997 reply[jss::ripplerpc].asString() >= "3.0")
998 {
999 // If there's an error_code, use that to determine the HTTP Status.
1000 if (reply.isMember(jss::error) &&
1001 reply[jss::error].isMember(jss::error_code) &&
1002 reply[jss::error][jss::error_code].isInt())
1003 {
1004 int const errCode = reply[jss::error][jss::error_code].asInt();
1005 return RPC::error_code_http_status(
1006 static_cast<error_code_i>(errCode));
1007 }
1008 }
1009 // Return OK.
1010 return 200;
1011 }();
1012
1013 auto response = to_string(reply);
1014
1015 rpc_time_.notify(std::chrono::duration_cast<std::chrono::milliseconds>(
1017 ++rpc_requests_;
1018 rpc_size_.notify(beast::insight::Event::value_type{response.size()});
1019
1020 response += '\n';
1021
1022 if (auto stream = m_journal.debug())
1023 {
1024 static int const maxSize = 10000;
1025 if (response.size() <= maxSize)
1026 stream << "Reply: " << response;
1027 else
1028 stream << "Reply: " << response.substr(0, maxSize);
1029 }
1030
1031 HTTPReply(httpStatus, response, output, rpcJ);
1032}
1033
1034//------------------------------------------------------------------------------
1035
1036/* This response is used with load balancing.
1037 If the server is overloaded, status 500 is reported. Otherwise status 200
1038 is reported, meaning the server can accept more connections.
1039*/
1040Handoff
1041ServerHandler::statusResponse(http_request_type const& request) const
1042{
1043 using namespace boost::beast::http;
1044 Handoff handoff;
1045 response<string_body> msg;
1046 std::string reason;
1047 if (app_.serverOkay(reason))
1048 {
1049 msg.result(boost::beast::http::status::ok);
1050 msg.body() = "<!DOCTYPE html><html><head><title>" + systemName() +
1051 " Test page for rippled</title></head><body><h1>" + systemName() +
1052 " Test</h1><p>This page shows rippled http(s) "
1053 "connectivity is working.</p></body></html>";
1054 }
1055 else
1056 {
1057 msg.result(boost::beast::http::status::internal_server_error);
1058 msg.body() = "<HTML><BODY>Server cannot accept clients: " + reason +
1059 "</BODY></HTML>";
1060 }
1061 msg.version(request.version());
1062 msg.insert("Server", BuildInfo::getFullVersionString());
1063 msg.insert("Content-Type", "text/html");
1064 msg.insert("Connection", "close");
1065 msg.prepare_payload();
1067 return handoff;
1068}
1069
1070//------------------------------------------------------------------------------
1071
1072void
1073ServerHandler::Setup::makeContexts()
1074{
1075 for (auto& p : ports)
1076 {
1077 if (p.secure())
1078 {
1079 if (p.ssl_key.empty() && p.ssl_cert.empty() && p.ssl_chain.empty())
1080 p.context = make_SSLContext(p.ssl_ciphers);
1081 else
1082 p.context = make_SSLContextAuthed(
1083 p.ssl_key, p.ssl_cert, p.ssl_chain, p.ssl_ciphers);
1084 }
1085 else
1086 {
1088 boost::asio::ssl::context::sslv23);
1089 }
1090 }
1091}
1092
1093static Port
1094to_Port(ParsedPort const& parsed, std::ostream& log)
1095{
1096 Port p;
1097 p.name = parsed.name;
1098
1099 if (!parsed.ip)
1100 {
1101 log << "Missing 'ip' in [" << p.name << "]";
1102 Throw<std::exception>();
1103 }
1104 p.ip = *parsed.ip;
1105
1106 if (!parsed.port)
1107 {
1108 log << "Missing 'port' in [" << p.name << "]";
1109 Throw<std::exception>();
1110 }
1111 p.port = *parsed.port;
1112
1113 if (parsed.protocol.empty())
1114 {
1115 log << "Missing 'protocol' in [" << p.name << "]";
1116 Throw<std::exception>();
1117 }
1118 p.protocol = parsed.protocol;
1119
1120 p.user = parsed.user;
1121 p.password = parsed.password;
1122 p.admin_user = parsed.admin_user;
1123 p.admin_password = parsed.admin_password;
1124 p.ssl_key = parsed.ssl_key;
1125 p.ssl_cert = parsed.ssl_cert;
1126 p.ssl_chain = parsed.ssl_chain;
1127 p.ssl_ciphers = parsed.ssl_ciphers;
1128 p.pmd_options = parsed.pmd_options;
1129 p.ws_queue_limit = parsed.ws_queue_limit;
1130 p.limit = parsed.limit;
1131 p.admin_nets_v4 = parsed.admin_nets_v4;
1132 p.admin_nets_v6 = parsed.admin_nets_v6;
1135
1136 return p;
1137}
1138
1139static std::vector<Port>
1140parse_Ports(Config const& config, std::ostream& log)
1141{
1142 std::vector<Port> result;
1143
1144 if (!config.exists("server"))
1145 {
1146 log << "Required section [server] is missing";
1147 Throw<std::exception>();
1148 }
1149
1150 ParsedPort common;
1151 parse_Port(common, config["server"], log);
1152
1153 auto const& names = config.section("server").values();
1154 result.reserve(names.size());
1155 for (auto const& name : names)
1156 {
1157 if (!config.exists(name))
1158 {
1159 log << "Missing section: [" << name << "]";
1160 Throw<std::exception>();
1161 }
1162
1163 // grpc ports are parsed by GRPCServer class. Do not validate
1164 // grpc port information in this file.
1165 if (name == SECTION_PORT_GRPC)
1166 continue;
1167
1168 ParsedPort parsed = common;
1169 parse_Port(parsed, config[name], log);
1170 result.push_back(to_Port(parsed, log));
1171 }
1172
1173 if (config.standalone())
1174 {
1175 auto it = result.begin();
1176
1177 while (it != result.end())
1178 {
1179 auto& p = it->protocol;
1180
1181 // Remove the peer protocol, and if that would
1182 // leave the port empty, remove the port as well
1183 if (p.erase("peer") && p.empty())
1184 it = result.erase(it);
1185 else
1186 ++it;
1187 }
1188 }
1189 else
1190 {
1191 auto const count =
1192 std::count_if(result.cbegin(), result.cend(), [](Port const& p) {
1193 return p.protocol.count("peer") != 0;
1194 });
1195
1196 if (count > 1)
1197 {
1198 log << "Error: More than one peer protocol configured in [server]";
1199 Throw<std::exception>();
1200 }
1201
1202 if (count == 0)
1203 log << "Warning: No peer protocol configured";
1204 }
1205
1206 return result;
1207}
1208
1209// Fill out the client portion of the Setup
1210static void
1212{
1213 decltype(setup.ports)::const_iterator iter;
1214 for (iter = setup.ports.cbegin(); iter != setup.ports.cend(); ++iter)
1215 if (iter->protocol.count("http") > 0 ||
1216 iter->protocol.count("https") > 0)
1217 break;
1218 if (iter == setup.ports.cend())
1219 return;
1220 setup.client.secure = iter->protocol.count("https") > 0;
1221 setup.client.ip = beast::IP::is_unspecified(iter->ip)
1222 ?
1223 // VFALCO HACK! to make localhost work
1224 (iter->ip.is_v6() ? "::1" : "127.0.0.1")
1225 : iter->ip.to_string();
1226 setup.client.port = iter->port;
1227 setup.client.user = iter->user;
1228 setup.client.password = iter->password;
1229 setup.client.admin_user = iter->admin_user;
1230 setup.client.admin_password = iter->admin_password;
1231}
1232
1233// Fill out the overlay portion of the Setup
1234static void
1236{
1237 auto const iter = std::find_if(
1238 setup.ports.cbegin(), setup.ports.cend(), [](Port const& port) {
1239 return port.protocol.count("peer") != 0;
1240 });
1241 if (iter == setup.ports.cend())
1242 {
1243 setup.overlay = {};
1244 return;
1245 }
1246 setup.overlay = {iter->ip, iter->port};
1247}
1248
1249ServerHandler::Setup
1251{
1253 setup.ports = parse_Ports(config, log);
1254
1255 setup_Client(setup);
1256 setup_Overlay(setup);
1257
1258 return setup;
1259}
1260
1263 Application& app,
1264 boost::asio::io_context& io_context,
1265 JobQueue& jobQueue,
1266 NetworkOPs& networkOPs,
1267 Resource::Manager& resourceManager,
1268 CollectorManager& cm)
1269{
1272 app,
1273 io_context,
1274 jobQueue,
1275 networkOPs,
1276 resourceManager,
1277 cm);
1278}
1279
1280} // namespace xrpl
T append(T... args)
T begin(T... args)
Decorator for streaming out compact json.
Unserialize a JSON document into a Value.
Definition json_reader.h:18
std::string getFormattedErrorMessages() const
Returns a user friendly string that list errors in the parsed document.
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:131
bool isArray() const
Value & append(Value const &value)
Append value to array at the end.
UInt size() const
Number of values in array or object.
bool isObjectOrNull() const
Int asInt() const
bool isString() const
bool isObject() const
Value removeMember(char const *key)
Remove and return the named member.
std::string asString() const
Returns the unquoted string value.
bool isNull() const
isNull() tests to see if this field is null.
bool isMember(char const *key) const
Return true if the object has a member named key.
bool isInt() const
A version-independent IP address and port combination.
Definition IPEndpoint.h:19
A generic endpoint for log messages.
Definition Journal.h:41
Stream error() const
Definition Journal.h:327
Stream debug() const
Definition Journal.h:309
Stream trace() const
Severity stream access functions.
Definition Journal.h:303
Stream warn() const
Definition Journal.h:321
virtual Config & config()=0
virtual LedgerMaster & getLedgerMaster()=0
virtual beast::Journal journal(std::string const &name)=0
virtual Overlay & overlay()=0
virtual NetworkOPs & getOPs()=0
bool exists(std::string const &name) const
Returns true if a section with the given name exists.
Section & section(std::string const &name)
Returns the section with the given name.
Provides the beast::insight::Collector service.
virtual beast::insight::Group::ptr const & group(std::string const &name)=0
bool BETA_RPC_API
Definition Config.h:269
bool standalone() const
Definition Config.h:318
A pool of threads to perform work.
Definition JobQueue.h:38
std::shared_ptr< Coro > postCoro(JobType t, std::string const &name, F &&f)
Creates a coroutine and adds a job to the queue which will run it.
Definition JobQueue.h:393
Provides server functionality for clients.
Definition NetworkOPs.h:70
virtual Handoff onHandoff(std::unique_ptr< stream_type > &&bundle, http_request_type &&request, boost::asio::ip::tcp::endpoint remote_address)=0
Conditionally accept an incoming HTTP request.
A consumption charge.
Definition Charge.h:11
An endpoint that consumes resources.
Definition Consumer.h:17
bool warn()
Returns true if the consumer should be warned.
Definition Consumer.cpp:98
bool disconnect(beast::Journal const &j)
Returns true if the consumer should be disconnected.
Definition Consumer.cpp:105
Disposition charge(Charge const &fee, std::string const &context={})
Apply a load charge to the consumer.
Definition Consumer.cpp:87
Tracks load and resource consumption.
std::vector< std::string > const & values() const
Returns all the values in the section.
Definition BasicConfig.h:60
Resource::Manager & m_resourceManager
std::condition_variable condition_
ServerHandler(ServerHandlerCreator const &, Application &app, boost::asio::io_context &io_context, JobQueue &jobQueue, NetworkOPs &networkOPs, Resource::Manager &resourceManager, CollectorManager &cm)
Handoff onHandoff(Session &session, std::unique_ptr< stream_type > &&bundle, http_request_type &&request, boost::asio::ip::tcp::endpoint const &remote_address)
Application & app_
void onClose(Session &session, boost::system::error_code const &)
Setup const & setup() const
bool onAccept(Session &session, boost::asio::ip::tcp::endpoint endpoint)
beast::insight::Event rpc_size_
void onStopped(Server &)
Handoff statusResponse(http_request_type const &request) const
void processRequest(Port const &port, std::string const &request, beast::IP::Endpoint const &remoteIPAddress, Output &&, std::shared_ptr< JobQueue::Coro > coro, std::string_view forwardedFor, std::string_view user)
std::map< std::reference_wrapper< Port const >, int > count_
void onRequest(Session &session)
beast::insight::Event rpc_time_
void onWSMessage(std::shared_ptr< WSSession > session, std::vector< boost::asio::const_buffer > const &buffers)
NetworkOPs & m_networkOPs
beast::Journal m_journal
std::unique_ptr< Server > m_server
beast::insight::Counter rpc_requests_
Json::Value processSession(std::shared_ptr< WSSession > const &session, std::shared_ptr< JobQueue::Coro > const &coro, Json::Value const &jv)
A multi-protocol server.
Definition ServerImpl.h:31
Persistent state information for a connection session.
Definition Session.h:24
virtual Port const & port()=0
Returns the Port settings for this connection.
virtual void close(bool graceful)=0
Close the session.
virtual std::shared_ptr< Session > detach()=0
Detach the session.
virtual http_request_type & request()=0
Returns the current HTTP request.
virtual std::shared_ptr< WSSession > websocketUpgrade()=0
Convert the connection to WebSocket.
void write(std::string const &s)
Send a copy of data asynchronously.
Definition Session.h:57
T count(T... args)
T empty(T... args)
T end(T... args)
T erase(T... args)
T find(T... args)
T insert(T... args)
T is_same_v
T make_shared(T... args)
void stream(Json::Value const &jv, Write const &write)
Stream compact JSON to the specified function.
@ arrayValue
array value (ordered list)
Definition json_value.h:26
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:27
int Int
unsigned int UInt
Endpoint from_asio(boost::asio::ip::address const &address)
Convert to Endpoint.
bool is_unspecified(Address const &addr)
Returns true if the address is unspecified.
Definition IPAddress.h:38
bool is_keep_alive(boost::beast::http::message< isRequest, Body, Fields > const &m)
Definition rfc2616.h:367
std::string const & getFullVersionString()
Full server version string.
Definition BuildInfo.cpp:62
static int constexpr maxRequestSize
static constexpr auto apiInvalidVersion
Definition ApiVersion.h:41
Role roleRequired(unsigned int version, bool betaEnabled, std::string const &method)
Status doCommand(RPC::JsonContext &context, Json::Value &result)
Execute an RPC command and store the results in a Json::Value.
unsigned int getAPIVersionNumber(Json::Value const &jv, bool betaEnabled)
Retrieve the api version number from the json value.
Definition ApiVersion.h:104
Json::Value make_error(error_code_i code)
Returns a new json object that reflects the error code.
Charge const feeReferenceRPC
Charge const feeMalformedRPC
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
static std::map< std::string, std::string > build_map(boost::beast::http::fields const &h)
void parse_Port(ParsedPort &port, Section const &section, std::ostream &log)
Definition Port.cpp:195
void HTTPReply(int nStatus, std::string const &strMsg, Json::Output const &, beast::Journal j)
static std::string buffers_to_string(ConstBufferSequence const &bs)
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
static void setup_Client(ServerHandler::Setup &setup)
boost::beast::http::request< boost::beast::http::dynamic_body > http_request_type
Definition Handoff.h:14
std::string base64_decode(std::string_view data)
static std::vector< Port > parse_Ports(Config const &config, std::ostream &log)
Json::Int constexpr wrong_version
std::shared_ptr< boost::asio::ssl::context > make_SSLContext(std::string const &cipherList)
Create a self-signed SSL context that allows anonymous Diffie Hellman.
static bool authorized(Port const &port, std::map< std::string, std::string > const &h)
Role requestRole(Role const &required, Port const &port, Json::Value const &params, beast::IP::Endpoint const &remoteIp, std::string_view user)
Return the allowed privilege role.
Definition Role.cpp:76
Resource::Consumer requestInboundEndpoint(Resource::Manager &manager, beast::IP::Endpoint const &remoteAddress, Role const &role, std::string_view user, std::string_view forwardedFor)
Definition Role.cpp:123
static Json::Value make_json_error(Json::Int code, Json::Value &&message)
std::string_view forwardedFor(http_request_type const &request)
Definition Role.cpp:243
ServerHandler::Setup setup_ServerHandler(Config const &config, std::ostream &&log)
static Port to_Port(ParsedPort const &parsed, std::ostream &log)
std::unique_ptr< ServerHandler > make_ServerHandler(Application &app, boost::asio::io_context &io_context, JobQueue &jobQueue, NetworkOPs &networkOPs, Resource::Manager &resourceManager, CollectorManager &cm)
@ jtCLIENT_RPC
Definition Job.h:30
@ jtCLIENT_WEBSOCKET
Definition Job.h:31
Json::Int constexpr forbidden
Json::Value rpcError(error_code_i iError)
Definition RPCErr.cpp:12
Json::Int constexpr method_not_found
std::unique_ptr< Server > make_Server(Handler &handler, boost::asio::io_context &io_context, beast::Journal journal)
Create the HTTP server using the specified handler.
Definition Server.h:16
Json::Int constexpr server_overloaded
void logDuration(Json::Value const &request, T const &duration, beast::Journal &journal)
Overlay::Setup setup_Overlay(BasicConfig const &config)
bool isUnlimited(Role const &role)
ADMIN and IDENTIFIED roles shall have unlimited resources.
Definition Role.cpp:106
std::shared_ptr< boost::asio::ssl::context > make_SSLContextAuthed(std::string const &keyFile, std::string const &certFile, std::string const &chainFile, std::string const &cipherList)
Create an authenticated SSL context using the specified files.
static Json::Output makeOutput(Session &session)
static bool isStatusRequest(http_request_type const &request)
error_code_i
Definition ErrorCodes.h:21
@ rpcSLOW_DOWN
Definition ErrorCodes.h:38
@ rpcINTERNAL
Definition ErrorCodes.h:111
@ rpcFORBIDDEN
Definition ErrorCodes.h:29
static Handoff statusRequestResponse(http_request_type const &request, boost::beast::http::status status)
T push_back(T... args)
T remove_suffix(T... args)
T reserve(T... args)
T size(T... args)
static IP::Endpoint from_asio(boost::asio::ip::address const &address)
Used to indicate the result of a server connection handoff.
Definition Handoff.h:21
std::shared_ptr< Writer > response
Definition Handoff.h:30
std::set< std::string, boost::beast::iless > protocol
Definition Port.h:83
boost::beast::websocket::permessage_deflate pmd_options
Definition Port.h:92
std::optional< boost::asio::ip::address > ip
Definition Port.h:96
std::vector< boost::asio::ip::network_v6 > secure_gateway_nets_v6
Definition Port.h:101
std::string admin_password
Definition Port.h:87
std::vector< boost::asio::ip::network_v4 > secure_gateway_nets_v4
Definition Port.h:100
std::string ssl_chain
Definition Port.h:90
std::string ssl_key
Definition Port.h:88
std::string name
Definition Port.h:82
std::uint16_t ws_queue_limit
Definition Port.h:94
std::vector< boost::asio::ip::network_v4 > admin_nets_v4
Definition Port.h:98
std::string user
Definition Port.h:84
std::optional< std::uint16_t > port
Definition Port.h:97
std::string ssl_ciphers
Definition Port.h:91
std::string password
Definition Port.h:85
std::vector< boost::asio::ip::network_v6 > admin_nets_v6
Definition Port.h:99
std::string admin_user
Definition Port.h:86
std::string ssl_cert
Definition Port.h:89
Configuration information for a Server listening port.
Definition Port.h:31
int limit
Definition Port.h:55
std::vector< boost::asio::ip::network_v4 > admin_nets_v4
Definition Port.h:38
std::string admin_password
Definition Port.h:45
std::vector< boost::asio::ip::network_v6 > secure_gateway_nets_v6
Definition Port.h:41
std::string ssl_key
Definition Port.h:46
std::string password
Definition Port.h:43
std::set< std::string, boost::beast::iless > protocol
Definition Port.h:37
std::string ssl_ciphers
Definition Port.h:49
std::string ssl_cert
Definition Port.h:47
std::string ssl_chain
Definition Port.h:48
boost::beast::websocket::permessage_deflate pmd_options
Definition Port.h:50
std::string admin_user
Definition Port.h:44
std::uint16_t ws_queue_limit
Definition Port.h:58
boost::asio::ip::address ip
Definition Port.h:35
std::vector< boost::asio::ip::network_v4 > secure_gateway_nets_v4
Definition Port.h:40
std::string name
Definition Port.h:34
std::vector< boost::asio::ip::network_v6 > admin_nets_v6
Definition Port.h:39
std::string user
Definition Port.h:42
std::uint16_t port
Definition Port.h:36
boost::asio::ip::tcp::endpoint overlay
std::vector< Port > ports
T substr(T... args)
T transform(T... args)
T what(T... args)