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 ripple {
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: " + reader.getFormatedErrorMessages(),
623 output,
624 rpcJ);
625 return;
626 }
627 }
628
629 bool batch = false;
630 unsigned size = 1;
631 if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == "batch")
632 {
633 batch = true;
634 if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
635 {
636 HTTPReply(400, "Malformed batch request", output, rpcJ);
637 return;
638 }
639 size = jsonOrig[jss::params].size();
640 }
641
643 auto const start(std::chrono::high_resolution_clock::now());
644 for (unsigned i = 0; i < size; ++i)
645 {
646 Json::Value const& jsonRPC =
647 batch ? jsonOrig[jss::params][i] : jsonOrig;
648
649 if (!jsonRPC.isObject())
650 {
652 r[jss::request] = jsonRPC;
653 r[jss::error] =
654 make_json_error(method_not_found, "Method not found");
655 reply.append(r);
656 continue;
657 }
658
659 unsigned apiVersion = RPC::apiVersionIfUnspecified;
660 if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
661 jsonRPC[jss::params].size() > 0 &&
662 jsonRPC[jss::params][0u].isObject())
663 {
664 apiVersion = RPC::getAPIVersionNumber(
665 jsonRPC[jss::params][Json::UInt(0)],
666 app_.config().BETA_RPC_API);
667 }
668
669 if (apiVersion == RPC::apiVersionIfUnspecified && batch)
670 {
671 // for batch request, api_version may be at a different level
672 apiVersion =
673 RPC::getAPIVersionNumber(jsonRPC, app_.config().BETA_RPC_API);
674 }
675
676 if (apiVersion == RPC::apiInvalidVersion)
677 {
678 if (!batch)
679 {
680 HTTPReply(400, jss::invalid_API_version.c_str(), output, rpcJ);
681 return;
682 }
684 r[jss::request] = jsonRPC;
685 r[jss::error] = make_json_error(
686 wrong_version, jss::invalid_API_version.c_str());
687 reply.append(r);
688 continue;
689 }
690
691 /* ------------------------------------------------------------------ */
692 auto role = Role::FORBID;
693 auto required = Role::FORBID;
694 if (jsonRPC.isMember(jss::method) && jsonRPC[jss::method].isString())
695 required = RPC::roleRequired(
696 apiVersion,
697 app_.config().BETA_RPC_API,
698 jsonRPC[jss::method].asString());
699
700 if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
701 jsonRPC[jss::params].size() > 0 &&
702 jsonRPC[jss::params][Json::UInt(0)].isObjectOrNull())
703 {
704 role = requestRole(
705 required,
706 port,
707 jsonRPC[jss::params][Json::UInt(0)],
708 remoteIPAddress,
709 user);
710 }
711 else
712 {
713 role = requestRole(
714 required, port, Json::objectValue, remoteIPAddress, user);
715 }
716
717 Resource::Consumer usage;
718 if (isUnlimited(role))
719 {
720 usage = m_resourceManager.newUnlimitedEndpoint(remoteIPAddress);
721 }
722 else
723 {
724 usage = m_resourceManager.newInboundEndpoint(
725 remoteIPAddress, role == Role::PROXY, forwardedFor);
726 if (usage.disconnect(m_journal))
727 {
728 if (!batch)
729 {
730 HTTPReply(503, "Server is overloaded", output, rpcJ);
731 return;
732 }
733 Json::Value r = jsonRPC;
734 r[jss::error] =
735 make_json_error(server_overloaded, "Server is overloaded");
736 reply.append(r);
737 continue;
738 }
739 }
740
741 if (role == Role::FORBID)
742 {
743 usage.charge(Resource::feeMalformedRPC);
744 if (!batch)
745 {
746 HTTPReply(403, "Forbidden", output, rpcJ);
747 return;
748 }
749 Json::Value r = jsonRPC;
750 r[jss::error] = make_json_error(forbidden, "Forbidden");
751 reply.append(r);
752 continue;
753 }
754
755 if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull())
756 {
757 usage.charge(Resource::feeMalformedRPC);
758 if (!batch)
759 {
760 HTTPReply(400, "Null method", output, rpcJ);
761 return;
762 }
763 Json::Value r = jsonRPC;
764 r[jss::error] = make_json_error(method_not_found, "Null method");
765 reply.append(r);
766 continue;
767 }
768
769 Json::Value const& method = jsonRPC[jss::method];
770 if (!method.isString())
771 {
772 usage.charge(Resource::feeMalformedRPC);
773 if (!batch)
774 {
775 HTTPReply(400, "method is not string", output, rpcJ);
776 return;
777 }
778 Json::Value r = jsonRPC;
779 r[jss::error] =
780 make_json_error(method_not_found, "method is not string");
781 reply.append(r);
782 continue;
783 }
784
785 std::string strMethod = method.asString();
786 if (strMethod.empty())
787 {
788 usage.charge(Resource::feeMalformedRPC);
789 if (!batch)
790 {
791 HTTPReply(400, "method is empty", output, rpcJ);
792 return;
793 }
794 Json::Value r = jsonRPC;
795 r[jss::error] =
796 make_json_error(method_not_found, "method is empty");
797 reply.append(r);
798 continue;
799 }
800
801 // Extract request parameters from the request Json as `params`.
802 //
803 // If the field "params" is empty, `params` is an empty object.
804 //
805 // Otherwise, that field must be an array of length 1 (why?)
806 // and we take that first entry and validate that it's an object.
807 Json::Value params;
808 if (!batch)
809 {
810 params = jsonRPC[jss::params];
811 if (!params)
813
814 else if (!params.isArray() || params.size() != 1)
815 {
816 usage.charge(Resource::feeMalformedRPC);
817 HTTPReply(400, "params unparseable", output, rpcJ);
818 return;
819 }
820 else
821 {
822 params = std::move(params[0u]);
823 if (!params.isObjectOrNull())
824 {
825 usage.charge(Resource::feeMalformedRPC);
826 HTTPReply(400, "params unparseable", output, rpcJ);
827 return;
828 }
829 }
830 }
831 else // batch
832 {
833 params = jsonRPC;
834 }
835
836 std::string ripplerpc = "1.0";
837 if (params.isMember(jss::ripplerpc))
838 {
839 if (!params[jss::ripplerpc].isString())
840 {
841 usage.charge(Resource::feeMalformedRPC);
842 if (!batch)
843 {
844 HTTPReply(400, "ripplerpc is not a string", output, rpcJ);
845 return;
846 }
847
848 Json::Value r = jsonRPC;
849 r[jss::error] = make_json_error(
850 method_not_found, "ripplerpc is not a string");
851 reply.append(r);
852 continue;
853 }
854 ripplerpc = params[jss::ripplerpc].asString();
855 }
856
861 if (role != Role::IDENTIFIED && role != Role::PROXY)
862 {
864 user.remove_suffix(user.size());
865 }
866
867 JLOG(m_journal.debug()) << "Query: " << strMethod << params;
868
869 // Provide the JSON-RPC method as the field "command" in the request.
870 params[jss::command] = strMethod;
871 JLOG(m_journal.trace())
872 << "doRpcCommand:" << strMethod << ":" << params;
873
874 Resource::Charge loadType = Resource::feeReferenceRPC;
875
876 RPC::JsonContext context{
877 {m_journal,
878 app_,
879 loadType,
880 m_networkOPs,
881 app_.getLedgerMaster(),
882 usage,
883 role,
884 coro,
886 apiVersion},
887 params,
888 {user, forwardedFor}};
889 Json::Value result;
890
891 auto start = std::chrono::system_clock::now();
892
893 try
894 {
895 RPC::doCommand(context, result);
896 }
897 catch (std::exception const& ex)
898 {
899 // LCOV_EXCL_START
900 result = RPC::make_error(rpcINTERNAL);
901 JLOG(m_journal.error()) << "Internal error : " << ex.what()
902 << " when processing request: "
903 << Json::Compact{Json::Value{params}};
904 // LCOV_EXCL_STOP
905 }
906
908
909 logDuration(params, end - start, m_journal);
910
911 usage.charge(loadType);
912 if (usage.warn())
913 result[jss::warning] = jss::load;
914
916 if (ripplerpc >= "2.0")
917 {
918 if (result.isMember(jss::error))
919 {
920 result[jss::status] = jss::error;
921 result["code"] = result[jss::error_code];
922 result["message"] = result[jss::error_message];
923 result.removeMember(jss::error_message);
924 JLOG(m_journal.debug()) << "rpcError: " << result[jss::error]
925 << ": " << result[jss::error_message];
926 r[jss::error] = std::move(result);
927 }
928 else
929 {
930 result[jss::status] = jss::success;
931 r[jss::result] = std::move(result);
932 }
933 }
934 else
935 {
936 // Always report "status". On an error report the request as
937 // received.
938 if (result.isMember(jss::error))
939 {
940 auto rq = params;
941
942 if (rq.isObject())
943 { // But mask potentially sensitive information.
944 if (rq.isMember(jss::passphrase.c_str()))
945 rq[jss::passphrase.c_str()] = "<masked>";
946 if (rq.isMember(jss::secret.c_str()))
947 rq[jss::secret.c_str()] = "<masked>";
948 if (rq.isMember(jss::seed.c_str()))
949 rq[jss::seed.c_str()] = "<masked>";
950 if (rq.isMember(jss::seed_hex.c_str()))
951 rq[jss::seed_hex.c_str()] = "<masked>";
952 }
953
954 result[jss::status] = jss::error;
955 result[jss::request] = rq;
956
957 JLOG(m_journal.debug()) << "rpcError: " << result[jss::error]
958 << ": " << result[jss::error_message];
959 }
960 else
961 {
962 result[jss::status] = jss::success;
963 }
964 r[jss::result] = std::move(result);
965 }
966
967 if (params.isMember(jss::jsonrpc))
968 r[jss::jsonrpc] = params[jss::jsonrpc];
969 if (params.isMember(jss::ripplerpc))
970 r[jss::ripplerpc] = params[jss::ripplerpc];
971 if (params.isMember(jss::id))
972 r[jss::id] = params[jss::id];
973 if (batch)
974 reply.append(std::move(r));
975 else
976 reply = std::move(r);
977
978 if (reply.isMember(jss::result) &&
979 reply[jss::result].isMember(jss::result))
980 {
981 reply = reply[jss::result];
982 if (reply.isMember(jss::status))
983 {
984 reply[jss::result][jss::status] = reply[jss::status];
985 reply.removeMember(jss::status);
986 }
987 }
988 }
989
990 // If we're returning an error_code, use that to determine the HTTP status.
991 int const httpStatus = [&reply]() {
992 // This feature is enabled with ripplerpc version 3.0 and above.
993 // Before ripplerpc version 3.0 always return 200.
994 if (reply.isMember(jss::ripplerpc) &&
995 reply[jss::ripplerpc].isString() &&
996 reply[jss::ripplerpc].asString() >= "3.0")
997 {
998 // If there's an error_code, use that to determine the HTTP Status.
999 if (reply.isMember(jss::error) &&
1000 reply[jss::error].isMember(jss::error_code) &&
1001 reply[jss::error][jss::error_code].isInt())
1002 {
1003 int const errCode = reply[jss::error][jss::error_code].asInt();
1004 return RPC::error_code_http_status(
1005 static_cast<error_code_i>(errCode));
1006 }
1007 }
1008 // Return OK.
1009 return 200;
1010 }();
1011
1012 auto response = to_string(reply);
1013
1014 rpc_time_.notify(std::chrono::duration_cast<std::chrono::milliseconds>(
1016 ++rpc_requests_;
1017 rpc_size_.notify(beast::insight::Event::value_type{response.size()});
1018
1019 response += '\n';
1020
1021 if (auto stream = m_journal.debug())
1022 {
1023 static int const maxSize = 10000;
1024 if (response.size() <= maxSize)
1025 stream << "Reply: " << response;
1026 else
1027 stream << "Reply: " << response.substr(0, maxSize);
1028 }
1029
1030 HTTPReply(httpStatus, response, output, rpcJ);
1031}
1032
1033//------------------------------------------------------------------------------
1034
1035/* This response is used with load balancing.
1036 If the server is overloaded, status 500 is reported. Otherwise status 200
1037 is reported, meaning the server can accept more connections.
1038*/
1039Handoff
1040ServerHandler::statusResponse(http_request_type const& request) const
1041{
1042 using namespace boost::beast::http;
1043 Handoff handoff;
1044 response<string_body> msg;
1045 std::string reason;
1046 if (app_.serverOkay(reason))
1047 {
1048 msg.result(boost::beast::http::status::ok);
1049 msg.body() = "<!DOCTYPE html><html><head><title>" + systemName() +
1050 " Test page for rippled</title></head><body><h1>" + systemName() +
1051 " Test</h1><p>This page shows rippled http(s) "
1052 "connectivity is working.</p></body></html>";
1053 }
1054 else
1055 {
1056 msg.result(boost::beast::http::status::internal_server_error);
1057 msg.body() = "<HTML><BODY>Server cannot accept clients: " + reason +
1058 "</BODY></HTML>";
1059 }
1060 msg.version(request.version());
1061 msg.insert("Server", BuildInfo::getFullVersionString());
1062 msg.insert("Content-Type", "text/html");
1063 msg.insert("Connection", "close");
1064 msg.prepare_payload();
1066 return handoff;
1067}
1068
1069//------------------------------------------------------------------------------
1070
1071void
1072ServerHandler::Setup::makeContexts()
1073{
1074 for (auto& p : ports)
1075 {
1076 if (p.secure())
1077 {
1078 if (p.ssl_key.empty() && p.ssl_cert.empty() && p.ssl_chain.empty())
1079 p.context = make_SSLContext(p.ssl_ciphers);
1080 else
1081 p.context = make_SSLContextAuthed(
1082 p.ssl_key, p.ssl_cert, p.ssl_chain, p.ssl_ciphers);
1083 }
1084 else
1085 {
1087 boost::asio::ssl::context::sslv23);
1088 }
1089 }
1090}
1091
1092static Port
1093to_Port(ParsedPort const& parsed, std::ostream& log)
1094{
1095 Port p;
1096 p.name = parsed.name;
1097
1098 if (!parsed.ip)
1099 {
1100 log << "Missing 'ip' in [" << p.name << "]";
1101 Throw<std::exception>();
1102 }
1103 p.ip = *parsed.ip;
1104
1105 if (!parsed.port)
1106 {
1107 log << "Missing 'port' in [" << p.name << "]";
1108 Throw<std::exception>();
1109 }
1110 p.port = *parsed.port;
1111
1112 if (parsed.protocol.empty())
1113 {
1114 log << "Missing 'protocol' in [" << p.name << "]";
1115 Throw<std::exception>();
1116 }
1117 p.protocol = parsed.protocol;
1118
1119 p.user = parsed.user;
1120 p.password = parsed.password;
1121 p.admin_user = parsed.admin_user;
1122 p.admin_password = parsed.admin_password;
1123 p.ssl_key = parsed.ssl_key;
1124 p.ssl_cert = parsed.ssl_cert;
1125 p.ssl_chain = parsed.ssl_chain;
1126 p.ssl_ciphers = parsed.ssl_ciphers;
1127 p.pmd_options = parsed.pmd_options;
1128 p.ws_queue_limit = parsed.ws_queue_limit;
1129 p.limit = parsed.limit;
1130 p.admin_nets_v4 = parsed.admin_nets_v4;
1131 p.admin_nets_v6 = parsed.admin_nets_v6;
1134
1135 return p;
1136}
1137
1138static std::vector<Port>
1139parse_Ports(Config const& config, std::ostream& log)
1140{
1141 std::vector<Port> result;
1142
1143 if (!config.exists("server"))
1144 {
1145 log << "Required section [server] is missing";
1146 Throw<std::exception>();
1147 }
1148
1149 ParsedPort common;
1150 parse_Port(common, config["server"], log);
1151
1152 auto const& names = config.section("server").values();
1153 result.reserve(names.size());
1154 for (auto const& name : names)
1155 {
1156 if (!config.exists(name))
1157 {
1158 log << "Missing section: [" << name << "]";
1159 Throw<std::exception>();
1160 }
1161
1162 // grpc ports are parsed by GRPCServer class. Do not validate
1163 // grpc port information in this file.
1164 if (name == SECTION_PORT_GRPC)
1165 continue;
1166
1167 ParsedPort parsed = common;
1168 parse_Port(parsed, config[name], log);
1169 result.push_back(to_Port(parsed, log));
1170 }
1171
1172 if (config.standalone())
1173 {
1174 auto it = result.begin();
1175
1176 while (it != result.end())
1177 {
1178 auto& p = it->protocol;
1179
1180 // Remove the peer protocol, and if that would
1181 // leave the port empty, remove the port as well
1182 if (p.erase("peer") && p.empty())
1183 it = result.erase(it);
1184 else
1185 ++it;
1186 }
1187 }
1188 else
1189 {
1190 auto const count =
1191 std::count_if(result.cbegin(), result.cend(), [](Port const& p) {
1192 return p.protocol.count("peer") != 0;
1193 });
1194
1195 if (count > 1)
1196 {
1197 log << "Error: More than one peer protocol configured in [server]";
1198 Throw<std::exception>();
1199 }
1200
1201 if (count == 0)
1202 log << "Warning: No peer protocol configured";
1203 }
1204
1205 return result;
1206}
1207
1208// Fill out the client portion of the Setup
1209static void
1211{
1212 decltype(setup.ports)::const_iterator iter;
1213 for (iter = setup.ports.cbegin(); iter != setup.ports.cend(); ++iter)
1214 if (iter->protocol.count("http") > 0 ||
1215 iter->protocol.count("https") > 0)
1216 break;
1217 if (iter == setup.ports.cend())
1218 return;
1219 setup.client.secure = iter->protocol.count("https") > 0;
1220 setup.client.ip = beast::IP::is_unspecified(iter->ip)
1221 ?
1222 // VFALCO HACK! to make localhost work
1223 (iter->ip.is_v6() ? "::1" : "127.0.0.1")
1224 : iter->ip.to_string();
1225 setup.client.port = iter->port;
1226 setup.client.user = iter->user;
1227 setup.client.password = iter->password;
1228 setup.client.admin_user = iter->admin_user;
1229 setup.client.admin_password = iter->admin_password;
1230}
1231
1232// Fill out the overlay portion of the Setup
1233static void
1235{
1236 auto const iter = std::find_if(
1237 setup.ports.cbegin(), setup.ports.cend(), [](Port const& port) {
1238 return port.protocol.count("peer") != 0;
1239 });
1240 if (iter == setup.ports.cend())
1241 {
1242 setup.overlay = {};
1243 return;
1244 }
1245 setup.overlay = {iter->ip, iter->port};
1246}
1247
1248ServerHandler::Setup
1250{
1252 setup.ports = parse_Ports(config, log);
1253
1254 setup_Client(setup);
1255 setup_Overlay(setup);
1256
1257 return setup;
1258}
1259
1262 Application& app,
1263 boost::asio::io_context& io_context,
1264 JobQueue& jobQueue,
1265 NetworkOPs& networkOPs,
1266 Resource::Manager& resourceManager,
1267 CollectorManager& cm)
1268{
1271 app,
1272 io_context,
1273 jobQueue,
1274 networkOPs,
1275 resourceManager,
1276 cm);
1277}
1278
1279} // namespace ripple
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:20
std::string getFormatedErrorMessages() 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 Overlay & overlay()=0
virtual beast::Journal journal(std::string const &name)=0
virtual NetworkOPs & getOPs()=0
virtual LedgerMaster & getLedgerMaster()=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 standalone() const
Definition Config.h:317
bool BETA_RPC_API
Definition Config.h:268
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_
Json::Value processSession(std::shared_ptr< WSSession > const &session, std::shared_ptr< JobQueue::Coro > const &coro, Json::Value const &jv)
void onWSMessage(std::shared_ptr< WSSession > session, std::vector< boost::asio::const_buffer > const &buffers)
std::unique_ptr< Server > m_server
ServerHandler(ServerHandlerCreator const &, Application &app, boost::asio::io_context &io_context, JobQueue &jobQueue, NetworkOPs &networkOPs, Resource::Manager &resourceManager, CollectorManager &cm)
beast::insight::Event rpc_size_
Setup const & setup() const
beast::insight::Counter rpc_requests_
beast::Journal m_journal
void onClose(Session &session, boost::system::error_code const &)
Handoff statusResponse(http_request_type const &request) const
NetworkOPs & m_networkOPs
beast::insight::Event rpc_time_
bool onAccept(Session &session, boost::asio::ip::tcp::endpoint endpoint)
std::map< std::reference_wrapper< Port const >, int > count_
void onRequest(Session &session)
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)
Handoff onHandoff(Session &session, std::unique_ptr< stream_type > &&bundle, http_request_type &&request, boost::asio::ip::tcp::endpoint const &remote_address)
A multi-protocol server.
Definition ServerImpl.h:31
Persistent state information for a connection session.
Definition Session.h:24
virtual std::shared_ptr< WSSession > websocketUpgrade()=0
Convert the connection to WebSocket.
virtual Port const & port()=0
Returns the Port settings for this connection.
virtual std::shared_ptr< Session > detach()=0
Detach the session.
virtual void close(bool graceful)=0
Close the session.
virtual http_request_type & request()=0
Returns the current HTTP request.
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
Json::Value make_error(error_code_i code)
Returns a new json object that reflects the error code.
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.
static constexpr auto apiInvalidVersion
Definition ApiVersion.h:41
unsigned int getAPIVersionNumber(Json::Value const &jv, bool betaEnabled)
Retrieve the api version number from the json value.
Definition ApiVersion.h:104
Charge const feeReferenceRPC
Charge const feeMalformedRPC
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
void HTTPReply(int nStatus, std::string const &strMsg, Json::Output const &, beast::Journal j)
static std::vector< Port > parse_Ports(Config const &config, std::ostream &log)
static Port to_Port(ParsedPort const &parsed, std::ostream &log)
static Json::Output makeOutput(Session &session)
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
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
Json::Int constexpr wrong_version
@ rpcSLOW_DOWN
Definition ErrorCodes.h:38
@ rpcINTERNAL
Definition ErrorCodes.h:111
@ rpcFORBIDDEN
Definition ErrorCodes.h:29
static Json::Value make_json_error(Json::Int code, Json::Value &&message)
std::string base64_decode(std::string_view data)
void parse_Port(ParsedPort &port, Section const &section, std::ostream &log)
Definition Port.cpp:195
Json::Value rpcError(int iError)
Definition RPCErr.cpp:12
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_SSLContext(std::string const &cipherList)
Create a self-signed SSL context that allows anonymous Diffie Hellman.
Json::Int constexpr method_not_found
Json::Int constexpr forbidden
ServerHandler::Setup setup_ServerHandler(Config const &config, std::ostream &&log)
void logDuration(Json::Value const &request, T const &duration, beast::Journal &journal)
std::string_view forwardedFor(http_request_type const &request)
Definition Role.cpp:243
boost::beast::http::request< boost::beast::http::dynamic_body > http_request_type
Definition Handoff.h:14
std::unique_ptr< ServerHandler > make_ServerHandler(Application &app, boost::asio::io_context &io_context, JobQueue &jobQueue, NetworkOPs &networkOPs, Resource::Manager &resourceManager, CollectorManager &cm)
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
static Handoff statusRequestResponse(http_request_type const &request, boost::beast::http::status status)
static std::string buffers_to_string(ConstBufferSequence const &bs)
static void setup_Client(ServerHandler::Setup &setup)
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.
Overlay::Setup setup_Overlay(BasicConfig const &config)
@ jtCLIENT_RPC
Definition Job.h:30
@ jtCLIENT_WEBSOCKET
Definition Job.h:31
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
static std::map< std::string, std::string > build_map(boost::beast::http::fields const &h)
static bool isStatusRequest(http_request_type const &request)
static bool authorized(Port const &port, std::map< std::string, std::string > const &h)
Json::Int constexpr server_overloaded
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::string ssl_ciphers
Definition Port.h:91
boost::beast::websocket::permessage_deflate pmd_options
Definition Port.h:92
std::optional< std::uint16_t > port
Definition Port.h:97
std::vector< boost::asio::ip::network_v4 > admin_nets_v4
Definition Port.h:98
std::string user
Definition Port.h:84
std::string ssl_key
Definition Port.h:88
std::uint16_t ws_queue_limit
Definition Port.h:94
std::vector< boost::asio::ip::network_v6 > secure_gateway_nets_v6
Definition Port.h:101
std::set< std::string, boost::beast::iless > protocol
Definition Port.h:83
std::string admin_password
Definition Port.h:87
std::string name
Definition Port.h:82
std::string ssl_chain
Definition Port.h:90
std::string password
Definition Port.h:85
std::vector< boost::asio::ip::network_v6 > admin_nets_v6
Definition Port.h:99
std::string ssl_cert
Definition Port.h:89
std::string admin_user
Definition Port.h:86
std::optional< boost::asio::ip::address > ip
Definition Port.h:96
std::vector< boost::asio::ip::network_v4 > secure_gateway_nets_v4
Definition Port.h:100
Configuration information for a Server listening port.
Definition Port.h:31
std::uint16_t port
Definition Port.h:36
std::string ssl_chain
Definition Port.h:48
std::string password
Definition Port.h:43
std::vector< boost::asio::ip::network_v6 > admin_nets_v6
Definition Port.h:39
std::set< std::string, boost::beast::iless > protocol
Definition Port.h:37
std::string ssl_cert
Definition Port.h:47
int limit
Definition Port.h:55
std::string ssl_key
Definition Port.h:46
std::vector< boost::asio::ip::network_v6 > secure_gateway_nets_v6
Definition Port.h:41
std::string admin_user
Definition Port.h:44
std::string user
Definition Port.h:42
std::vector< boost::asio::ip::network_v4 > secure_gateway_nets_v4
Definition Port.h:40
boost::asio::ip::address ip
Definition Port.h:35
std::uint16_t ws_queue_limit
Definition Port.h:58
std::string ssl_ciphers
Definition Port.h:49
std::string admin_password
Definition Port.h:45
std::string name
Definition Port.h:34
std::vector< boost::asio::ip::network_v4 > admin_nets_v4
Definition Port.h:38
boost::beast::websocket::permessage_deflate pmd_options
Definition Port.h:50
boost::asio::ip::tcp::endpoint overlay
std::vector< Port > ports
T substr(T... args)
T transform(T... args)
T what(T... args)