rippled
Loading...
Searching...
No Matches
Server_test.cpp
1#include <test/jtx.h>
2#include <test/jtx/CaptureLogs.h>
3#include <test/jtx/envconfig.h>
4#include <test/unit_test/SuiteJournal.h>
5
6#include <xrpld/core/ConfigSections.h>
7
8#include <xrpl/basics/make_SSLContext.h>
9#include <xrpl/beast/rfc2616.h>
10#include <xrpl/beast/unit_test.h>
11#include <xrpl/server/Server.h>
12#include <xrpl/server/Session.h>
13
14#include <boost/asio.hpp>
15#include <boost/asio/executor_work_guard.hpp>
16#include <boost/beast/core/tcp_stream.hpp>
17#include <boost/beast/ssl/ssl_stream.hpp>
18#include <boost/utility/in_place_factory.hpp>
19
20#include <chrono>
21#include <optional>
22#include <stdexcept>
23#include <thread>
24
25namespace ripple {
26namespace test {
27
28using socket_type = boost::beast::tcp_stream;
29using stream_type = boost::beast::ssl_stream<socket_type>;
30
32{
33public:
35 {
36 private:
37 boost::asio::io_context io_context_;
38 std::optional<boost::asio::executor_work_guard<
39 boost::asio::io_context::executor_type>>
42
43 public:
45 : work_(std::in_place, boost::asio::make_work_guard(io_context_))
46 , thread_([&]() { this->io_context_.run(); })
47 {
48 }
49
51 {
52 work_.reset();
53 thread_.join();
54 }
55
56 boost::asio::io_context&
58 {
59 return io_context_;
60 }
61 };
62
63 //--------------------------------------------------------------------------
64
66 {
68
69 public:
71 : Sink(beast::severities::kWarning, false), suite_(suite)
72 {
73 }
74
75 void
77 override
78 {
79 if (level < threshold())
80 return;
81
82 suite_.log << text << std::endl;
83 }
84
85 void
87 override
88 {
89 suite_.log << text << std::endl;
90 }
91 };
92
93 //--------------------------------------------------------------------------
94
96 {
97 bool
98 onAccept(Session& session, boost::asio::ip::tcp::endpoint endpoint)
99 {
100 return true;
101 }
102
103 Handoff
105 Session& session,
107 http_request_type&& request,
108 boost::asio::ip::tcp::endpoint remote_address)
109 {
110 return Handoff{};
111 }
112
113 Handoff
115 Session& session,
116 http_request_type&& request,
117 boost::asio::ip::tcp::endpoint remote_address)
118 {
119 return Handoff{};
120 }
121
122 void
124 {
125 session.write(std::string("Hello, world!\n"));
127 session.complete();
128 else
129 session.close(true);
130 }
131
132 void
138
139 void
140 onClose(Session& session, boost::system::error_code const&)
141 {
142 }
143
144 void
146 {
147 }
148 };
149
150 //--------------------------------------------------------------------------
151
152 // Connect to an address
153 template <class Socket>
154 bool
155 connect(Socket& s, typename Socket::endpoint_type const& ep)
156 {
157 try
158 {
159 s.connect(ep);
160 pass();
161 return true;
162 }
163 catch (std::exception const& e)
164 {
165 fail(e.what());
166 }
167
168 return false;
169 }
170
171 // Write a string to the stream
172 template <class SyncWriteStream>
173 bool
174 write(SyncWriteStream& s, std::string const& text)
175 {
176 try
177 {
178 boost::asio::write(s, boost::asio::buffer(text));
179 pass();
180 return true;
181 }
182 catch (std::exception const& e)
183 {
184 fail(e.what());
185 }
186 return false;
187 }
188
189 // Expect that reading the stream produces a matching string
190 template <class SyncReadStream>
191 bool
192 expect_read(SyncReadStream& s, std::string const& match)
193 {
194 boost::asio::streambuf b(1000); // limit on read
195 try
196 {
197 auto const n = boost::asio::read_until(s, b, '\n');
198 if (BEAST_EXPECT(n == match.size()))
199 {
200 std::string got;
201 got.resize(n);
202 boost::asio::buffer_copy(
203 boost::asio::buffer(&got[0], n), b.data());
204 return BEAST_EXPECT(got == match);
205 }
206 }
207 catch (std::length_error const& e)
208 {
209 fail(e.what());
210 }
211 catch (std::exception const& e)
212 {
213 fail(e.what());
214 }
215 return false;
216 }
217
218 void
219 test_request(boost::asio::ip::tcp::endpoint const& ep)
220 {
221 boost::asio::io_context ios;
222 using socket = boost::asio::ip::tcp::socket;
223 socket s(ios);
224
225 if (!connect(s, ep))
226 return;
227
228 if (!write(
229 s,
230 "GET / HTTP/1.1\r\n"
231 "Connection: close\r\n"
232 "\r\n"))
233 return;
234
235 if (!expect_read(s, "Hello, world!\n"))
236 return;
237
238 boost::system::error_code ec;
239 s.shutdown(socket::shutdown_both, ec);
240
242 }
243
244 void
245 test_keepalive(boost::asio::ip::tcp::endpoint const& ep)
246 {
247 boost::asio::io_context ios;
248 using socket = boost::asio::ip::tcp::socket;
249 socket s(ios);
250
251 if (!connect(s, ep))
252 return;
253
254 if (!write(
255 s,
256 "GET / HTTP/1.1\r\n"
257 "Connection: Keep-Alive\r\n"
258 "\r\n"))
259 return;
260
261 if (!expect_read(s, "Hello, world!\n"))
262 return;
263
264 if (!write(
265 s,
266 "GET / HTTP/1.1\r\n"
267 "Connection: close\r\n"
268 "\r\n"))
269 return;
270
271 if (!expect_read(s, "Hello, world!\n"))
272 return;
273
274 boost::system::error_code ec;
275 s.shutdown(socket::shutdown_both, ec);
276 }
277
278 void
280 {
281 testcase("Basic client/server");
282 TestSink sink{*this};
284 sink.threshold(beast::severities::Severity::kAll);
285 beast::Journal journal{sink};
286 TestHandler handler;
287 auto s = make_Server(handler, thread.get_io_context(), journal);
288 std::vector<Port> serverPort(1);
289 serverPort.back().ip =
290 boost::asio::ip::make_address(getEnvLocalhostAddr()),
291 serverPort.back().port = 0;
292 serverPort.back().protocol.insert("http");
293 auto eps = s->ports(serverPort);
294 test_request(eps.begin()->second);
295 test_keepalive(eps.begin()->second);
296 // s->close();
297 s = nullptr;
298 pass();
299 }
300
301 void
303 {
304 testcase("stress test");
305 struct NullHandler
306 {
307 bool
308 onAccept(Session& session, boost::asio::ip::tcp::endpoint endpoint)
309 {
310 return true;
311 }
312
313 Handoff
314 onHandoff(
315 Session& session,
317 http_request_type&& request,
318 boost::asio::ip::tcp::endpoint remote_address)
319 {
320 return Handoff{};
321 }
322
323 Handoff
324 onHandoff(
325 Session& session,
326 http_request_type&& request,
327 boost::asio::ip::tcp::endpoint remote_address)
328 {
329 return Handoff{};
330 }
331
332 void
333 onRequest(Session& session)
334 {
335 }
336
337 void
338 onWSMessage(
341 {
342 }
343
344 void
345 onClose(Session& session, boost::system::error_code const&)
346 {
347 }
348
349 void
350 onStopped(Server& server)
351 {
352 }
353 };
354
355 using namespace beast::severities;
356 SuiteJournal journal("Server_test", *this);
357
358 NullHandler h;
359 for (int i = 0; i < 1000; ++i)
360 {
362 auto s = make_Server(h, thread.get_io_context(), journal);
363 std::vector<Port> serverPort(1);
364 serverPort.back().ip =
365 boost::asio::ip::make_address(getEnvLocalhostAddr()),
366 serverPort.back().port = 0;
367 serverPort.back().protocol.insert("http");
368 s->ports(serverPort);
369 }
370 pass();
371 }
372
373 void
375 {
376 testcase("Server config - invalid options");
377 using namespace test::jtx;
378
379 std::string messages;
380
381 except([&] {
382 Env env{
383 *this,
385 (*cfg).deprecatedClearSection("port_rpc");
386 return cfg;
387 }),
389 });
390 BEAST_EXPECT(
391 messages.find("Missing 'ip' in [port_rpc]") != std::string::npos);
392
393 except([&] {
394 Env env{
395 *this,
397 (*cfg).deprecatedClearSection("port_rpc");
398 (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
399 return cfg;
400 }),
402 });
403 BEAST_EXPECT(
404 messages.find("Missing 'port' in [port_rpc]") != std::string::npos);
405
406 except([&] {
407 Env env{
408 *this,
410 (*cfg).deprecatedClearSection("port_rpc");
411 (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
412 (*cfg)["port_rpc"].set("port", "0");
413 return cfg;
414 }),
416 });
417 BEAST_EXPECT(
418 messages.find("Invalid value '0' for key 'port' in [port_rpc]") ==
419 std::string::npos);
420
421 except([&] {
422 Env env{
423 *this,
425 (*cfg)["server"].set("port", "0");
426 return cfg;
427 }),
429 });
430 BEAST_EXPECT(
431 messages.find("Invalid value '0' for key 'port' in [server]") !=
432 std::string::npos);
433
434 except([&] {
435 Env env{
436 *this,
438 (*cfg).deprecatedClearSection("port_rpc");
439 (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
440 (*cfg)["port_rpc"].set("port", "8081");
441 (*cfg)["port_rpc"].set("protocol", "");
442 return cfg;
443 }),
445 });
446 BEAST_EXPECT(
447 messages.find("Missing 'protocol' in [port_rpc]") !=
448 std::string::npos);
449
450 except(
451 [&] // this creates a standard test config without the server
452 // section
453 {
454 Env env{
455 *this,
458 cfg->overwrite(
459 ConfigSection::nodeDatabase(), "type", "memory");
460 cfg->overwrite(
461 ConfigSection::nodeDatabase(), "path", "main");
462 cfg->deprecatedClearSection(
464 cfg->legacy("database_path", "");
465 cfg->setupControl(true, true, true);
466 (*cfg)["port_peer"].set("ip", getEnvLocalhostAddr());
467 (*cfg)["port_peer"].set("port", "8080");
468 (*cfg)["port_peer"].set("protocol", "peer");
469 (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
470 (*cfg)["port_rpc"].set("port", "8081");
471 (*cfg)["port_rpc"].set("protocol", "http,ws2");
472 (*cfg)["port_rpc"].set("admin", getEnvLocalhostAddr());
473 (*cfg)["port_ws"].set("ip", getEnvLocalhostAddr());
474 (*cfg)["port_ws"].set("port", "8082");
475 (*cfg)["port_ws"].set("protocol", "ws");
476 (*cfg)["port_ws"].set("admin", getEnvLocalhostAddr());
477 return cfg;
478 }),
480 });
481 BEAST_EXPECT(
482 messages.find("Required section [server] is missing") !=
483 std::string::npos);
484
485 except([&] // this creates a standard test config without some of the
486 // port sections
487 {
488 Env env{
489 *this,
492 cfg->overwrite(
493 ConfigSection::nodeDatabase(), "type", "memory");
494 cfg->overwrite(
495 ConfigSection::nodeDatabase(), "path", "main");
496 cfg->deprecatedClearSection(
498 cfg->legacy("database_path", "");
499 cfg->setupControl(true, true, true);
500 (*cfg)["server"].append("port_peer");
501 (*cfg)["server"].append("port_rpc");
502 (*cfg)["server"].append("port_ws");
503 return cfg;
504 }),
506 });
507 BEAST_EXPECT(
508 messages.find("Missing section: [port_peer]") != std::string::npos);
509 }
510
511 void
512 run() override
513 {
514 basicTests();
515 stressTest();
517 }
518};
519
520BEAST_DEFINE_TESTSUITE(Server, server, ripple);
521
522} // namespace test
523} // namespace ripple
T back(T... args)
Abstraction for the underlying message destination.
Definition Journal.h:57
virtual Severity threshold() const
Returns the minimum severity level this sink will report.
A generic endpoint for log messages.
Definition Journal.h:41
A testsuite class.
Definition suite.h:52
log_os< char > log
Logging output stream.
Definition suite.h:149
void pass()
Record a successful test condition.
Definition suite.h:508
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:152
friend class thread
Definition suite.h:304
bool except(F &&f, String const &reason)
Definition suite.h:445
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:530
A multi-protocol server.
Definition ServerImpl.h:31
Persistent state information for a connection session.
Definition Session.h:24
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
virtual void complete()=0
Indicate that the response is complete.
void writeAlways(beast::severities::Severity level, std::string const &text) override
Bypass filter and write text to the sink at the specified severity.
TestSink(beast::unit_test::suite &suite)
void write(beast::severities::Severity level, std::string const &text) override
Write text to the sink at the specified severity.
beast::unit_test::suite & suite_
boost::asio::io_context & get_io_context()
boost::asio::io_context io_context_
std::optional< boost::asio::executor_work_guard< boost::asio::io_context::executor_type > > work_
bool expect_read(SyncReadStream &s, std::string const &match)
bool connect(Socket &s, typename Socket::endpoint_type const &ep)
bool write(SyncWriteStream &s, std::string const &text)
void test_keepalive(boost::asio::ip::tcp::endpoint const &ep)
void test_request(boost::asio::ip::tcp::endpoint const &ep)
void run() override
Runs the suite.
A transaction testing environment.
Definition Env.h:102
T endl(T... args)
T find(T... args)
T is_same_v
T join(T... args)
bool is_keep_alive(boost::beast::http::message< isRequest, Body, Fields > const &m)
Definition rfc2616.h:367
A namespace for easy access to logging severity values.
Definition Journal.h:11
Severity
Severity level / threshold of a Journal message.
Definition Journal.h:13
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:35
boost::beast::tcp_stream socket_type
boost::beast::ssl_stream< socket_type > stream_type
char const * getEnvLocalhostAddr()
Definition envconfig.h:17
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
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
boost::beast::http::request< boost::beast::http::dynamic_body > http_request_type
Definition Handoff.h:14
STL namespace.
T reset(T... args)
T resize(T... args)
T sleep_for(T... args)
static std::string nodeDatabase()
static std::string importNodeDatabase()
Used to indicate the result of a server connection handoff.
Definition Handoff.h:21
void onClose(Session &session, boost::system::error_code const &)
bool onAccept(Session &session, boost::asio::ip::tcp::endpoint endpoint)
Handoff onHandoff(Session &session, std::unique_ptr< stream_type > &&bundle, http_request_type &&request, boost::asio::ip::tcp::endpoint remote_address)
void onWSMessage(std::shared_ptr< WSSession > session, std::vector< boost::asio::const_buffer > const &)
Handoff onHandoff(Session &session, http_request_type &&request, boost::asio::ip::tcp::endpoint remote_address)
T what(T... args)