Refactor web server (#667)

Fixs #674
This commit is contained in:
cyan317
2023-06-08 13:25:49 +01:00
committed by GitHub
parent 9836e4ceaf
commit 435db339df
35 changed files with 2857 additions and 1789 deletions

View File

@@ -22,7 +22,6 @@
#include <util/MockBackend.h>
#include <util/MockWsBase.h>
#include <util/TestObject.h>
#include <webserver/WsBase.h>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
@@ -66,7 +65,7 @@ TEST(SubscriptionManagerTest, InitAndReport)
}
void
CheckSubscriberMessage(std::string out, std::shared_ptr<WsBase> session, int retry = 10)
CheckSubscriberMessage(std::string out, std::shared_ptr<Server::ConnectionBase> session, int retry = 10)
{
auto sessionPtr = static_cast<MockSession*>(session.get());
while (retry-- != 0)
@@ -87,7 +86,7 @@ protected:
clio::Config cfg;
std::shared_ptr<SubscriptionManager> subManagerPtr;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
std::shared_ptr<WsBase> session;
std::shared_ptr<Server::ConnectionBase> session;
void
SetUp() override
{
@@ -119,8 +118,8 @@ TEST_F(SubscriptionManagerSimpleBackendTest, ReportCurrentSubscriber)
"books":2,
"book_changes":2
})";
std::shared_ptr<WsBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
subManagerPtr->subBookChanges(session1);
subManagerPtr->subBookChanges(session2);
subManagerPtr->subManifest(session1);
@@ -259,7 +258,7 @@ TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerAccountProposedT
auto account = GetAccountIDWithString(ACCOUNT1);
subManagerPtr->subProposedAccount(account, session);
std::shared_ptr<WsBase> sessionIdle = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> sessionIdle = std::make_shared<MockSession>(tagDecoratorFactory);
auto accountIdle = GetAccountIDWithString(ACCOUNT2);
subManagerPtr->subProposedAccount(accountIdle, sessionIdle);
@@ -743,7 +742,7 @@ TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerOrderBook)
CheckSubscriberMessage(OrderbookPublish, session);
// trigger by offer cancel meta data
std::shared_ptr<WsBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
subManagerPtr->subBook(book, session1);
metaObj = CreateMetaDataForCancelOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
@@ -832,7 +831,7 @@ TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerOrderBook)
"engine_result":"tesSUCCESS",
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
subManagerPtr->subBook(book, session2);
metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();

View File

@@ -17,7 +17,9 @@
*/
//==============================================================================
#include <subscriptions/Message.h>
#include <subscriptions/SubscriptionManager.h>
#include <util/Fixtures.h>
#include <util/MockWsBase.h>
@@ -49,8 +51,8 @@ class SubscriptionMapTest : public SubscriptionTest
TEST_F(SubscriptionTest, SubscriptionCount)
{
Subscription sub(ctx);
std::shared_ptr<WsBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
sub.subscribe(session1);
sub.subscribe(session2);
ctx.run();
@@ -79,13 +81,13 @@ TEST_F(SubscriptionTest, SubscriptionCount)
TEST_F(SubscriptionTest, SubscriptionPublish)
{
Subscription sub(ctx);
std::shared_ptr<WsBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
sub.subscribe(session1);
sub.subscribe(session2);
ctx.run();
EXPECT_EQ(sub.count(), 2);
sub.publish(std::make_shared<Message>("message"));
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
MockSession* p1 = (MockSession*)(session1.get());
@@ -95,7 +97,7 @@ TEST_F(SubscriptionTest, SubscriptionPublish)
sub.unsubscribe(session1);
ctx.restart();
ctx.run();
sub.publish(std::make_shared<Message>("message2"));
sub.publish(std::make_shared<std::string>("message2"));
ctx.restart();
ctx.run();
EXPECT_EQ(p1->message, "message");
@@ -106,16 +108,16 @@ TEST_F(SubscriptionTest, SubscriptionPublish)
TEST_F(SubscriptionTest, SubscriptionDeadRemoveSubscriber)
{
Subscription sub(ctx);
std::shared_ptr<WsBase> session1(new MockDeadSession(tagDecoratorFactory));
std::shared_ptr<Server::ConnectionBase> session1(new MockDeadSession(tagDecoratorFactory));
sub.subscribe(session1);
ctx.run();
EXPECT_EQ(sub.count(), 1);
// trigger dead
sub.publish(std::make_shared<Message>("message"));
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(session1->dead(), true);
sub.publish(std::make_shared<Message>("message"));
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(sub.count(), 0);
@@ -123,9 +125,9 @@ TEST_F(SubscriptionTest, SubscriptionDeadRemoveSubscriber)
TEST_F(SubscriptionMapTest, SubscriptionMapCount)
{
std::shared_ptr<WsBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session3 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session3 = std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionMap<std::string> subMap(ctx);
subMap.subscribe(session1, "topic1");
subMap.subscribe(session2, "topic1");
@@ -155,8 +157,8 @@ TEST_F(SubscriptionMapTest, SubscriptionMapCount)
TEST_F(SubscriptionMapTest, SubscriptionMapPublish)
{
std::shared_ptr<WsBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionMap<std::string> subMap(ctx);
const std::string topic1 = "topic1";
const std::string topic2 = "topic2";
@@ -166,9 +168,9 @@ TEST_F(SubscriptionMapTest, SubscriptionMapPublish)
subMap.subscribe(session2, topic2);
ctx.run();
EXPECT_EQ(subMap.count(), 2);
auto message1 = std::make_shared<Message>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<Message>(topic2Message.data()), topic2); // rvalue
auto message1 = std::make_shared<std::string>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<std::string>(topic2Message.data()), topic2); // rvalue
ctx.restart();
ctx.run();
MockSession* p1 = (MockSession*)(session1.get());
@@ -179,8 +181,8 @@ TEST_F(SubscriptionMapTest, SubscriptionMapPublish)
TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber)
{
std::shared_ptr<WsBase> session1(new MockDeadSession(tagDecoratorFactory));
std::shared_ptr<WsBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<Server::ConnectionBase> session1(new MockDeadSession(tagDecoratorFactory));
std::shared_ptr<Server::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionMap<std::string> subMap(ctx);
const std::string topic1 = "topic1";
const std::string topic2 = "topic2";
@@ -190,9 +192,9 @@ TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber)
subMap.subscribe(session2, topic2);
ctx.run();
EXPECT_EQ(subMap.count(), 2);
auto message1 = std::make_shared<Message>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<Message>(topic2Message.data()), topic2); // rvalue
auto message1 = std::make_shared<std::string>(topic1Message);
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<std::string>(topic2Message), topic2); // rvalue
ctx.restart();
ctx.run();
MockDeadSession* p1 = (MockDeadSession*)(session1.get());

View File

@@ -62,7 +62,7 @@ protected:
}
std::shared_ptr<SubscriptionManager> subManager_;
std::shared_ptr<WsBase> session_;
std::shared_ptr<Server::ConnectionBase> session_;
};
struct SubscribeParamTestCaseBundle
@@ -740,17 +740,11 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothSnapshotSet)
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
auto const issuer = GetAccountIDWithString(ACCOUNT);
auto const getsXRPPaysUSDBook = getBookBase(std::get<ripple::Book>(RPC::parseBook(
ripple::to_currency("USD"), // pays
issuer,
ripple::xrpCurrency(), // gets
ripple::xrpAccount())));
auto const getsXRPPaysUSDBook = getBookBase(std::get<ripple::Book>(
RPC::parseBook(ripple::to_currency("USD"), issuer, ripple::xrpCurrency(), ripple::xrpAccount())));
auto const reversedBook = getBookBase(std::get<ripple::Book>(RPC::parseBook(
ripple::xrpCurrency(), // pays
ripple::xrpAccount(),
ripple::to_currency("USD"), // gets
issuer)));
auto const reversedBook = getBookBase(std::get<ripple::Book>(
RPC::parseBook(ripple::xrpCurrency(), ripple::xrpAccount(), ripple::to_currency("USD"), issuer)));
ON_CALL(*rawBackendPtr, doFetchSuccessorKey(getsXRPPaysUSDBook, MAXSEQ, _))
.WillByDefault(Return(ripple::uint256{PAYS20USDGETS10XRPBOOKDIR}));

View File

@@ -55,7 +55,7 @@ protected:
}
std::shared_ptr<SubscriptionManager> subManager_;
std::shared_ptr<WsBase> session_;
std::shared_ptr<Server::ConnectionBase> session_;
};
struct UnsubscribeParamTestCaseBundle

View File

@@ -128,8 +128,18 @@ struct AsyncAsioContextTest : virtual public NoLoggerFixture
~AsyncAsioContextTest()
{
work.reset();
if (runner.joinable())
runner.join();
ctx.stop();
runner.join();
}
void
stop()
{
work.reset();
ctx.stop();
if (runner.joinable())
runner.join();
}
protected:

View File

@@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Types.h>
#include <webserver/Context.h>
#include <boost/asio.hpp>
#include <gtest/gtest.h>
#include <string>
struct MockAsyncRPCEngine
{
public:
MockAsyncRPCEngine()
{
work_.emplace(ioc_); // make sure ctx does not stop on its own
runner_.emplace([this] { ioc_.run(); });
}
~MockAsyncRPCEngine()
{
work_.reset();
ioc_.stop();
if (runner_->joinable())
runner_->join();
}
template <typename Fn>
bool
post(Fn&& func, std::string const& ip)
{
boost::asio::spawn(ioc_, [handler = std::move(func)](auto yield) mutable { handler(yield); });
return true;
}
MOCK_METHOD(void, notifyComplete, (std::string const&, std::chrono::microseconds const&), ());
MOCK_METHOD(void, notifyErrored, (std::string const&), ());
MOCK_METHOD(void, notifyForwarded, (std::string const&), ());
MOCK_METHOD(RPC::Result, buildResponse, (Web::Context const&), ());
private:
boost::asio::io_context ioc_;
std::optional<boost::asio::io_service::work> work_;
std::optional<std::thread> runner_;
};
struct MockRPCEngine
{
public:
MOCK_METHOD(bool, post, (std::function<void(boost::asio::yield_context)>&&, std::string const&), ());
MOCK_METHOD(void, notifyComplete, (std::string const&, std::chrono::microseconds const&), ());
MOCK_METHOD(void, notifyErrored, (std::string const&), ());
MOCK_METHOD(void, notifyForwarded, (std::string const&), ());
MOCK_METHOD(RPC::Result, buildResponse, (Web::Context const&), ());
};

View File

@@ -20,7 +20,7 @@
#pragma once
#include <ripple/ledger/ReadView.h>
#include <webserver/WsBase.h>
#include <webserver/interface/ConnectionBase.h>
#include <boost/asio/spawn.hpp>
#include <boost/json.hpp>
@@ -28,7 +28,11 @@
struct MockSubscriptionManager
{
using session_ptr = std::shared_ptr<WsBase>;
public:
using session_ptr = std::shared_ptr<Server::ConnectionBase>;
MockSubscriptionManager()
{
}
MOCK_METHOD(boost::json::object, subLedger, (boost::asio::yield_context&, session_ptr), ());
@@ -60,9 +64,9 @@ struct MockSubscriptionManager
MOCK_METHOD(void, unsubBook, (ripple::Book const&, session_ptr), ());
MOCK_METHOD(void, subBookChanges, (std::shared_ptr<WsBase>), ());
MOCK_METHOD(void, subBookChanges, (session_ptr), ());
MOCK_METHOD(void, unsubBookChanges, (std::shared_ptr<WsBase>), ());
MOCK_METHOD(void, unsubBookChanges, (session_ptr), ());
MOCK_METHOD(void, subManifest, (session_ptr), ());

View File

@@ -19,30 +19,43 @@
#pragma once
#include <webserver/WsBase.h>
#include <webserver/interface/ConnectionBase.h>
struct MockSession : public WsBase
struct MockSession : public Server::ConnectionBase
{
std::string message;
void
send(std::shared_ptr<Message> msg_type) override
send(std::shared_ptr<std::string> msg_type) override
{
message += std::string(msg_type->data());
}
MockSession(util::TagDecoratorFactory const& factory) : WsBase(factory)
void
send(std::string&& msg, boost::beast::http::status status = boost::beast::http::status::ok) override
{
message += msg;
}
MockSession(util::TagDecoratorFactory const& factory) : Server::ConnectionBase(factory, "")
{
}
};
struct MockDeadSession : public WsBase
struct MockDeadSession : public Server::ConnectionBase
{
void
send(std::shared_ptr<Message> msg_type) override
send(std::shared_ptr<std::string> _) override
{
// err happen, the session should remove from subscribers
ec_.assign(2, boost::system::system_category());
}
MockDeadSession(util::TagDecoratorFactory const& factory) : WsBase(factory)
void
send(std::string&& _, boost::beast::http::status __ = boost::beast::http::status::ok) override
{
}
MockDeadSession(util::TagDecoratorFactory const& factory) : Server::ConnectionBase(factory, "")
{
}
};

View File

@@ -0,0 +1,253 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/beast/http.hpp>
#include <string>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
struct HttpSyncClient
{
static std::string
syncPost(std::string const& host, std::string const& port, std::string const& body)
{
boost::asio::io_context ioc;
// These objects perform our I/O
net::ip::tcp::resolver resolver(ioc);
boost::beast::tcp_stream stream(ioc);
// Look up the domain name
auto const results = resolver.resolve(host, port);
// Make the connection on the IP address we get from a lookup
stream.connect(results);
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
// Send the HTTP request to the remote host
http::write(stream, req);
// This buffer is used for reading and must be persisted
boost::beast::flat_buffer buffer;
// Declare a container to hold the response
http::response<http::string_body> res;
// Receive the HTTP response
http::read(stream, buffer, res);
// Gracefully close the socket
boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return std::string(res.body());
}
};
class WebSocketSyncClient
{
// The io_context is required for all I/O
net::io_context ioc_;
// These objects perform our I/O
tcp::resolver resolver_{ioc_};
boost::beast::websocket::stream<tcp::socket> ws_{ioc_};
public:
void
connect(std::string const& host, std::string const& port)
{
// Look up the domain name
auto const results = resolver_.resolve(host, port);
// Make the connection on the IP address we get from a lookup
auto const ep = net::connect(ws_.next_layer(), results);
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
auto const hostPort = host + ':' + std::to_string(ep.port());
// Set a decorator to change the User-Agent of the handshake
ws_.set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
// Perform the websocket handshake
ws_.handshake(hostPort, "/");
}
void
disconnect()
{
ws_.close(boost::beast::websocket::close_code::normal);
}
std::string
syncPost(std::string const& body)
{
// Send the message
ws_.write(net::buffer(std::string(body)));
// This buffer will hold the incoming message
boost::beast::flat_buffer buffer;
// Read a message into our buffer
ws_.read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
};
struct HttpsSyncClient
{
static bool
verify_certificate(bool preverified, boost::asio::ssl::verify_context& ctx)
{
return true;
}
static std::string
syncPost(std::string const& host, std::string const& port, std::string const& body)
{
// The io_context is required for all I/O
net::io_context ioc;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
// Verify the remote server's certificate
ctx.set_verify_mode(ssl::verify_none);
// These objects perform our I/O
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
// disable ssl verification just for testing
// stream.set_verify_callback(HttpsSyncClient::verify_certificate);
// Set SNI Hostname (many hosts need this to handshake successfully)
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
{
boost::beast::error_code ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
throw boost::beast::system_error{ec};
}
// Look up the domain name
auto const results = resolver.resolve(host, port);
// Make the connection on the IP address we get from a lookup
boost::beast::get_lowest_layer(stream).connect(results);
// Perform the SSL handshake
stream.handshake(ssl::stream_base::client);
// Set up an HTTP GET request message
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
// Send the HTTP request to the remote host
http::write(stream, req);
// This buffer is used for reading and must be persisted
boost::beast::flat_buffer buffer;
// Declare a container to hold the response
http::response<http::string_body> res;
// Receive the HTTP response
http::read(stream, buffer, res);
// Write the message to standard out
std::cout << res << std::endl;
// Gracefully close the stream
boost::beast::error_code ec;
stream.shutdown(ec);
return std::string(res.body());
}
};
class WebServerSslSyncClient
{
net::io_context ioc_;
std::optional<boost::beast::websocket::stream<boost::beast::ssl_stream<tcp::socket>>> ws_;
public:
void
connect(std::string const& host, std::string const& port)
{
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
// Verify the remote server's certificate
ctx.set_verify_mode(ssl::verify_none);
// These objects perform our I/O
tcp::resolver resolver{ioc_};
ws_.emplace(ioc_, ctx);
// Look up the domain name
auto const results = resolver.resolve(host, port);
// Make the connection on the IP address we get from a lookup
net::connect(ws_->next_layer().next_layer(), results.begin(), results.end());
// Perform the SSL handshake
ws_->next_layer().handshake(ssl::stream_base::client);
// Set a decorator to change the User-Agent of the handshake
ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
// Perform the websocket handshake
ws_->handshake(host, "/");
}
void
disconnect()
{
ws_->close(boost::beast::websocket::close_code::normal);
}
std::string
syncPost(std::string const& body)
{
// Send the message
ws_->write(net::buffer(std::string(body)));
// This buffer will hold the incoming message
boost::beast::flat_buffer buffer;
// Read a message into our buffer
ws_->read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
};

View File

@@ -0,0 +1,675 @@
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <util/Fixtures.h>
#include <util/MockETLService.h>
#include <util/MockRPCEngine.h>
#include <webserver/RPCExecutor.h>
#include <chrono>
#include <gtest/gtest.h>
using namespace std::chrono_literals;
constexpr static auto MINSEQ = 10;
constexpr static auto MAXSEQ = 30;
struct MockWsBase : public Server::ConnectionBase
{
std::string message;
void
send(std::shared_ptr<std::string> msg_type) override
{
message += std::string(msg_type->data());
}
void
send(std::string&& msg, boost::beast::http::status status = boost::beast::http::status::ok) override
{
message += std::string(msg.data());
}
MockWsBase(util::TagDecoratorFactory const& factory) : Server::ConnectionBase(factory, "localhost.fake.ip")
{
}
};
class WebRPCExecutorTest : public MockBackendTest
{
protected:
void
SetUp() override
{
MockBackendTest::SetUp();
etl = std::make_shared<MockETLService>();
rpcEngine = std::make_shared<MockAsyncRPCEngine>();
tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
subManager = std::make_shared<SubscriptionManager>(cfg, mockBackendPtr);
session = std::make_shared<MockWsBase>(*tagFactory);
rpcExecutor = std::make_shared<RPCExecutor<MockAsyncRPCEngine, MockETLService>>(
cfg, mockBackendPtr, rpcEngine, etl, subManager);
}
void
TearDown() override
{
MockBackendTest::TearDown();
}
std::shared_ptr<MockAsyncRPCEngine> rpcEngine;
std::shared_ptr<MockETLService> etl;
std::shared_ptr<SubscriptionManager> subManager;
std::shared_ptr<util::TagDecoratorFactory> tagFactory;
std::shared_ptr<RPCExecutor<MockAsyncRPCEngine, MockETLService>> rpcExecutor;
std::shared_ptr<MockWsBase> session;
clio::Config cfg;
};
TEST_F(WebRPCExecutorTest, HTTPDefaultPath)
{
auto request = boost::json::parse(R"({
"method": "server_info",
"params": [{}]
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr result = "{}";
static auto constexpr response = R"({
"result":{
"status":"success"
},
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
}
]
})";
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(boost::json::parse(result).as_object()));
EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsNormalPath)
{
session->upgraded = true;
auto request = boost::json::parse(R"({
"command": "server_info",
"id": 99
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr result = "{}";
static auto constexpr response = R"({
"result":{
},
"id":99,
"status":"success",
"type":"response",
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
}
]
})";
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(boost::json::parse(result).as_object()));
EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPForwardedPath)
{
auto request = boost::json::parse(R"({
"method": "server_info",
"params": [{}]
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr result = R"({
"result": {
"index": 1
},
"forwarded": true
})";
static auto constexpr response = R"({
"result":{
"index": 1,
"status": "success"
},
"forwarded": true,
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
}
]
})";
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(boost::json::parse(result).as_object()));
EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsForwardedPath)
{
session->upgraded = true;
auto request = boost::json::parse(R"({
"command": "server_info",
"id": 99
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr result = R"({
"result": {
"index": 1
},
"forwarded": true
})";
static auto constexpr response = R"({
"result":{
"index": 1
},
"forwarded": true,
"id":99,
"status":"success",
"type":"response",
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
}
]
})";
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(boost::json::parse(result).as_object()));
EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPErrorPath)
{
static auto constexpr response = R"({
"result": {
"error": "invalidParams",
"error_code": 31,
"error_message": "ledgerIndexMalformed",
"status": "error",
"type": "response",
"request": {
"method": "ledger",
"params": [
{
"ledger_index": "xx"
}
]
}
},
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
}
]
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr requestJSON = R"({
"method": "ledger",
"params": [
{
"ledger_index": "xx"
}
]
})";
auto request = boost::json::parse(requestJSON).as_object();
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"}));
EXPECT_CALL(*rpcEngine, notifyErrored("ledger")).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsErrorPath)
{
session->upgraded = true;
static auto constexpr response = R"({
"id": "123",
"error": "invalidParams",
"error_code": 31,
"error_message": "ledgerIndexMalformed",
"status": "error",
"type": "response",
"request": {
"command": "ledger",
"ledger_index": "xx",
"id": "123"
},
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
}
]
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr requestJSON = R"({
"command": "ledger",
"ledger_index": "xx",
"id": "123"
})";
auto request = boost::json::parse(requestJSON).as_object();
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(RPC::Status{RPC::RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"}));
EXPECT_CALL(*rpcEngine, notifyErrored("ledger")).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPNotReady)
{
auto request = boost::json::parse(R"({
"method": "server_info",
"params": [{}]
})")
.as_object();
static auto constexpr response = R"({
"result":{
"error":"notReady",
"error_code":13,
"error_message":"Not ready to handle this request.",
"status":"error",
"type":"response",
"request":{
"method":"server_info",
"params":[
{
}
]
}
}
})";
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsNotReady)
{
session->upgraded = true;
auto request = boost::json::parse(R"({
"command": "server_info",
"id": 99
})")
.as_object();
static auto constexpr response = R"({
"error":"notReady",
"error_code":13,
"error_message":"Not ready to handle this request.",
"status":"error",
"type":"response",
"id":99,
"request":{
"command":"server_info",
"id":99
}
})";
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPBadSyntax)
{
auto request = boost::json::parse(R"({
"method2": "server_info"
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"result":{
"error":"badSyntax",
"error_code":1,
"error_message":"Syntax error.",
"status":"error",
"type":"response",
"request":{
"method2":"server_info",
"params":[{}]
}
}
})";
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPBadSyntaxWhenRequestSubscribe)
{
auto request = boost::json::parse(R"({
"method": "subscribe"
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"result":{
"error":"badSyntax",
"error_code":1,
"error_message":"Syntax error.",
"status":"error",
"type":"response",
"request":{
"method":"subscribe",
"params":[{}]
}
}
})";
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsBadSyntax)
{
session->upgraded = true;
auto request = boost::json::parse(R"(
{
"command2": "server_info",
"id": 99
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"error":"badSyntax",
"error_code":1,
"error_message":"Syntax error.",
"status":"error",
"type":"response",
"id":99,
"request":{
"command2":"server_info",
"id":99
}
})";
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPInternalError)
{
static auto constexpr response = R"({
"result": {
"error":"internal",
"error_code":73,
"error_message":"Internal error.",
"status":"error",
"type":"response",
"request":{
"method": "ledger",
"params": [
{
}
]
}
}
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr requestJSON = R"({
"method": "ledger",
"params": [
{
}
]
})";
auto request = boost::json::parse(requestJSON).as_object();
EXPECT_CALL(*rpcEngine, buildResponse(testing::_)).Times(1).WillOnce(testing::Throw(std::runtime_error("MyError")));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsInternalError)
{
session->upgraded = true;
static auto constexpr response = R"({
"error":"internal",
"error_code":73,
"error_message":"Internal error.",
"status":"error",
"type":"response",
"id":"123",
"request":{
"command":"ledger",
"id":"123"
}
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr requestJSON = R"({
"command": "ledger",
"id": "123"
})";
auto request = boost::json::parse(requestJSON).as_object();
EXPECT_CALL(*rpcEngine, buildResponse(testing::_)).Times(1).WillOnce(testing::Throw(std::runtime_error("MyError")));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPOutDated)
{
auto request = boost::json::parse(R"({
"method": "server_info",
"params": [{}]
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr result = "{}";
static auto constexpr response = R"({
"result":{
"status":"success"
},
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
},
{
"id":2002,
"message":"This server may be out of date"
}
]
})";
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(boost::json::parse(result).as_object()));
EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(61));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsOutdated)
{
session->upgraded = true;
auto request = boost::json::parse(R"({
"command": "server_info",
"id": 99
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr result = "{}";
static auto constexpr response = R"({
"result":{
},
"id":99,
"status":"success",
"type":"response",
"warnings":[
{
"id":2001,
"message":"This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request"
},
{
"id":2002,
"message":"This server may be out of date"
}
]
})";
EXPECT_CALL(*rpcEngine, buildResponse(testing::_))
.WillOnce(testing::Return(boost::json::parse(result).as_object()));
EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1);
EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(61));
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WsTooBusy)
{
session->upgraded = true;
auto rpcEngine2 = std::make_shared<MockRPCEngine>();
auto rpcExecutor2 =
std::make_shared<RPCExecutor<MockRPCEngine, MockETLService>>(cfg, mockBackendPtr, rpcEngine2, etl, subManager);
auto request = boost::json::parse(R"({
"command": "server_info",
"id": 99
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response =
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})";
EXPECT_CALL(*rpcEngine2, post).WillOnce(testing::Return(false));
(*rpcExecutor2)(std::move(request), session);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPTooBusy)
{
auto rpcEngine2 = std::make_shared<MockRPCEngine>();
auto rpcExecutor2 =
std::make_shared<RPCExecutor<MockRPCEngine, MockETLService>>(cfg, mockBackendPtr, rpcEngine2, etl, subManager);
auto request = boost::json::parse(R"({
"method": "server_info",
"params": [{}]
})")
.as_object();
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response =
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})";
EXPECT_CALL(*rpcEngine2, post).WillOnce(testing::Return(false));
(*rpcExecutor2)(std::move(request), session);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}

View File

@@ -0,0 +1,357 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <util/Fixtures.h>
#include <util/TestHttpSyncClient.h>
#include <webserver/Server.h>
#include <boost/json/parse.hpp>
#include <fmt/core.h>
#include <gtest/gtest.h>
#include <optional>
constexpr static auto JSONData = R"JSON(
{
"server":{
"ip":"0.0.0.0",
"port":8888
},
"dos_guard": {
"max_fetches": 100,
"sweep_interval": 1000,
"max_connections": 2,
"max_requests": 3,
"whitelist": ["127.0.0.1"]
}
}
)JSON";
constexpr static auto JSONDataOverload = R"JSON(
{
"server":{
"ip":"0.0.0.0",
"port":8888
},
"dos_guard": {
"max_fetches": 100,
"sweep_interval": 1000,
"max_connections": 2,
"max_requests": 1
}
}
)JSON";
// for testing, we use a self-signed certificate
std::optional<ssl::context>
parseCertsForTest()
{
std::string const key = R"(-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4
32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd
g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x
evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC
tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC
oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4
YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1
vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ
ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh
+lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk
naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK
nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn
K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z
dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz
fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs
fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7
zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl
6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf
pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV
zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI
9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya
LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD
KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du
O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7
Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA==
-----END RSA PRIVATE KEY-----)";
std::string const cert = R"(-----BEGIN CERTIFICATE-----
MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV
BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz
Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN
MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m
pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y
7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w
A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk
PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979
WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p
AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x
MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN
BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn
svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO
p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb
8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp
LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+
PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw==
-----END CERTIFICATE-----)";
ssl::context ctx{ssl::context::tlsv12};
ctx.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2);
ctx.use_certificate_chain(boost::asio::buffer(cert.data(), cert.size()));
ctx.use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context::file_format::pem);
return ctx;
}
class WebServerTest : public NoLoggerFixture
{
protected:
WebServerTest()
{
work.emplace(ctx); // make sure ctx does not stop on its own
runner.emplace([this] { ctx.run(); });
}
~WebServerTest()
{
work.reset();
ctx.stop();
if (runner->joinable())
runner->join();
}
void
SetUp() override
{
NoLoggerFixture::SetUp();
}
// this ctx is for dos timer
boost::asio::io_context ctxSync;
clio::Config cfg{boost::json::parse(JSONData)};
clio::IntervalSweepHandler sweepHandler = clio::IntervalSweepHandler{cfg, ctxSync};
clio::DOSGuard dosGuard = clio::DOSGuard{cfg, sweepHandler};
clio::Config cfgOverload{boost::json::parse(JSONDataOverload)};
clio::IntervalSweepHandler sweepHandlerOverload = clio::IntervalSweepHandler{cfgOverload, ctxSync};
clio::DOSGuard dosGuardOverload = clio::DOSGuard{cfgOverload, sweepHandlerOverload};
// this ctx is for http server
boost::asio::io_context ctx;
private:
std::optional<boost::asio::io_service::work> work;
std::optional<std::thread> runner;
};
class EchoExecutor
{
public:
void
operator()(boost::json::object&& req, std::shared_ptr<Server::ConnectionBase> const& ws)
{
ws->send(boost::json::serialize(req), http::status::ok);
}
void
operator()(boost::beast::error_code ec, std::shared_ptr<Server::ConnectionBase> const& ws)
{
}
};
class ExceptionExecutor
{
public:
void
operator()(boost::json::object&& req, std::shared_ptr<Server::ConnectionBase> const& ws)
{
throw std::runtime_error("MyError");
}
void
operator()(boost::beast::error_code ec, std::shared_ptr<Server::ConnectionBase> const& ws)
{
}
};
TEST_F(WebServerTest, Http)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", "8888", R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})");
}
TEST_F(WebServerTest, Ws)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", "8888");
auto const res = wsClient.syncPost(R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})");
wsClient.disconnect();
}
TEST_F(WebServerTest, HttpBodyNotJsonValue)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", "8888", R"({)");
EXPECT_EQ(
res,
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})");
}
TEST_F(WebServerTest, HttpBodyNotJsonObject)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", "8888", R"("123")");
EXPECT_EQ(
res,
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})");
}
TEST_F(WebServerTest, WsBodyNotJsonValue)
{
auto e = std::make_shared<EchoExecutor>();
WebSocketSyncClient wsClient;
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
wsClient.connect("localhost", "8888");
auto const res = wsClient.syncPost(R"({)");
wsClient.disconnect();
EXPECT_EQ(
res,
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response","request":["{"]})");
}
TEST_F(WebServerTest, WsBodyNotJsonObject)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", "8888");
auto const res = wsClient.syncPost(R"("Hello")");
wsClient.disconnect();
EXPECT_EQ(
res,
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response","request":"Hello"})");
}
TEST_F(WebServerTest, HttpInternalError)
{
auto e = std::make_shared<ExceptionExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", "8888", R"({})");
EXPECT_EQ(
res,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})");
}
TEST_F(WebServerTest, WsInternalError)
{
auto e = std::make_shared<ExceptionExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuard, e);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", "8888");
auto const res = wsClient.syncPost(R"({})");
wsClient.disconnect();
EXPECT_EQ(
res,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","request":{}})");
}
TEST_F(WebServerTest, Https)
{
auto e = std::make_shared<EchoExecutor>();
auto sslCtx = parseCertsForTest();
auto const ctxSslRef = sslCtx ? std::optional<std::reference_wrapper<ssl::context>>{sslCtx.value()} : std::nullopt;
auto const server = Server::make_HttpServer(cfg, ctx, ctxSslRef, dosGuard, e);
auto const res = HttpsSyncClient::syncPost("localhost", "8888", R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})");
}
TEST_F(WebServerTest, Wss)
{
auto e = std::make_shared<EchoExecutor>();
auto sslCtx = parseCertsForTest();
auto const ctxSslRef = sslCtx ? std::optional<std::reference_wrapper<ssl::context>>{sslCtx.value()} : std::nullopt;
auto const server = Server::make_HttpServer(cfg, ctx, ctxSslRef, dosGuard, e);
WebServerSslSyncClient wsClient;
wsClient.connect("localhost", "8888");
auto const res = wsClient.syncPost(R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})");
wsClient.disconnect();
}
TEST_F(WebServerTest, HttpRequestOverload)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuardOverload, e);
auto res = HttpSyncClient::syncPost("localhost", "8888", R"({})");
EXPECT_EQ(res, "{}");
res = HttpSyncClient::syncPost("localhost", "8888", R"({})");
EXPECT_EQ(
res,
R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})");
}
TEST_F(WebServerTest, WsRequestOverload)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuardOverload, e);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", "8888");
auto res = wsClient.syncPost(R"({})");
wsClient.disconnect();
EXPECT_EQ(res, "{}");
WebSocketSyncClient wsClient2;
wsClient2.connect("localhost", "8888");
res = wsClient2.syncPost(R"({})");
wsClient2.disconnect();
EXPECT_EQ(
res,
R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response","request":{}})");
}
TEST_F(WebServerTest, HttpPayloadOverload)
{
std::string const s100(100, 'a');
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuardOverload, e);
auto const res = HttpSyncClient::syncPost("localhost", "8888", fmt::format(R"({{"payload":"{}"}})", s100));
EXPECT_EQ(
res,
R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})");
}
TEST_F(WebServerTest, WsPayloadOverload)
{
std::string const s100(100, 'a');
auto e = std::make_shared<EchoExecutor>();
auto const server = Server::make_HttpServer(cfg, ctx, std::nullopt, dosGuardOverload, e);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", "8888");
auto const res = wsClient.syncPost(fmt::format(R"({{"payload":"{}"}})", s100));
wsClient.disconnect();
EXPECT_EQ(
res,
R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})");
}