xrpld
Loading...
Searching...
No Matches
WSClient.cpp
1#include <test/jtx/WSClient.h>
2
3#include <xrpld/core/Config.h>
4
5#include <xrpl/basics/Mutex.hpp>
6#include <xrpl/basics/contract.h>
7#include <xrpl/config/BasicConfig.h>
8#include <xrpl/config/Constants.h>
9#include <xrpl/json/json_reader.h>
10#include <xrpl/json/json_value.h>
11#include <xrpl/json/to_string.h>
12#include <xrpl/protocol/jss.h>
13#include <xrpl/server/Port.h>
14
15#include <boost/asio/bind_executor.hpp>
16#include <boost/asio/buffer.hpp>
17#include <boost/asio/executor_work_guard.hpp>
18#include <boost/asio/io_context.hpp>
19#include <boost/asio/ip/address_v4.hpp>
20#include <boost/asio/ip/address_v6.hpp>
21#include <boost/asio/ip/tcp.hpp>
22#include <boost/asio/post.hpp>
23#include <boost/asio/strand.hpp>
24#include <boost/beast/core/multi_buffer.hpp>
25#include <boost/beast/websocket/error.hpp>
26#include <boost/beast/websocket/rfc6455.hpp>
27#include <boost/beast/websocket/stream.hpp>
28#include <boost/beast/websocket/stream_base.hpp>
29#include <boost/system/detail/error_code.hpp>
30#include <boost/system/system_error.hpp>
31
32#include <chrono>
33#include <condition_variable>
34#include <cstddef>
35#include <exception>
36#include <functional>
37#include <iostream>
38#include <list>
39#include <memory>
40#include <mutex>
41#include <optional>
42#include <stdexcept>
43#include <string>
44#include <thread>
45#include <unordered_map>
46#include <utility>
47
48namespace xrpl::test {
49
50class WSClientImpl : public WSClient
51{
52 using error_code = boost::system::error_code;
53
54 struct Msg
55 {
57
58 explicit Msg(json::Value&& jv) : jv(std::move(jv))
59 {
60 }
61 };
62
63 static boost::asio::ip::tcp::endpoint
64 getEndpoint(BasicConfig const& cfg, bool v2)
65 {
66 auto& log = std::cerr;
67 ParsedPort common;
68 parsePort(common, cfg[Sections::kServer], log);
69 auto const ps = v2 ? "ws2" : "ws";
70 for (auto const& name : cfg.section(Sections::kServer).values())
71 {
72 if (!cfg.exists(name))
73 continue;
74 ParsedPort pp;
75 parsePort(pp, cfg[name], log);
76 if (!pp.protocol.contains(ps))
77 continue;
78 using namespace boost::asio::ip;
79 if (pp.ip && pp.ip->is_unspecified())
80 {
81 *pp.ip = pp.ip->is_v6() ? address{address_v6::loopback()}
82 : address{address_v4::loopback()};
83 }
84
85 if (!pp.port)
86 Throw<std::runtime_error>("Use fixConfigPorts with auto ports");
87
88 return {*pp.ip, *pp.port}; // NOLINT(bugprone-unchecked-optional-access)
89 }
90 Throw<std::runtime_error>("Missing WebSocket port");
91 return {}; // Silence compiler control paths return value warning
92 }
93
94 template <class ConstBuffers>
95 static std::string
96 bufferString(ConstBuffers const& b)
97 {
98 using boost::asio::buffer;
99 using boost::asio::buffer_size;
100 std::string s;
101 s.resize(buffer_size(b));
102 buffer_copy(buffer(&s[0], s.size()), b);
103 return s;
104 }
105
106 boost::asio::io_context ios_;
108 boost::asio::strand<boost::asio::io_context::executor_type> strand_;
110 boost::asio::ip::tcp::socket stream_;
111 boost::beast::websocket::stream<boost::asio::ip::tcp::socket&> ws_;
112 boost::beast::multi_buffer rb_;
113
114 bool peerClosed_ = false;
115
116 // disconnect() waits on this until the read loop ends (for any reason:
117 // the server acknowledged our close, or a timeout force-closed the socket).
118 static constexpr auto kDisconnectTimeout = std::chrono::seconds{1};
119 xrpl::Mutex<bool> readEnded_;
121
122 // synchronize message queue
126
127 unsigned rpcVersion_;
128
129 void
131 {
132 boost::asio::post(
133 ios_, //
134 boost::asio::bind_executor(strand_, [this] {
135 if (!peerClosed_)
136 {
137 ws_.async_close(
138 {}, //
139 boost::asio::bind_executor(strand_, [&](error_code) {
140 try
141 {
142 stream_.cancel();
143 }
144 // NOLINTNEXTLINE(bugprone-empty-catch)
145 catch (boost::system::system_error const&)
146 {
147 // ignored
148 }
149 }));
150 }
151 }));
152 work_ = std::nullopt;
153 thread_.join();
154 }
155
156public:
158 Config const& cfg,
159 bool v2,
160 unsigned rpcVersion,
162 : work_(std::in_place, boost::asio::make_work_guard(ios_))
163 , strand_(boost::asio::make_strand(ios_))
164 , thread_([&] { ios_.run(); })
165 , stream_(ios_)
166 , ws_(stream_)
167 , rpcVersion_(rpcVersion)
168 {
169 try
170 {
171 auto const ep = getEndpoint(cfg, v2);
172 stream_.connect(ep);
173 ws_.set_option(
174 boost::beast::websocket::stream_base::decorator(
175 [&](boost::beast::websocket::request_type& req) {
176 for (auto const& h : headers)
177 req.set(h.first, h.second);
178 }));
179 ws_.handshake(ep.address().to_string() + ":" + std::to_string(ep.port()), "/");
180 ws_.async_read(
181 rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) {
182 onReadMsg(ec);
183 }));
184 }
185 catch (std::exception&)
186 {
187 cleanup();
188 rethrow();
189 }
190 }
191
192 ~WSClientImpl() override
193 {
194 cleanup();
195 }
196
198 invoke(std::string const& cmd, json::Value const& params) override
199 {
200 using boost::asio::buffer;
201 using namespace std::chrono_literals;
202
203 {
204 json::Value jp;
205 if (params)
206 jp = params;
207 if (rpcVersion_ == 2)
208 {
209 jp[jss::method] = cmd;
210 jp[jss::jsonrpc] = "2.0";
211 jp[jss::ripplerpc] = "2.0";
212 jp[jss::id] = 5;
213 }
214 else
215 {
216 jp[jss::command] = cmd;
217 }
218 auto const s = to_string(jp);
219
220 // Use the error_code overload to avoid an unhandled exception
221 // when the server closes the WebSocket connection (e.g. after
222 // booting a client that exceeded resource thresholds).
223 error_code ec;
224 ws_.write_some(true, buffer(s), ec);
225 if (ec)
226 return {};
227 }
228
229 auto jv =
230 findMsg(5s, [&](json::Value const& jval) { return jval[jss::type] == jss::response; });
231 if (jv)
232 {
233 // Normalize JSON output
234 jv->removeMember(jss::type);
235 if ((*jv).isMember(jss::status) && (*jv)[jss::status] == jss::error)
236 {
237 json::Value ret;
238 ret[jss::result] = *jv;
239 if ((*jv).isMember(jss::error))
240 ret[jss::error] = (*jv)[jss::error];
241 ret[jss::status] = jss::error;
242 return ret;
243 }
244 if ((*jv).isMember(jss::status) && (*jv).isMember(jss::result))
245 (*jv)[jss::result][jss::status] = (*jv)[jss::status];
246 return *jv;
247 }
248 return {};
249 }
250
252 getMsg(std::chrono::milliseconds const& timeout) override
253 {
255 {
257 if (!cv_.wait_for(lock, timeout, [&] { return !msgs_.empty(); }))
258 return std::nullopt;
259 m = std::move(msgs_.back());
260 msgs_.pop_back();
261 }
262 return std::move(m->jv);
263 }
264
266 findMsg(std::chrono::milliseconds const& timeout, std::function<bool(json::Value const&)> pred)
267 override
268 {
270 {
272 if (!cv_.wait_for(lock, timeout, [&] {
273 for (auto it = msgs_.begin(); it != msgs_.end(); ++it)
274 {
275 if (pred((*it)->jv))
276 {
277 m = std::move(*it);
278 msgs_.erase(it);
279 return true;
280 }
281 }
282 return false;
283 }))
284 {
285 return std::nullopt;
286 }
287 }
288 return std::move(m->jv);
289 }
290
291 [[nodiscard]] unsigned
292 version() const override
293 {
294 return rpcVersion_;
295 }
296
297 void
298 disconnect() override
299 {
300 // Perform a graceful WebSocket closing handshake and block until the
301 // read loop ends, so the server observes a clean close (not a RST) and
302 // has finished tearing the connection down by the time we return.
303 // If the server already closed, the wait below returns immediately.
304 boost::asio::post(
305 ios_,
306 boost::asio::bind_executor(
307 strand_, //
308 [this] {
309 if (!peerClosed_)
310 {
311 ws_.async_close(
312 boost::beast::websocket::close_code::normal,
313 boost::asio::bind_executor(strand_, [](error_code) {}));
314 }
315 }));
316
317 auto lock = readEnded_.lock<std::unique_lock>();
318 readEndCv_.wait_for(lock, kDisconnectTimeout, [&lock] { return *lock; });
319
320 // On timeout (server gone or not replying) force the socket closed so
321 // the outstanding read ends and the worker thread can later be joined.
322 if (!*lock)
323 {
324 boost::asio::post(
325 ios_,
326 boost::asio::bind_executor(
327 strand_, //
328 [this] {
329 boost::system::error_code ec;
330 stream_.close(ec);
331 }));
332 }
333 }
334
335private:
336 void
338 {
339 if (ec)
340 {
341 if (ec == boost::beast::websocket::error::closed)
342 peerClosed_ = true;
343
344 *readEnded_.lock() = true;
345 readEndCv_.notify_all();
346
347 return;
348 }
349
350 json::Value jv;
351 json::Reader jr;
352
353 jr.parse(bufferString(rb_.data()), jv);
354 rb_.consume(rb_.size());
355
356 auto m = std::make_shared<Msg>(std::move(jv));
357 {
358 std::scoped_lock const lock(m_);
359 msgs_.push_front(m);
360 cv_.notify_all();
361 }
362
363 ws_.async_read(
364 rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) {
365 onReadMsg(ec);
366 }));
367 }
368};
369
372 Config const& cfg,
373 bool v2,
374 unsigned rpcVersion,
376{
377 return std::make_unique<WSClientImpl>(cfg, v2, rpcVersion, headers);
378}
379
380} // namespace xrpl::test
Unserialize a JSON document into a Value.
Definition json_reader.h:20
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:117
Holds unparsed configuration information.
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.
std::vector< std::string > const & values() const
Returns all the values in the section.
Definition BasicConfig.h:69
std::list< std::shared_ptr< Msg > > msgs_
Definition WSClient.cpp:125
xrpl::Mutex< bool > readEnded_
Definition WSClient.cpp:119
void disconnect() override
Close the client's connection to the server.
Definition WSClient.cpp:298
void onReadMsg(error_code const &ec)
Definition WSClient.cpp:337
boost::beast::multi_buffer rb_
Definition WSClient.cpp:112
boost::asio::io_context ios_
Definition WSClient.cpp:106
unsigned version() const override
Get RPC 1.0 or RPC 2.0.
Definition WSClient.cpp:292
std::optional< boost::asio::executor_work_guard< boost::asio::io_context::executor_type > > work_
Definition WSClient.cpp:107
boost::asio::ip::tcp::socket stream_
Definition WSClient.cpp:110
std::optional< json::Value > getMsg(std::chrono::milliseconds const &timeout) override
Retrieve a message.
Definition WSClient.cpp:252
boost::beast::websocket::stream< boost::asio::ip::tcp::socket & > ws_
Definition WSClient.cpp:111
json::Value invoke(std::string const &cmd, json::Value const &params) override
Submit a command synchronously.
Definition WSClient.cpp:198
std::optional< json::Value > findMsg(std::chrono::milliseconds const &timeout, std::function< bool(json::Value const &)> pred) override
Retrieve a message that meets the predicate criteria.
Definition WSClient.cpp:266
WSClientImpl(Config const &cfg, bool v2, unsigned rpcVersion, std::unordered_map< std::string, std::string > const &headers={})
Definition WSClient.cpp:157
std::condition_variable cv_
Definition WSClient.cpp:124
static constexpr auto kDisconnectTimeout
Definition WSClient.cpp:118
boost::system::error_code error_code
Definition WSClient.cpp:52
static std::string bufferString(ConstBuffers const &b)
Definition WSClient.cpp:96
static boost::asio::ip::tcp::endpoint getEndpoint(BasicConfig const &cfg, bool v2)
Definition WSClient.cpp:64
boost::asio::strand< boost::asio::io_context::executor_type > strand_
Definition WSClient.cpp:108
std::condition_variable readEndCv_
Definition WSClient.cpp:120
T contains(T... args)
T make_shared(T... args)
T make_unique(T... args)
STL namespace.
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpcVersion, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:371
void parsePort(ParsedPort &port, Section const &section, std::ostream &log)
Definition Port.cpp:195
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
XRPL_NO_SANITIZE_ADDRESS void rethrow()
Rethrow the exception currently being handled.
Definition contract.h:36
XRPL_NO_SANITIZE_ADDRESS void Throw(Args &&... args)
Definition contract.h:52
T resize(T... args)
T size(T... args)
std::set< std::string, boost::beast::iless > protocol
Definition Port.h:81
std::optional< boost::asio::ip::address > ip
Definition Port.h:94
std::optional< std::uint16_t > port
Definition Port.h:95
static constexpr auto kServer
Definition Constants.h:56
T to_string(T... args)