mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 11:55:51 +00:00
@@ -47,7 +47,6 @@ CliArgs::parse(int argc, char const* argv[])
|
||||
("help,h", "Print help message and exit")
|
||||
("version,v", "Print version and exit")
|
||||
("conf,c", po::value<std::string>()->default_value(kDEFAULT_CONFIG_PATH), "Configuration file")
|
||||
("ng-web-server,w", "Use ng-web-server")
|
||||
("migrate", po::value<std::string>(), "Start migration helper")
|
||||
("verify", "Checks the validity of config values")
|
||||
("config-description,d", po::value<std::string>(), "Generate config description markdown file")
|
||||
@@ -93,8 +92,7 @@ CliArgs::parse(int argc, char const* argv[])
|
||||
if (parsed.count("verify") != 0u)
|
||||
return Action{Action::VerifyConfig{.configPath = std::move(configPath)}};
|
||||
|
||||
return Action{Action::Run{.configPath = std::move(configPath), .useNgWebServer = parsed.count("ng-web-server") != 0}
|
||||
};
|
||||
return Action{Action::Run{.configPath = std::move(configPath)}};
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
@@ -45,7 +45,6 @@ public:
|
||||
/** @brief Run action. */
|
||||
struct Run {
|
||||
std::string configPath; ///< Configuration file path.
|
||||
bool useNgWebServer; ///< Whether to use a ng web server
|
||||
};
|
||||
|
||||
/** @brief Exit action. */
|
||||
|
||||
@@ -49,8 +49,6 @@
|
||||
#include "web/dosguard/IntervalSweepHandler.hpp"
|
||||
#include "web/dosguard/Weights.hpp"
|
||||
#include "web/dosguard/WhitelistHandler.hpp"
|
||||
#include "web/ng/RPCServerHandler.hpp"
|
||||
#include "web/ng/Server.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
@@ -96,7 +94,7 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
|
||||
}
|
||||
|
||||
int
|
||||
ClioApplication::run(bool const useNgWebServer)
|
||||
ClioApplication::run()
|
||||
{
|
||||
auto const threads = config_.get<uint16_t>("io_threads");
|
||||
LOG(util::LogService::info()) << "Number of io threads = " << threads;
|
||||
@@ -170,51 +168,37 @@ ClioApplication::run(bool const useNgWebServer)
|
||||
auto const rpcEngine =
|
||||
RPCEngineType::makeRPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
|
||||
|
||||
if (useNgWebServer or config_.get<bool>("server.__ng_web_server")) {
|
||||
web::ng::RPCServerHandler<RPCEngineType> handler{config_, backend, rpcEngine, etl, dosGuard};
|
||||
web::RPCServerHandler<RPCEngineType> handler{config_, backend, rpcEngine, etl, dosGuard};
|
||||
|
||||
auto expectedAdminVerifier = web::makeAdminVerificationStrategy(config_);
|
||||
if (not expectedAdminVerifier.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error creating admin verifier: " << expectedAdminVerifier.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
auto const adminVerifier = std::move(expectedAdminVerifier).value();
|
||||
auto expectedAdminVerifier = web::makeAdminVerificationStrategy(config_);
|
||||
if (not expectedAdminVerifier.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error creating admin verifier: " << expectedAdminVerifier.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
auto const adminVerifier = std::move(expectedAdminVerifier).value();
|
||||
|
||||
auto httpServer = web::ng::makeServer(config_, OnConnectCheck{dosGuard}, DisconnectHook{dosGuard}, ioc);
|
||||
auto httpServer = web::makeServer(config_, OnConnectCheck{dosGuard}, DisconnectHook{dosGuard}, ioc);
|
||||
|
||||
if (not httpServer.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
httpServer->onGet("/metrics", MetricsHandler{adminVerifier});
|
||||
httpServer->onGet("/health", HealthCheckHandler{});
|
||||
auto requestHandler = RequestHandler{adminVerifier, handler};
|
||||
httpServer->onPost("/", requestHandler);
|
||||
httpServer->onWs(std::move(requestHandler));
|
||||
|
||||
auto const maybeError = httpServer->run();
|
||||
if (maybeError.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error starting web server: " << *maybeError;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
appStopper_.setOnStop(
|
||||
Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc)
|
||||
);
|
||||
|
||||
// Blocks until stopped.
|
||||
// When stopped, shared_ptrs fall out of scope
|
||||
// Calls destructors on all resources, and destructs in order
|
||||
start(ioc, threads);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
if (not httpServer.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Init the web server
|
||||
auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(config_, backend, rpcEngine, etl, dosGuard);
|
||||
httpServer->onGet("/metrics", MetricsHandler{adminVerifier});
|
||||
httpServer->onGet("/health", HealthCheckHandler{});
|
||||
auto requestHandler = RequestHandler{adminVerifier, handler};
|
||||
httpServer->onPost("/", requestHandler);
|
||||
httpServer->onWs(std::move(requestHandler));
|
||||
|
||||
auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler);
|
||||
auto const maybeError = httpServer->run();
|
||||
if (maybeError.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error starting web server: " << *maybeError;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
appStopper_.setOnStop(
|
||||
Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc)
|
||||
);
|
||||
|
||||
// Blocks until stopped.
|
||||
// When stopped, shared_ptrs fall out of scope
|
||||
|
||||
@@ -44,12 +44,10 @@ public:
|
||||
/**
|
||||
* @brief Run the application
|
||||
*
|
||||
* @param useNgWebServer Whether to use the new web server
|
||||
*
|
||||
* @return exit code
|
||||
*/
|
||||
int
|
||||
run(bool useNgWebServer);
|
||||
run();
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Server.hpp"
|
||||
#include "web/Server.hpp"
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
@@ -74,7 +74,7 @@ public:
|
||||
* @param ioc The io_context to stop.
|
||||
* @return The callback to be called on application stop.
|
||||
*/
|
||||
template <web::ng::SomeServer ServerType>
|
||||
template <web::SomeServer ServerType>
|
||||
static std::function<void(boost::asio::yield_context)>
|
||||
makeOnStopCallback(
|
||||
ServerType& server,
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/prometheus/Http.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
@@ -41,13 +41,13 @@ OnConnectCheck::OnConnectCheck(web::dosguard::DOSGuardInterface& dosguard) : dos
|
||||
{
|
||||
}
|
||||
|
||||
std::expected<void, web::ng::Response>
|
||||
OnConnectCheck::operator()(web::ng::Connection const& connection)
|
||||
std::expected<void, web::Response>
|
||||
OnConnectCheck::operator()(web::Connection const& connection)
|
||||
{
|
||||
dosguard_.get().increment(connection.ip());
|
||||
if (not dosguard_.get().isOk(connection.ip())) {
|
||||
return std::unexpected{
|
||||
web::ng::Response{boost::beast::http::status::too_many_requests, "Too many requests", connection}
|
||||
web::Response{boost::beast::http::status::too_many_requests, "Too many requests", connection}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ DisconnectHook::DisconnectHook(web::dosguard::DOSGuardInterface& dosguard) : dos
|
||||
}
|
||||
|
||||
void
|
||||
DisconnectHook::operator()(web::ng::Connection const& connection)
|
||||
DisconnectHook::operator()(web::Connection const& connection)
|
||||
{
|
||||
dosguard_.get().decrement(connection.ip());
|
||||
}
|
||||
@@ -69,10 +69,10 @@ MetricsHandler::MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> a
|
||||
{
|
||||
}
|
||||
|
||||
web::ng::Response
|
||||
web::Response
|
||||
MetricsHandler::operator()(
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata& connectionMetadata,
|
||||
web::Request const& request,
|
||||
web::ConnectionMetadata& connectionMetadata,
|
||||
web::SubscriptionContextPtr,
|
||||
boost::asio::yield_context
|
||||
)
|
||||
@@ -86,13 +86,13 @@ MetricsHandler::operator()(
|
||||
httpRequest, adminVerifier_->isAdmin(httpRequest, connectionMetadata.ip())
|
||||
);
|
||||
ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus");
|
||||
return web::ng::Response{std::move(maybeResponse).value(), request};
|
||||
return web::Response{std::move(maybeResponse).value(), request};
|
||||
}
|
||||
|
||||
web::ng::Response
|
||||
web::Response
|
||||
HealthCheckHandler::operator()(
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata&,
|
||||
web::Request const& request,
|
||||
web::ConnectionMetadata&,
|
||||
web::SubscriptionContextPtr,
|
||||
boost::asio::yield_context
|
||||
)
|
||||
@@ -105,7 +105,7 @@ HealthCheckHandler::operator()(
|
||||
</html>
|
||||
)html";
|
||||
|
||||
return web::ng::Response{boost::beast::http::status::ok, kHEALTH_CHECK_HTML, request};
|
||||
return web::Response{boost::beast::http::status::ok, kHEALTH_CHECK_HTML, request};
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
@@ -60,8 +60,8 @@ public:
|
||||
* @param connection The connection to check.
|
||||
* @return A response if the connection is not allowed to proceed or void otherwise.
|
||||
*/
|
||||
std::expected<void, web::ng::Response>
|
||||
operator()(web::ng::Connection const& connection);
|
||||
std::expected<void, web::Response>
|
||||
operator()(web::Connection const& connection);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -84,7 +84,7 @@ public:
|
||||
* @param connection The connection which has disconnected.
|
||||
*/
|
||||
void
|
||||
operator()(web::ng::Connection const& connection);
|
||||
operator()(web::Connection const& connection);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -108,10 +108,10 @@ public:
|
||||
* @param connectionMetadata The connection metadata.
|
||||
* @return The response to the request.
|
||||
*/
|
||||
web::ng::Response
|
||||
web::Response
|
||||
operator()(
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata& connectionMetadata,
|
||||
web::Request const& request,
|
||||
web::ConnectionMetadata& connectionMetadata,
|
||||
web::SubscriptionContextPtr,
|
||||
boost::asio::yield_context
|
||||
);
|
||||
@@ -128,10 +128,10 @@ public:
|
||||
* @param request The request to handle.
|
||||
* @return The response to the request
|
||||
*/
|
||||
web::ng::Response
|
||||
web::Response
|
||||
operator()(
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata&,
|
||||
web::Request const& request,
|
||||
web::ConnectionMetadata&,
|
||||
web::SubscriptionContextPtr,
|
||||
boost::asio::yield_context
|
||||
);
|
||||
@@ -169,10 +169,10 @@ public:
|
||||
* @param yield The yield context.
|
||||
* @return The response to the request.
|
||||
*/
|
||||
web::ng::Response
|
||||
web::Response
|
||||
operator()(
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata& connectionMetadata,
|
||||
web::Request const& request,
|
||||
web::ConnectionMetadata& connectionMetadata,
|
||||
web::SubscriptionContextPtr subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
@@ -188,7 +188,7 @@ public:
|
||||
try {
|
||||
return rpcHandler_(request, connectionMetadata, std::move(subscriptionContext), yield);
|
||||
} catch (std::exception const&) {
|
||||
return web::ng::Response{
|
||||
return web::Response{
|
||||
boost::beast::http::status::internal_server_error,
|
||||
rpc::makeError(rpc::RippledError::rpcINTERNAL),
|
||||
request
|
||||
|
||||
@@ -57,7 +57,7 @@ try {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
app::ClioApplication clio{gClioConfig};
|
||||
return clio.run(run.useNgWebServer);
|
||||
return clio.run();
|
||||
},
|
||||
[](app::CliArgs::Action::Migrate const& migrate) {
|
||||
if (not app::parseConfig(migrate.configPath))
|
||||
|
||||
@@ -338,7 +338,6 @@ static ClioConfigDefinition gClioConfig = ClioConfigDefinition{
|
||||
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateUint16)},
|
||||
{"server.ws_max_sending_queue_size",
|
||||
ConfigValue{ConfigType::Integer}.defaultValue(1500).withConstraint(gValidateUint32)},
|
||||
{"server.__ng_web_server", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
|
||||
|
||||
{"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
|
||||
{"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
|
||||
|
||||
@@ -7,14 +7,14 @@ target_sources(
|
||||
dosguard/IntervalSweepHandler.cpp
|
||||
dosguard/Weights.cpp
|
||||
dosguard/WhitelistHandler.cpp
|
||||
ng/Connection.cpp
|
||||
ng/impl/ErrorHandling.cpp
|
||||
ng/impl/ConnectionHandler.cpp
|
||||
ng/impl/ServerSslContext.cpp
|
||||
ng/Request.cpp
|
||||
ng/Response.cpp
|
||||
ng/Server.cpp
|
||||
ng/SubscriptionContext.cpp
|
||||
Connection.cpp
|
||||
impl/ErrorHandling.cpp
|
||||
impl/ConnectionHandler.cpp
|
||||
impl/ServerSslContext.cpp
|
||||
Request.cpp
|
||||
Response.cpp
|
||||
Server.cpp
|
||||
SubscriptionContext.cpp
|
||||
Resolver.cpp
|
||||
SubscriptionContext.cpp
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
ConnectionMetadata::ConnectionMetadata(std::string ip, util::TagDecoratorFactory const& tagDecoratorFactory)
|
||||
: util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}
|
||||
@@ -54,4 +54,4 @@ Connection::Connection(
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -20,9 +20,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
@@ -35,7 +35,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief An interface for a connection metadata class.
|
||||
@@ -159,4 +159,4 @@ public:
|
||||
*/
|
||||
using ConnectionPtr = std::unique_ptr<Connection>;
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Error of any async operation.
|
||||
*/
|
||||
using Error = boost::system::error_code;
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -1,144 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "util/Taggable.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/PlainWsSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/HttpBase.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
/**
|
||||
* @brief Represents a HTTP connection established by a client.
|
||||
*
|
||||
* It will handle the upgrade to websocket, pass the ownership of the socket to the upgrade session.
|
||||
* Otherwise, it will pass control to the base class.
|
||||
*
|
||||
* @tparam HandlerType The type of the server handler to use
|
||||
*/
|
||||
template <SomeServerHandler HandlerType>
|
||||
class HttpSession : public impl::HttpBase<HttpSession, HandlerType>,
|
||||
public std::enable_shared_from_this<HttpSession<HandlerType>> {
|
||||
boost::beast::tcp_stream stream_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new session.
|
||||
*
|
||||
* @param socket The socket. Ownership is transferred to HttpSession
|
||||
* @param ip Client's IP address
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit HttpSession(
|
||||
tcp::socket&& socket,
|
||||
std::string const& ip,
|
||||
std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer buffer,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: impl::HttpBase<HttpSession, HandlerType>(
|
||||
ip,
|
||||
tagFactory,
|
||||
adminVerification,
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer)
|
||||
)
|
||||
, stream_(std::move(socket))
|
||||
, tagFactory_(tagFactory)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
~HttpSession() override = default;
|
||||
|
||||
/** @return The TCP stream */
|
||||
boost::beast::tcp_stream&
|
||||
stream()
|
||||
{
|
||||
return stream_;
|
||||
}
|
||||
|
||||
/** @brief Starts reading from the stream. */
|
||||
void
|
||||
run()
|
||||
{
|
||||
boost::asio::dispatch(
|
||||
stream_.get_executor(),
|
||||
boost::beast::bind_front_handler(
|
||||
&impl::HttpBase<HttpSession, HandlerType>::doRead, this->shared_from_this()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @brief Closes the underlying socket. */
|
||||
void
|
||||
doClose()
|
||||
{
|
||||
boost::beast::error_code ec;
|
||||
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
|
||||
}
|
||||
|
||||
/** @brief Upgrade to WebSocket connection. */
|
||||
void
|
||||
upgrade()
|
||||
{
|
||||
std::make_shared<WsUpgrader<HandlerType>>(
|
||||
std::move(stream_),
|
||||
this->clientIp,
|
||||
tagFactory_,
|
||||
this->dosGuard_,
|
||||
this->handler_,
|
||||
std::move(this->buffer_),
|
||||
std::move(this->req_),
|
||||
ConnectionBase::isAdmin(),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
};
|
||||
} // namespace web
|
||||
@@ -19,16 +19,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Handler for messages.
|
||||
@@ -36,4 +36,4 @@ namespace web::ng {
|
||||
using MessageHandler =
|
||||
std::function<Response(Request const&, ConnectionMetadata&, SubscriptionContextPtr, boost::asio::yield_context)>;
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -1,204 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "util/Taggable.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/WsBase.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/parser.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Represents a non-secure websocket session.
|
||||
*
|
||||
* Majority of the operations are handled by the base class.
|
||||
*/
|
||||
template <SomeServerHandler HandlerType>
|
||||
class PlainWsSession : public impl::WsBase<PlainWsSession, HandlerType> {
|
||||
using StreamType = boost::beast::websocket::stream<boost::beast::tcp_stream>;
|
||||
StreamType ws_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new non-secure websocket session.
|
||||
*
|
||||
* @param socket The socket. Ownership is transferred
|
||||
* @param ip Client's IP address
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param isAdmin Whether the connection has admin privileges,
|
||||
* @param maxSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit PlainWsSession(
|
||||
boost::asio::ip::tcp::socket&& socket,
|
||||
std::string ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
bool isAdmin,
|
||||
std::uint32_t maxSendingQueueSize
|
||||
)
|
||||
: impl::WsBase<PlainWsSession, HandlerType>(
|
||||
ip,
|
||||
tagFactory,
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer),
|
||||
maxSendingQueueSize
|
||||
)
|
||||
, ws_(std::move(socket))
|
||||
{
|
||||
ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer)
|
||||
}
|
||||
|
||||
~PlainWsSession() override = default;
|
||||
|
||||
/** @return The websocket stream. */
|
||||
StreamType&
|
||||
ws()
|
||||
{
|
||||
return ws_;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The websocket upgrader class, upgrade from an HTTP session to a non-secure websocket session.
|
||||
*
|
||||
* Pass the socket to the session class after upgrade.
|
||||
*/
|
||||
template <SomeServerHandler HandlerType>
|
||||
class WsUpgrader : public std::enable_shared_from_this<WsUpgrader<HandlerType>> {
|
||||
using std::enable_shared_from_this<WsUpgrader<HandlerType>>::shared_from_this;
|
||||
|
||||
boost::beast::tcp_stream http_;
|
||||
boost::optional<http::request_parser<http::string_body>> parser_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
|
||||
http::request<http::string_body> req_;
|
||||
std::string ip_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
bool isAdmin_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new upgrader to non-secure websocket.
|
||||
*
|
||||
* @param stream The TCP stream. Ownership is transferred
|
||||
* @param ip Client's IP address
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer. Ownership is transferred
|
||||
* @param request The request. Ownership is transferred
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
WsUpgrader(
|
||||
boost::beast::tcp_stream&& stream,
|
||||
std::string ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
http::request<http::string_body> request,
|
||||
bool isAdmin,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: http_(std::move(stream))
|
||||
, buffer_(std::move(buffer))
|
||||
, tagFactory_(tagFactory)
|
||||
, dosGuard_(dosGuard)
|
||||
, req_(std::move(request))
|
||||
, ip_(std::move(ip))
|
||||
, handler_(handler)
|
||||
, isAdmin_(isAdmin)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Initiate the upgrade. */
|
||||
void
|
||||
run()
|
||||
{
|
||||
boost::asio::dispatch(
|
||||
http_.get_executor(),
|
||||
boost::beast::bind_front_handler(&WsUpgrader<HandlerType>::doUpgrade, shared_from_this())
|
||||
);
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
doUpgrade()
|
||||
{
|
||||
parser_.emplace();
|
||||
|
||||
static constexpr auto kMAX_BODY_SIZE = 10000;
|
||||
parser_->body_limit(kMAX_BODY_SIZE);
|
||||
|
||||
boost::beast::get_lowest_layer(http_).expires_after(std::chrono::seconds(30));
|
||||
onUpgrade();
|
||||
}
|
||||
|
||||
void
|
||||
onUpgrade()
|
||||
{
|
||||
if (!boost::beast::websocket::is_upgrade(req_))
|
||||
return;
|
||||
|
||||
// Disable the timeout. The websocket::stream uses its own timeout settings.
|
||||
boost::beast::get_lowest_layer(http_).expires_never();
|
||||
|
||||
std::make_shared<PlainWsSession<HandlerType>>(
|
||||
http_.release_socket(),
|
||||
ip_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
isAdmin_,
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run(std::move(req_));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace web
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Requests processing policy.
|
||||
*/
|
||||
enum class ProcessingPolicy { Sequential, Parallel };
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -1,15 +1,67 @@
|
||||
# Web server subsystem
|
||||
# Web Server Subsystem
|
||||
|
||||
This folder contains all of the classes for running the web server.
|
||||
This folder contains all of the classes for running Clio's web server.
|
||||
|
||||
## Overview
|
||||
|
||||
The web server subsystem:
|
||||
|
||||
- Handles JSON-RPC and websocket requests.
|
||||
- Handles JSON-RPC requests over HTTP and WebSocket connections
|
||||
- Supports SSL/TLS encryption if certificate and key files are specified in the config
|
||||
- Processes all types of requests on a single port
|
||||
- Implements asynchronous request handling using [Boost Asio](https://www.boost.org/doc/libs/1_83_0/doc/html/boost_asio.html)
|
||||
- Provides request rate limiting through a built-in Denial-of-Service (DoS) Guard mechanism
|
||||
- Supports both sequential and parallel request processing policies
|
||||
|
||||
- Supports SSL if a cert and key file are specified in the config.
|
||||
## Key Components
|
||||
|
||||
- Handles all types of requests on a single port.
|
||||
### Core Components
|
||||
|
||||
Each request is handled asynchronously using [Boost Asio](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio.html).
|
||||
- **Server** (`Server.hpp/cpp`): The main web server class that manages connections and routes requests
|
||||
- **Connection** (`Connection.hpp/cpp`): Represents a client connection and provides an abstraction layer over HTTP and WebSocket connections
|
||||
- **Request/Response** (`Request.hpp/cpp`, `Response.hpp/cpp`): Classes for handling HTTP requests and responses
|
||||
- **MessageHandler** (`MessageHandler.hpp`): An interface for handling different types of messages (e.g., HTTP GET/POST, WebSocket)
|
||||
- **RPCServerHandler** (`RPCServerHandler.hpp`): Handles RPC requests and integrates with the RPC engine
|
||||
|
||||
Much of this code was originally copied from Boost beast example code.
|
||||
### Connection Processing
|
||||
|
||||
- **ConnectionHandler** (`impl/ConnectionHandler.hpp/cpp`): Manages the lifecycle of connections and processes requests
|
||||
- **ProcessingPolicy** (`ProcessingPolicy.hpp`): Defines whether requests are processed sequentially or in parallel
|
||||
- **HttpConnection/WsConnection** (`impl/HttpConnection.hpp`, `impl/WsConnection.hpp`): Concrete implementations for HTTP and WebSocket connections
|
||||
|
||||
### Security Features
|
||||
|
||||
- **DOSGuard** (`dosguard/DOSGuard.hpp/cpp`): Denial-of-Service protection system that implements rate limiting
|
||||
- **IntervalSweepHandler** (`dosguard/IntervalSweepHandler.hpp/cpp`): Periodically clears DoS guard state
|
||||
- **WhitelistHandler** (`dosguard/WhitelistHandler.hpp/cpp`): Manages IP address whitelisting for bypass of rate limits
|
||||
- **AdminVerificationStrategy** (`AdminVerificationStrategy.hpp/cpp`): Handles verification of admin privileges
|
||||
|
||||
### Subscription
|
||||
|
||||
- **SubscriptionContext** (`SubscriptionContext.hpp/cpp`): Manages WebSocket subscriptions for streaming updates
|
||||
|
||||
## Architecture
|
||||
|
||||
The server design uses the following patterns:
|
||||
|
||||
- **RAII**: Resource management through C++ RAII principles
|
||||
- **Dependency Injection**: Components accept their dependencies through constructor parameters
|
||||
- **Interface-based design**: Components depend on interfaces rather than concrete implementations
|
||||
- **Asynchronous programming**: Uses Boost Asio for non-blocking I/O operations with coroutines
|
||||
|
||||
Each incoming request is handled asynchronously, with the processing being dispatched to appropriate handlers based on the request type (GET, POST, WebSocket). The server supports both secure (SSL/TLS) and non-secure connections based on configuration.
|
||||
|
||||
## SSL Support
|
||||
|
||||
The server creates an SSL context if certificate and key files are specified in the configuration. When SSL is enabled, all connections are encrypted.
|
||||
|
||||
## Request Flow
|
||||
|
||||
1. Client connects to the server
|
||||
2. Server performs security checks (e.g., DoS Guard, admin verification if needed)
|
||||
3. Server reads the request asynchronously
|
||||
4. Request is routed to appropriate handler based on HTTP method and target
|
||||
5. Handler processes the request and generates a response
|
||||
6. Response is sent back to the client
|
||||
7. For persistent connections, the server returns to step 3
|
||||
8. When the client disconnects or an error occurs, the connection is closed
|
||||
|
||||
@@ -26,17 +26,23 @@
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/impl/APIVersionParser.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Profiler.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/ErrorHandling.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
@@ -48,8 +54,8 @@
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ratio>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -65,9 +71,9 @@ class RPCServerHandler {
|
||||
std::shared_ptr<BackendInterface const> const backend_;
|
||||
std::shared_ptr<RPCEngineType> const rpcEngine_;
|
||||
std::shared_ptr<etlng::ETLServiceInterface const> const etl_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosguard_;
|
||||
util::TagDecoratorFactory const tagFactory_;
|
||||
rpc::impl::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed
|
||||
std::reference_wrapper<web::dosguard::DOSGuardInterface> dosguard_;
|
||||
|
||||
util::Logger log_{"RPC"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
@@ -87,14 +93,14 @@ public:
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<RPCEngineType> const& rpcEngine,
|
||||
std::shared_ptr<etlng::ETLServiceInterface const> const& etl,
|
||||
web::dosguard::DOSGuardInterface& dosguard
|
||||
dosguard::DOSGuardInterface& dosguard
|
||||
)
|
||||
: backend_(backend)
|
||||
, rpcEngine_(rpcEngine)
|
||||
, etl_(etl)
|
||||
, dosguard_(dosguard)
|
||||
, tagFactory_(config)
|
||||
, apiVersionParser_(config.getObject("api_version"))
|
||||
, dosguard_(dosguard)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -102,123 +108,165 @@ public:
|
||||
* @brief The callback when server receives a request.
|
||||
*
|
||||
* @param request The request
|
||||
* @param connection The connection
|
||||
* @param connectionMetadata The connection metadata
|
||||
* @param subscriptionContext The subscription context
|
||||
* @param yield The yield context
|
||||
* @return The response
|
||||
*/
|
||||
void
|
||||
operator()(std::string const& request, std::shared_ptr<web::ConnectionBase> const& connection)
|
||||
[[nodiscard]] Response
|
||||
operator()(
|
||||
Request const& request,
|
||||
ConnectionMetadata const& connectionMetadata,
|
||||
SubscriptionContextPtr subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
if (not dosguard_.get().isOk(connection->clientIp)) {
|
||||
connection->sendSlowDown(request);
|
||||
return;
|
||||
if (not dosguard_.get().isOk(connectionMetadata.ip())) {
|
||||
return makeSlowDownResponse(request, std::nullopt);
|
||||
}
|
||||
|
||||
try {
|
||||
auto req = boost::json::parse(request).as_object();
|
||||
LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue";
|
||||
std::optional<Response> response;
|
||||
util::CoroutineGroup coroutineGroup{yield, 1};
|
||||
auto const onTaskComplete = coroutineGroup.registerForeign(yield);
|
||||
ASSERT(onTaskComplete.has_value(), "Coroutine group can't be full");
|
||||
|
||||
if (not connection->upgraded and shouldReplaceParams(req))
|
||||
req[JS(params)] = boost::json::array({boost::json::object{}});
|
||||
bool const postSuccessful = rpcEngine_->post(
|
||||
[this,
|
||||
&request,
|
||||
&response,
|
||||
&onTaskComplete = onTaskComplete.value(),
|
||||
&connectionMetadata,
|
||||
subscriptionContext = std::move(subscriptionContext)](boost::asio::yield_context innerYield) mutable {
|
||||
try {
|
||||
boost::system::error_code ec;
|
||||
auto parsedRequest = boost::json::parse(request.message(), ec);
|
||||
if (ec.failed() or not parsedRequest.is_object()) {
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
response = impl::ErrorHelper{request}.makeJsonParsingError();
|
||||
if (ec.failed()) {
|
||||
LOG(log_.warn())
|
||||
<< "Error parsing JSON: " << ec.message() << ". For request: " << request.message();
|
||||
} else {
|
||||
LOG(log_.warn()) << "Received not a JSON object. For request: " << request.message();
|
||||
}
|
||||
} else {
|
||||
auto parsedObject = std::move(parsedRequest).as_object();
|
||||
|
||||
if (not dosguard_.get().request(connection->clientIp, req)) {
|
||||
connection->sendSlowDown(request);
|
||||
return;
|
||||
}
|
||||
if (not dosguard_.get().request(connectionMetadata.ip(), parsedObject)) {
|
||||
response = makeSlowDownResponse(request, parsedObject);
|
||||
} else {
|
||||
LOG(perfLog_.debug()) << connectionMetadata.tag() << "Adding to work queue";
|
||||
|
||||
if (!rpcEngine_->post(
|
||||
[this, request = std::move(req), connection](boost::asio::yield_context yield) mutable {
|
||||
handleRequest(yield, std::move(request), connection);
|
||||
},
|
||||
connection->clientIp
|
||||
)) {
|
||||
rpcEngine_->notifyTooBusy();
|
||||
web::impl::ErrorHelper(connection).sendTooBusyError();
|
||||
}
|
||||
} catch (boost::system::system_error const& ex) {
|
||||
// system_error thrown when json parsing failed
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
web::impl::ErrorHelper(connection).sendJsonParsingError();
|
||||
LOG(log_.warn()) << "Error parsing JSON: " << ex.what() << ". For request: " << request;
|
||||
} catch (std::invalid_argument const& ex) {
|
||||
// thrown when json parses something that is not an object at top level
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
LOG(log_.warn()) << "Invalid argument error: " << ex.what() << ". For request: " << request;
|
||||
web::impl::ErrorHelper(connection).sendJsonParsingError();
|
||||
} catch (std::exception const& ex) {
|
||||
LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
|
||||
rpcEngine_->notifyInternalError();
|
||||
throw;
|
||||
if (not connectionMetadata.wasUpgraded() and shouldReplaceParams(parsedObject))
|
||||
parsedObject[JS(params)] = boost::json::array({boost::json::object{}});
|
||||
|
||||
response = handleRequest(
|
||||
innerYield,
|
||||
request,
|
||||
std::move(parsedObject),
|
||||
connectionMetadata,
|
||||
std::move(subscriptionContext)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (std::exception const& ex) {
|
||||
LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
rpcEngine_->notifyInternalError();
|
||||
response = impl::ErrorHelper{request}.makeInternalError();
|
||||
}
|
||||
|
||||
// notify the coroutine group that the foreign task is done
|
||||
onTaskComplete();
|
||||
},
|
||||
connectionMetadata.ip()
|
||||
);
|
||||
|
||||
if (not postSuccessful) {
|
||||
// onTaskComplete must be called to notify coroutineGroup that the foreign task is done
|
||||
onTaskComplete->operator()();
|
||||
rpcEngine_->notifyTooBusy();
|
||||
return impl::ErrorHelper{request}.makeTooBusyError();
|
||||
}
|
||||
|
||||
// Put the coroutine to sleep until the foreign task is done
|
||||
coroutineGroup.asyncWait(yield);
|
||||
ASSERT(response.has_value(), "Woke up coroutine without setting response");
|
||||
|
||||
if (not dosguard_.get().add(connectionMetadata.ip(), response->message().size())) {
|
||||
response->setMessage(makeLoadWarning(*response));
|
||||
}
|
||||
|
||||
return std::move(response).value();
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
Response
|
||||
handleRequest(
|
||||
boost::asio::yield_context yield,
|
||||
Request const& rawRequest,
|
||||
boost::json::object&& request,
|
||||
std::shared_ptr<web::ConnectionBase> const& connection
|
||||
ConnectionMetadata const& connectionMetadata,
|
||||
SubscriptionContextPtr subscriptionContext
|
||||
)
|
||||
{
|
||||
LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
|
||||
LOG(log_.info()) << connectionMetadata.tag() << (connectionMetadata.wasUpgraded() ? "ws" : "http")
|
||||
<< " received request from work queue: " << util::removeSecret(request)
|
||||
<< " ip = " << connection->clientIp;
|
||||
<< " ip = " << connectionMetadata.ip();
|
||||
|
||||
try {
|
||||
auto const range = backend_->fetchLedgerRange();
|
||||
if (!range) {
|
||||
// for error that happened before the handler, we don't attach any warnings
|
||||
rpcEngine_->notifyNotReady();
|
||||
web::impl::ErrorHelper(connection, std::move(request)).sendNotReadyError();
|
||||
|
||||
return;
|
||||
return impl::ErrorHelper{rawRequest, std::move(request)}.makeNotReadyError();
|
||||
}
|
||||
|
||||
auto const context = [&] {
|
||||
if (connection->upgraded) {
|
||||
if (connectionMetadata.wasUpgraded()) {
|
||||
ASSERT(subscriptionContext != nullptr, "Subscription context must exist for a WS connection");
|
||||
return rpc::makeWsContext(
|
||||
yield,
|
||||
request,
|
||||
connection->makeSubscriptionContext(tagFactory_),
|
||||
tagFactory_.with(connection->tag()),
|
||||
std::move(subscriptionContext),
|
||||
tagFactory_.with(connectionMetadata.tag()),
|
||||
*range,
|
||||
connection->clientIp,
|
||||
connectionMetadata.ip(),
|
||||
std::cref(apiVersionParser_),
|
||||
connection->isAdmin()
|
||||
connectionMetadata.isAdmin()
|
||||
);
|
||||
}
|
||||
return rpc::makeHttpContext(
|
||||
yield,
|
||||
request,
|
||||
tagFactory_.with(connection->tag()),
|
||||
tagFactory_.with(connectionMetadata.tag()),
|
||||
*range,
|
||||
connection->clientIp,
|
||||
connectionMetadata.ip(),
|
||||
std::cref(apiVersionParser_),
|
||||
connection->isAdmin()
|
||||
connectionMetadata.isAdmin()
|
||||
);
|
||||
}();
|
||||
|
||||
if (!context) {
|
||||
auto const err = context.error();
|
||||
LOG(perfLog_.warn()) << connection->tag() << "Could not create Web context: " << err;
|
||||
LOG(log_.warn()) << connection->tag() << "Could not create Web context: " << err;
|
||||
LOG(perfLog_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
|
||||
LOG(log_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
|
||||
|
||||
// we count all those as BadSyntax - as the WS path would.
|
||||
// Although over HTTP these will yield a 400 status with a plain text response (for most).
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
web::impl::ErrorHelper(connection, std::move(request)).sendError(err);
|
||||
|
||||
return;
|
||||
return impl::ErrorHelper(rawRequest, std::move(request)).makeError(err);
|
||||
}
|
||||
|
||||
auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
|
||||
|
||||
auto const us = std::chrono::duration<int, std::milli>(timeDiff);
|
||||
auto us = std::chrono::duration<int, std::milli>(timeDiff);
|
||||
rpc::logDuration(request, context->tag(), us);
|
||||
|
||||
boost::json::object response;
|
||||
|
||||
if (!result.response.has_value()) {
|
||||
// note: error statuses are counted/notified in buildResponse itself
|
||||
response = web::impl::ErrorHelper(connection, request).composeError(result.response.error());
|
||||
response = impl::ErrorHelper(rawRequest, request).composeError(result.response.error());
|
||||
auto const responseStr = boost::json::serialize(response);
|
||||
|
||||
LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
|
||||
@@ -237,7 +285,7 @@ private:
|
||||
// if the result is forwarded - just use it as is
|
||||
// if forwarded request has error, for http, error should be in "result"; for ws, error should
|
||||
// be at top
|
||||
if (isForwarded && (json.contains(JS(result)) || connection->upgraded)) {
|
||||
if (isForwarded && (json.contains(JS(result)) || connectionMetadata.wasUpgraded())) {
|
||||
for (auto const& [k, v] : json)
|
||||
response.insert_or_assign(k, v);
|
||||
} else {
|
||||
@@ -249,7 +297,7 @@ private:
|
||||
|
||||
// for ws there is an additional field "status" in the response,
|
||||
// otherwise the "status" is in the "result" field
|
||||
if (connection->upgraded) {
|
||||
if (connectionMetadata.wasUpgraded()) {
|
||||
auto const appendFieldIfExist = [&](auto const& field) {
|
||||
if (request.contains(field) and not request.at(field).is_null())
|
||||
response[field] = request.at(field);
|
||||
@@ -275,20 +323,51 @@ private:
|
||||
warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
|
||||
|
||||
response["warnings"] = warnings;
|
||||
connection->send(boost::json::serialize(response));
|
||||
return Response{boost::beast::http::status::ok, response, rawRequest};
|
||||
} catch (std::exception const& ex) {
|
||||
// note: while we are catching this in buildResponse too, this is here to make sure
|
||||
// that any other code that may throw is outside of buildResponse is also worked around.
|
||||
LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
|
||||
LOG(log_.error()) << connection->tag() << "Caught exception: " << ex.what();
|
||||
LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
LOG(log_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
|
||||
rpcEngine_->notifyInternalError();
|
||||
web::impl::ErrorHelper(connection, std::move(request)).sendInternalError();
|
||||
|
||||
return;
|
||||
return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError();
|
||||
}
|
||||
}
|
||||
|
||||
static Response
|
||||
makeSlowDownResponse(Request const& request, std::optional<boost::json::value> requestJson)
|
||||
{
|
||||
auto error = rpc::makeError(rpc::RippledError::rpcSLOW_DOWN);
|
||||
|
||||
if (not request.isHttp()) {
|
||||
try {
|
||||
if (not requestJson.has_value()) {
|
||||
requestJson = boost::json::parse(request.message());
|
||||
}
|
||||
if (requestJson->is_object() && requestJson->as_object().contains("id"))
|
||||
error["id"] = requestJson->as_object().at("id");
|
||||
error["request"] = request.message();
|
||||
} catch (std::exception const&) {
|
||||
error["request"] = request.message();
|
||||
}
|
||||
}
|
||||
return web::Response{boost::beast::http::status::service_unavailable, error, request};
|
||||
}
|
||||
|
||||
static boost::json::object
|
||||
makeLoadWarning(Response const& response)
|
||||
{
|
||||
auto jsonResponse = boost::json::parse(response.message()).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
|
||||
} else {
|
||||
jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
bool
|
||||
shouldReplaceParams(boost::json::object const& req) const
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/Request.hpp"
|
||||
|
||||
#include "util/OverloadSet.hpp"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -142,4 +142,4 @@ Request::httpRequest() const
|
||||
return std::get<HttpRequest>(data_);
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -29,7 +29,7 @@
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Represents an HTTP or WebSocket request.
|
||||
@@ -150,4 +150,4 @@ private:
|
||||
httpRequest() const;
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -17,13 +17,13 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/Response.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/OverloadSet.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -193,4 +193,4 @@ Response::asWsResponse() const&
|
||||
return boost::asio::buffer(message.data(), message.size());
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/Request.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
@@ -30,7 +30,7 @@
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
class Connection;
|
||||
|
||||
@@ -133,4 +133,4 @@ public:
|
||||
asWsResponse() const&;
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -17,19 +17,19 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/Server.hpp"
|
||||
#include "web/Server.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ObjectView.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/MessageHandler.hpp"
|
||||
#include "web/ProcessingPolicy.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/HttpConnection.hpp"
|
||||
#include "web/impl/ServerSslContext.hpp"
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
@@ -54,7 +54,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
namespace web {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -316,7 +316,7 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield
|
||||
boost::asio::spawn(
|
||||
ctx_.get(),
|
||||
[connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) {
|
||||
web::ng::impl::ConnectionHandler::stopConnection(*connection, yield);
|
||||
web::impl::ConnectionHandler::stopConnection(*connection, yield);
|
||||
}
|
||||
);
|
||||
return;
|
||||
@@ -382,4 +382,4 @@ makeServer(
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
} // namespace web
|
||||
@@ -1,7 +1,7 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
Copyright (c) 2024, 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
|
||||
@@ -20,363 +20,173 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/HttpSession.hpp"
|
||||
#include "web/SslHttpSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/MessageHandler.hpp"
|
||||
#include "web/ProcessingPolicy.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/ConnectionHandler.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/socket_base.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* @brief This namespace implements the web server and related components.
|
||||
*
|
||||
* The web server is leveraging the power of `boost::asio` with it's coroutine support thru `boost::asio::yield_context`
|
||||
* and `boost::asio::spawn`.
|
||||
*
|
||||
* Majority of the code is based on examples that came with boost.
|
||||
*/
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief The Detector class to detect if the connection is a ssl or not.
|
||||
*
|
||||
* If it is an SSL connection, the Detector will pass the ownership of the socket to SslSessionType, otherwise to
|
||||
* PlainSessionType.
|
||||
*
|
||||
* @tparam PlainSessionType The plain session type
|
||||
* @tparam SslSessionType The SSL session type
|
||||
* @tparam HandlerType The executor to handle the requests
|
||||
* @brief A tag class for server to help identify Server in templated code.
|
||||
*/
|
||||
template <
|
||||
template <typename> class PlainSessionType,
|
||||
template <typename> class SslSessionType,
|
||||
SomeServerHandler HandlerType>
|
||||
class Detector : public std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>> {
|
||||
using std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
|
||||
|
||||
util::Logger log_{"WebServer"};
|
||||
boost::beast::tcp_stream stream_;
|
||||
std::optional<std::reference_wrapper<boost::asio::ssl::context>> ctx_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> const dosGuard_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::shared_ptr<AdminVerificationStrategy> const adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new detector.
|
||||
*
|
||||
* @param socket The socket. Ownership is transferred
|
||||
* @param ctx The SSL context if any
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
Detector(
|
||||
tcp::socket&& socket,
|
||||
std::optional<std::reference_wrapper<boost::asio::ssl::context>> ctx,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: stream_(std::move(socket))
|
||||
, ctx_(ctx)
|
||||
, tagFactory_(std::cref(tagFactory))
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(std::move(handler))
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A helper function that is called when any error occurs.
|
||||
*
|
||||
* @param ec The error code
|
||||
* @param message The message to include in the log
|
||||
*/
|
||||
void
|
||||
fail(boost::system::error_code ec, char const* message)
|
||||
{
|
||||
if (ec == boost::asio::ssl::error::stream_truncated)
|
||||
return;
|
||||
|
||||
LOG(log_.info()) << "Detector failed (" << message << "): " << ec.message();
|
||||
}
|
||||
|
||||
/** @brief Initiate the detection. */
|
||||
void
|
||||
run()
|
||||
{
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
|
||||
async_detect_ssl(stream_, buffer_, boost::beast::bind_front_handler(&Detector::onDetect, shared_from_this()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles detection result.
|
||||
*
|
||||
* @param ec The error code
|
||||
* @param result true if SSL is detected; false otherwise
|
||||
*/
|
||||
void
|
||||
onDetect(boost::beast::error_code ec, bool result)
|
||||
{
|
||||
if (ec)
|
||||
return fail(ec, "detect");
|
||||
|
||||
std::string ip;
|
||||
try {
|
||||
ip = stream_.socket().remote_endpoint().address().to_string();
|
||||
} catch (std::exception const&) {
|
||||
return fail(ec, "cannot get remote endpoint");
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (!ctx_)
|
||||
return fail(ec, "SSL is not supported by this server");
|
||||
|
||||
std::make_shared<SslSessionType<HandlerType>>(
|
||||
stream_.release_socket(),
|
||||
ip,
|
||||
adminVerification_,
|
||||
*ctx_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
return;
|
||||
}
|
||||
|
||||
std::make_shared<PlainSessionType<HandlerType>>(
|
||||
stream_.release_socket(),
|
||||
ip,
|
||||
adminVerification_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
struct ServerTag {
|
||||
virtual ~ServerTag() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The WebServer class. It creates server socket and start listening on it.
|
||||
*
|
||||
* Once there is client connection, it will accept it and pass the socket to Detector to detect ssl or plain.
|
||||
*
|
||||
* @tparam PlainSessionType The plain session to handle non-ssl connection.
|
||||
* @tparam SslSessionType The SSL session to handle SSL connection.
|
||||
* @tparam HandlerType The handler to process the request and return response.
|
||||
*/
|
||||
template <
|
||||
template <typename> class PlainSessionType,
|
||||
template <typename> class SslSessionType,
|
||||
SomeServerHandler HandlerType>
|
||||
class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> {
|
||||
using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
|
||||
template <typename T>
|
||||
concept SomeServer = std::derived_from<T, ServerTag>;
|
||||
|
||||
/**
|
||||
* @brief Web server class.
|
||||
*/
|
||||
class Server : public ServerTag {
|
||||
public:
|
||||
/**
|
||||
* @brief Check to perform for each new client connection. The check takes client ip as input and returns a Response
|
||||
* if the check failed. Response will be sent to the client and the connection will be closed.
|
||||
*/
|
||||
using OnConnectCheck = std::function<std::expected<void, Response>(Connection const&)>;
|
||||
|
||||
/**
|
||||
* @brief Hook called when any connection disconnects
|
||||
*/
|
||||
using OnDisconnectHook = impl::ConnectionHandler::OnDisconnectHook;
|
||||
|
||||
private:
|
||||
util::Logger log_{"WebServer"};
|
||||
std::reference_wrapper<boost::asio::io_context> ioc_;
|
||||
std::optional<boost::asio::ssl::context> ctx_;
|
||||
util::TagDecoratorFactory tagFactory_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
|
||||
std::shared_ptr<HandlerType> handler_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
std::reference_wrapper<boost::asio::io_context> ctx_;
|
||||
std::optional<boost::asio::ssl::context> sslContext_;
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory_;
|
||||
|
||||
impl::ConnectionHandler connectionHandler_;
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
|
||||
OnConnectCheck onConnectCheck_;
|
||||
|
||||
bool running_{false};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new instance of the web server.
|
||||
* @brief Construct a new Server object.
|
||||
*
|
||||
* @param ioc The io_context to run the server on
|
||||
* @param ctx The SSL context if any
|
||||
* @param endpoint The endpoint to listen on
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
* @param ctx The boost::asio::io_context to use.
|
||||
* @param endpoint The endpoint to listen on.
|
||||
* @param sslContext The SSL context to use (optional).
|
||||
* @param processingPolicy The requests processing policy (parallel or sequential).
|
||||
* @param parallelRequestLimit The limit of requests for one connection that can be processed in parallel. Only used
|
||||
* if processingPolicy is parallel.
|
||||
* @param tagDecoratorFactory The tag decorator factory.
|
||||
* @param maxSubscriptionSendQueueSize The maximum size of the subscription send queue.
|
||||
* @param onConnectCheck The check to perform on each connection.
|
||||
* @param onDisconnectHook The hook to call on each disconnection.
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ioc,
|
||||
std::optional<boost::asio::ssl::context> ctx,
|
||||
tcp::endpoint endpoint,
|
||||
util::TagDecoratorFactory tagFactory,
|
||||
dosguard::DOSGuardInterface& dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: ioc_(std::ref(ioc))
|
||||
, ctx_(std::move(ctx))
|
||||
, tagFactory_(tagFactory)
|
||||
, dosGuard_(std::ref(dosGuard))
|
||||
, handler_(std::move(handler))
|
||||
, acceptor_(boost::asio::make_strand(ioc))
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
boost::beast::error_code ec;
|
||||
boost::asio::io_context& ctx,
|
||||
boost::asio::ip::tcp::endpoint endpoint,
|
||||
std::optional<boost::asio::ssl::context> sslContext,
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
OnConnectCheck onConnectCheck,
|
||||
OnDisconnectHook onDisconnectHook
|
||||
);
|
||||
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec)
|
||||
return;
|
||||
/**
|
||||
* @brief Copy constructor is deleted. The Server couldn't be copied.
|
||||
*/
|
||||
Server(Server const&) = delete;
|
||||
|
||||
acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec);
|
||||
if (ec)
|
||||
return;
|
||||
/**
|
||||
* @brief Move constructor is deleted because connectionHandler_ contains references to some fields of the Server.
|
||||
*/
|
||||
Server(Server&&) = delete;
|
||||
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
LOG(log_.error()) << "Failed to bind to endpoint: " << endpoint << ". message: " << ec.message();
|
||||
throw std::runtime_error(
|
||||
fmt::format("Failed to bind to endpoint: {}:{}", endpoint.address().to_string(), endpoint.port())
|
||||
);
|
||||
}
|
||||
|
||||
acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
LOG(log_.error()) << "Failed to listen at endpoint: " << endpoint << ". message: " << ec.message();
|
||||
throw std::runtime_error(
|
||||
fmt::format("Failed to listen at endpoint: {}:{}", endpoint.address().to_string(), endpoint.port())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Start accepting incoming connections. */
|
||||
/**
|
||||
* @brief Set handler for GET requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param target The target of the request.
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
run()
|
||||
{
|
||||
doAccept();
|
||||
}
|
||||
onGet(std::string const& target, MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Set handler for POST requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param target The target of the request.
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onPost(std::string const& target, MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Set handler for WebSocket requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onWs(MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Run the server.
|
||||
*
|
||||
* @return std::nullopt if the server started successfully, otherwise an error message.
|
||||
*/
|
||||
std::optional<std::string>
|
||||
run();
|
||||
|
||||
/**
|
||||
* @brief Stop the server. This method will asynchronously sleep unless all the users are disconnected.
|
||||
* @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
|
||||
*
|
||||
* @param yield The coroutine context.
|
||||
*/
|
||||
void
|
||||
stop(boost::asio::yield_context yield);
|
||||
|
||||
private:
|
||||
void
|
||||
doAccept()
|
||||
{
|
||||
acceptor_.async_accept(
|
||||
boost::asio::make_strand(ioc_.get()),
|
||||
boost::beast::bind_front_handler(&Server::onAccept, shared_from_this())
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
onAccept(boost::beast::error_code ec, tcp::socket socket)
|
||||
{
|
||||
if (!ec) {
|
||||
auto ctxRef =
|
||||
ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt;
|
||||
|
||||
std::make_shared<Detector<PlainSessionType, SslSessionType, HandlerType>>(
|
||||
std::move(socket),
|
||||
ctxRef,
|
||||
std::cref(tagFactory_),
|
||||
dosGuard_,
|
||||
handler_,
|
||||
adminVerification_,
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
|
||||
doAccept();
|
||||
}
|
||||
handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
/** @brief The final type of the HttpServer used by Clio. */
|
||||
template <typename HandlerType>
|
||||
using HttpServer = Server<HttpSession, SslHttpSession, HandlerType>;
|
||||
|
||||
/**
|
||||
* @brief A factory function that spawns a ready to use HTTP server.
|
||||
* @brief Create a new Server.
|
||||
*
|
||||
* @tparam HandlerType The type of handler to process the request
|
||||
* @param config The config to create server
|
||||
* @param ioc The server will run under this io_context
|
||||
* @param dosGuard The dos guard to protect the server
|
||||
* @param handler The handler to process the request
|
||||
* @return The server instance
|
||||
* @param config The configuration.
|
||||
* @param onConnectCheck The check to perform on each client connection.
|
||||
* @param onDisconnectHook The hook to call when client disconnects.
|
||||
* @param context The boost::asio::io_context to use.
|
||||
*
|
||||
* @return The Server or an error message.
|
||||
*/
|
||||
template <typename HandlerType>
|
||||
static std::shared_ptr<HttpServer<HandlerType>>
|
||||
makeHttpServer(
|
||||
std::expected<Server, std::string>
|
||||
makeServer(
|
||||
util::config::ClioConfigDefinition const& config,
|
||||
boost::asio::io_context& ioc,
|
||||
dosguard::DOSGuardInterface& dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler
|
||||
)
|
||||
{
|
||||
static util::Logger const log{"WebServer"}; // NOLINT(readability-identifier-naming)
|
||||
|
||||
auto expectedSslContext = ng::impl::makeServerSslContext(config);
|
||||
if (not expectedSslContext) {
|
||||
LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto const serverConfig = config.getObject("server");
|
||||
auto const address = boost::asio::ip::make_address(serverConfig.get<std::string>("ip"));
|
||||
auto const port = serverConfig.get<unsigned short>("port");
|
||||
|
||||
auto expectedAdminVerification = makeAdminVerificationStrategy(config);
|
||||
if (not expectedAdminVerification.has_value()) {
|
||||
LOG(log.error()) << expectedAdminVerification.error();
|
||||
throw std::logic_error{expectedAdminVerification.error()};
|
||||
}
|
||||
|
||||
// If the transactions number is 200 per ledger, A client which subscribes everything will send 400+ feeds for
|
||||
// each ledger. we allow user delay 3 ledgers by default
|
||||
auto const maxWsSendingQueueSize = serverConfig.get<uint32_t>("ws_max_sending_queue_size");
|
||||
|
||||
auto server = std::make_shared<HttpServer<HandlerType>>(
|
||||
ioc,
|
||||
std::move(expectedSslContext).value(),
|
||||
boost::asio::ip::tcp::endpoint{address, port},
|
||||
util::TagDecoratorFactory(config),
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(expectedAdminVerification).value(),
|
||||
maxWsSendingQueueSize
|
||||
);
|
||||
|
||||
server->run();
|
||||
return server;
|
||||
}
|
||||
Server::OnConnectCheck onConnectCheck,
|
||||
Server::OnDisconnectHook onDisconnectHook,
|
||||
boost::asio::io_context& context
|
||||
);
|
||||
|
||||
} // namespace web
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "util/Taggable.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/SslWsSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/HttpBase.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/ssl/ssl_stream.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
/**
|
||||
* @brief Represents a HTTPS connection established by a client.
|
||||
*
|
||||
* It will handle the upgrade to secure websocket, pass the ownership of the socket to the upgrade session.
|
||||
* Otherwise, it will pass control to the base class.
|
||||
*
|
||||
* @tparam HandlerType The type of the server handler to use
|
||||
*/
|
||||
template <SomeServerHandler HandlerType>
|
||||
class SslHttpSession : public impl::HttpBase<SslHttpSession, HandlerType>,
|
||||
public std::enable_shared_from_this<SslHttpSession<HandlerType>> {
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream> stream_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new SSL session.
|
||||
*
|
||||
* @param socket The socket. Ownership is transferred to HttpSession
|
||||
* @param ip Client's IP address
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param ctx The SSL context
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit SslHttpSession(
|
||||
tcp::socket&& socket,
|
||||
std::string const& ip,
|
||||
std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
|
||||
boost::asio::ssl::context& ctx,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer buffer,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: impl::HttpBase<SslHttpSession, HandlerType>(
|
||||
ip,
|
||||
tagFactory,
|
||||
adminVerification,
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer)
|
||||
)
|
||||
, stream_(std::move(socket), ctx)
|
||||
, tagFactory_(tagFactory)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
~SslHttpSession() override = default;
|
||||
|
||||
/** @return The SSL stream. */
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream>&
|
||||
stream()
|
||||
{
|
||||
return stream_;
|
||||
}
|
||||
|
||||
/** @brief Initiates the handshake. */
|
||||
void
|
||||
run()
|
||||
{
|
||||
auto self = this->shared_from_this();
|
||||
boost::asio::dispatch(stream_.get_executor(), [self]() {
|
||||
// Set the timeout.
|
||||
boost::beast::get_lowest_layer(self->stream()).expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Perform the SSL handshake
|
||||
// Note, this is the buffered version of the handshake.
|
||||
self->stream_.async_handshake(
|
||||
boost::asio::ssl::stream_base::server,
|
||||
self->buffer_.data(),
|
||||
boost::beast::bind_front_handler(&SslHttpSession<HandlerType>::onHandshake, self)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles the handshake.
|
||||
*
|
||||
* @param ec Error code if any
|
||||
* @param bytesUsed The total amount of data read from the stream
|
||||
*/
|
||||
void
|
||||
onHandshake(boost::beast::error_code ec, std::size_t bytesUsed)
|
||||
{
|
||||
if (ec)
|
||||
return this->httpFail(ec, "handshake");
|
||||
|
||||
this->buffer_.consume(bytesUsed);
|
||||
this->doRead();
|
||||
}
|
||||
|
||||
/** @brief Closes the underlying connection. */
|
||||
void
|
||||
doClose()
|
||||
{
|
||||
boost::beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
|
||||
stream_.async_shutdown(boost::beast::bind_front_handler(&SslHttpSession::onShutdown, this->shared_from_this()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles a connection shutdown.
|
||||
*
|
||||
* @param ec Error code if any
|
||||
*/
|
||||
void
|
||||
onShutdown(boost::beast::error_code ec)
|
||||
{
|
||||
if (ec)
|
||||
return this->httpFail(ec, "shutdown");
|
||||
// At this point the connection is closed gracefully
|
||||
}
|
||||
|
||||
/** @brief Upgrades connection to secure websocket. */
|
||||
void
|
||||
upgrade()
|
||||
{
|
||||
std::make_shared<SslWsUpgrader<HandlerType>>(
|
||||
std::move(stream_),
|
||||
this->clientIp,
|
||||
tagFactory_,
|
||||
this->dosGuard_,
|
||||
this->handler_,
|
||||
std::move(this->buffer_),
|
||||
std::move(this->req_),
|
||||
ConnectionBase::isAdmin(),
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run();
|
||||
}
|
||||
};
|
||||
} // namespace web
|
||||
@@ -1,208 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "util/Taggable.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/WsBase.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/parser.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/ssl/ssl_stream.hpp>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Represents a secure websocket session.
|
||||
*
|
||||
* Majority of the operations are handled by the base class.
|
||||
*/
|
||||
template <SomeServerHandler HandlerType>
|
||||
class SslWsSession : public impl::WsBase<SslWsSession, HandlerType> {
|
||||
using StreamType = boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>;
|
||||
StreamType ws_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new non-secure websocket session.
|
||||
*
|
||||
* @param stream The SSL stream. Ownership is transferred
|
||||
* @param ip Client's IP address
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
explicit SslWsSession(
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream>&& stream,
|
||||
std::string ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
bool isAdmin,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: impl::WsBase<SslWsSession, HandlerType>(
|
||||
ip,
|
||||
tagFactory,
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(buffer),
|
||||
maxWsSendingQueueSize
|
||||
)
|
||||
, ws_(std::move(stream))
|
||||
{
|
||||
ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer)
|
||||
}
|
||||
|
||||
/** @return The secure websocket stream. */
|
||||
StreamType&
|
||||
ws()
|
||||
{
|
||||
return ws_;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The HTTPS upgrader class, upgrade from an HTTPS session to a secure websocket session.
|
||||
*
|
||||
* Pass the stream to the session class after upgrade.
|
||||
*/
|
||||
template <SomeServerHandler HandlerType>
|
||||
class SslWsUpgrader : public std::enable_shared_from_this<SslWsUpgrader<HandlerType>> {
|
||||
using std::enable_shared_from_this<SslWsUpgrader<HandlerType>>::shared_from_this;
|
||||
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream> https_;
|
||||
boost::optional<http::request_parser<http::string_body>> parser_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::string ip_;
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
http::request<http::string_body> req_;
|
||||
bool isAdmin_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new upgrader to secure websocket.
|
||||
*
|
||||
* @param stream The SSL stream. Ownership is transferred
|
||||
* @param ip Client's IP address
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param buffer Buffer with initial data received from the peer. Ownership is transferred
|
||||
* @param request The request. Ownership is transferred
|
||||
* @param isAdmin Whether the connection has admin privileges
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
SslWsUpgrader(
|
||||
boost::beast::ssl_stream<boost::beast::tcp_stream> stream,
|
||||
std::string ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
http::request<http::string_body> request,
|
||||
bool isAdmin,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: https_(std::move(stream))
|
||||
, buffer_(std::move(buffer))
|
||||
, ip_(std::move(ip))
|
||||
, tagFactory_(tagFactory)
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(std::move(handler))
|
||||
, req_(std::move(request))
|
||||
, isAdmin_(isAdmin)
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
}
|
||||
|
||||
~SslWsUpgrader() = default;
|
||||
|
||||
/** @brief Initiate the upgrade. */
|
||||
void
|
||||
run()
|
||||
{
|
||||
boost::beast::get_lowest_layer(https_).expires_after(std::chrono::seconds(30));
|
||||
|
||||
boost::asio::dispatch(
|
||||
https_.get_executor(),
|
||||
boost::beast::bind_front_handler(&SslWsUpgrader<HandlerType>::doUpgrade, shared_from_this())
|
||||
);
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
doUpgrade()
|
||||
{
|
||||
parser_.emplace();
|
||||
|
||||
// Apply a reasonable limit to the allowed size of the body in bytes to prevent abuse.
|
||||
static constexpr auto kMAX_BODY_SIZE = 10000;
|
||||
parser_->body_limit(kMAX_BODY_SIZE);
|
||||
|
||||
boost::beast::get_lowest_layer(https_).expires_after(std::chrono::seconds(30));
|
||||
onUpgrade();
|
||||
}
|
||||
|
||||
void
|
||||
onUpgrade()
|
||||
{
|
||||
if (!boost::beast::websocket::is_upgrade(req_))
|
||||
return;
|
||||
|
||||
// Disable the timeout. The websocket::stream uses its own timeout settings.
|
||||
boost::beast::get_lowest_layer(https_).expires_never();
|
||||
|
||||
std::make_shared<SslWsSession<HandlerType>>(
|
||||
std::move(https_),
|
||||
ip_,
|
||||
tagFactory_,
|
||||
dosGuard_,
|
||||
handler_,
|
||||
std::move(buffer_),
|
||||
isAdmin_,
|
||||
maxWsSendingQueueSize_
|
||||
)
|
||||
->run(std::move(req_));
|
||||
}
|
||||
};
|
||||
} // namespace web
|
||||
@@ -21,10 +21,14 @@
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -32,22 +36,39 @@ namespace web {
|
||||
|
||||
SubscriptionContext::SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
std::shared_ptr<ConnectionBase> connection
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
)
|
||||
: SubscriptionContextInterface{factory}, connection_{connection}
|
||||
: web::SubscriptionContextInterface(factory)
|
||||
, connection_(connection)
|
||||
, maxSendQueueSize_(maxSendQueueSize)
|
||||
, tasksGroup_(yield)
|
||||
, yield_(yield)
|
||||
, errorHandler_(std::move(errorHandler))
|
||||
{
|
||||
}
|
||||
|
||||
SubscriptionContext::~SubscriptionContext()
|
||||
{
|
||||
onDisconnect_(this);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::send(std::shared_ptr<std::string> message)
|
||||
{
|
||||
if (auto connection = connection_.lock(); connection != nullptr)
|
||||
connection->send(std::move(message));
|
||||
if (disconnected_)
|
||||
return;
|
||||
|
||||
if (maxSendQueueSize_.has_value() and tasksGroup_.size() >= *maxSendQueueSize_) {
|
||||
tasksGroup_.spawn(yield_, [this](boost::asio::yield_context innerYield) {
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
disconnected_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
tasksGroup_.spawn(yield_, [this, message = std::move(message)](boost::asio::yield_context innerYield) {
|
||||
auto const maybeError = connection_.get().sendBuffer(boost::asio::buffer(*message), innerYield);
|
||||
if (maybeError.has_value() and errorHandler_(*maybeError, connection_))
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -59,13 +80,21 @@ SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
|
||||
void
|
||||
SubscriptionContext::setApiSubversion(uint32_t value)
|
||||
{
|
||||
apiSubVersion_ = value;
|
||||
apiSubversion_ = value;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
SubscriptionContext::apiSubversion() const
|
||||
{
|
||||
return apiSubVersion_;
|
||||
return apiSubversion_;
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::disconnect(boost::asio::yield_context yield)
|
||||
{
|
||||
onDisconnect_(this);
|
||||
disconnected_ = true;
|
||||
tasksGroup_.asyncWait(yield);
|
||||
}
|
||||
|
||||
} // namespace web
|
||||
|
||||
@@ -19,32 +19,55 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief A context of a WsBase connection for subscriptions.
|
||||
* @brief Implementation of SubscriptionContextInterface.
|
||||
* @note This class is designed to be used with SubscriptionManager. The class is safe to use from multiple threads.
|
||||
* The method disconnect() must be called before the object is destroyed.
|
||||
*/
|
||||
class SubscriptionContext : public SubscriptionContextInterface {
|
||||
std::weak_ptr<ConnectionBase> connection_;
|
||||
class SubscriptionContext : public web::SubscriptionContextInterface {
|
||||
public:
|
||||
/**
|
||||
* @brief Error handler definition. Error handler returns true if connection should be closed false otherwise.
|
||||
*/
|
||||
using ErrorHandler = std::function<bool(Error const&, Connection const&)>;
|
||||
|
||||
private:
|
||||
std::reference_wrapper<impl::WsConnectionBase> connection_;
|
||||
std::optional<size_t> maxSendQueueSize_;
|
||||
util::CoroutineGroup tasksGroup_;
|
||||
boost::asio::yield_context yield_;
|
||||
ErrorHandler errorHandler_;
|
||||
|
||||
boost::signals2::signal<void(SubscriptionContextInterface*)> onDisconnect_;
|
||||
std::atomic_bool disconnected_{false};
|
||||
|
||||
/**
|
||||
* @brief The API version of the web stream client.
|
||||
* This is used to track the api version of this connection, which mainly is used by subscription. It is different
|
||||
* from the api version in Context, which is only used for the current request.
|
||||
*/
|
||||
std::atomic_uint32_t apiSubVersion_ = 0;
|
||||
std::atomic_uint32_t apiSubversion_ = 0u;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -52,17 +75,21 @@ public:
|
||||
*
|
||||
* @param factory The tag decorator factory to use to init taggable.
|
||||
* @param connection The connection for which the context is created.
|
||||
* @param maxSendQueueSize The maximum size of the send queue. If the queue is full, the connection will be closed.
|
||||
* @param yield The yield context to spawn sending coroutines.
|
||||
* @param errorHandler The error handler.
|
||||
*/
|
||||
SubscriptionContext(util::TagDecoratorFactory const& factory, std::shared_ptr<ConnectionBase> connection);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Subscription Context object
|
||||
*/
|
||||
~SubscriptionContext() override;
|
||||
SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Send message to the client
|
||||
* @note This method will not do anything if the related connection got disconnected.
|
||||
* @note This method does nothing after disconnected() was called.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
@@ -91,6 +118,15 @@ public:
|
||||
*/
|
||||
uint32_t
|
||||
apiSubversion() const override;
|
||||
|
||||
/**
|
||||
* @brief Notify the context that related connection is disconnected and wait for all the task to complete.
|
||||
* @note This method must be called before the object is destroyed.
|
||||
*
|
||||
* @param yield The yield context to wait for all the tasks to complete.
|
||||
*/
|
||||
void
|
||||
disconnect(boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
} // namespace web
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
template <typename T>
|
||||
concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
|
||||
@@ -32,4 +32,4 @@ concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
|
||||
template <typename T>
|
||||
concept IsSslTcpStream = std::is_same_v<std::decay_t<T>, boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -17,20 +17,20 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/impl/ConnectionHandler.hpp"
|
||||
#include "web/impl/ConnectionHandler.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/MessageHandler.hpp"
|
||||
#include "web/ProcessingPolicy.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/SubscriptionContext.hpp"
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
@@ -47,7 +47,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -387,4 +387,4 @@ ConnectionHandler::handleRequest(
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -26,13 +26,13 @@
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "util/prometheus/Label.hpp"
|
||||
#include "util/prometheus/Prometheus.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/MessageHandler.hpp"
|
||||
#include "web/ProcessingPolicy.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/signals2/signal.hpp>
|
||||
@@ -47,7 +47,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
class ConnectionHandler {
|
||||
public:
|
||||
@@ -161,4 +161,4 @@ private:
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -17,13 +17,13 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/impl/ErrorHandling.hpp"
|
||||
#include "web/impl/ErrorHandling.hpp"
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -161,4 +161,4 @@ ErrorHelper::composeError(rpc::RippledError error) const
|
||||
return composeErrorImpl(error, rawRequest_, request_);
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -1,7 +1,7 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, the clio developers.
|
||||
Copyright (c) 2024, 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
|
||||
@@ -20,9 +20,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
@@ -31,11 +30,8 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace web::impl {
|
||||
|
||||
@@ -43,137 +39,76 @@ namespace web::impl {
|
||||
* @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
|
||||
*/
|
||||
class ErrorHelper {
|
||||
std::shared_ptr<web::ConnectionBase> connection_;
|
||||
std::reference_wrapper<Request const> rawRequest_;
|
||||
std::optional<boost::json::object> request_;
|
||||
|
||||
public:
|
||||
ErrorHelper(
|
||||
std::shared_ptr<web::ConnectionBase> const& connection,
|
||||
std::optional<boost::json::object> request = std::nullopt
|
||||
)
|
||||
: connection_{connection}, request_{std::move(request)}
|
||||
{
|
||||
}
|
||||
/**
|
||||
* @brief Construct a new Error Helper object
|
||||
*
|
||||
* @param rawRequest The request that caused the error.
|
||||
* @param request The parsed request that caused the error.
|
||||
*/
|
||||
ErrorHelper(Request const& rawRequest, std::optional<boost::json::object> request = std::nullopt);
|
||||
|
||||
void
|
||||
sendError(rpc::Status const& err) const
|
||||
{
|
||||
if (connection_->upgraded) {
|
||||
connection_->send(boost::json::serialize(composeError(err)));
|
||||
} else {
|
||||
// Note: a collection of crutches to match rippled output follows
|
||||
if (auto const clioCode = std::get_if<rpc::ClioError>(&err.code)) {
|
||||
switch (*clioCode) {
|
||||
case rpc::ClioError::RpcInvalidApiVersion:
|
||||
connection_->send(
|
||||
std::string{rpc::getErrorInfo(*clioCode).error}, boost::beast::http::status::bad_request
|
||||
);
|
||||
break;
|
||||
case rpc::ClioError::RpcCommandIsMissing:
|
||||
connection_->send("Null method", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case rpc::ClioError::RpcCommandIsEmpty:
|
||||
connection_->send("method is empty", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case rpc::ClioError::RpcCommandNotString:
|
||||
connection_->send("method is not string", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
case rpc::ClioError::RpcParamsUnparsable:
|
||||
connection_->send("params unparsable", boost::beast::http::status::bad_request);
|
||||
break;
|
||||
/**
|
||||
* @brief Make an error response from a status.
|
||||
*
|
||||
* @param err The status to make an error response from.
|
||||
* @return
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeError(rpc::Status const& err) const;
|
||||
|
||||
// others are not applicable but we want a compilation error next time we add one
|
||||
case rpc::ClioError::RpcUnknownOption:
|
||||
case rpc::ClioError::RpcMalformedCurrency:
|
||||
case rpc::ClioError::RpcMalformedRequest:
|
||||
case rpc::ClioError::RpcMalformedOwner:
|
||||
case rpc::ClioError::RpcMalformedAddress:
|
||||
case rpc::ClioError::RpcFieldNotFoundTransaction:
|
||||
case rpc::ClioError::RpcMalformedOracleDocumentId:
|
||||
case rpc::ClioError::RpcMalformedAuthorizedCredentials:
|
||||
case rpc::ClioError::EtlConnectionError:
|
||||
case rpc::ClioError::EtlRequestError:
|
||||
case rpc::ClioError::EtlRequestTimeout:
|
||||
case rpc::ClioError::EtlInvalidResponse:
|
||||
ASSERT(
|
||||
false, "Unknown rpc error code {}", static_cast<int>(*clioCode)
|
||||
); // this should never happen
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
connection_->send(boost::json::serialize(composeError(err)), boost::beast::http::status::bad_request);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Make an internal error response.
|
||||
*
|
||||
* @return A response with an internal error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeInternalError() const;
|
||||
|
||||
void
|
||||
sendInternalError() const
|
||||
{
|
||||
connection_->send(
|
||||
boost::json::serialize(composeError(rpc::RippledError::rpcINTERNAL)),
|
||||
boost::beast::http::status::internal_server_error
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @brief Make a response for when the server is not ready.
|
||||
*
|
||||
* @return A response with a not ready error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeNotReadyError() const;
|
||||
|
||||
void
|
||||
sendNotReadyError() const
|
||||
{
|
||||
connection_->send(
|
||||
boost::json::serialize(composeError(rpc::RippledError::rpcNOT_READY)), boost::beast::http::status::ok
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @brief Make a response for when the server is too busy.
|
||||
*
|
||||
* @return A response with a too busy error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeTooBusyError() const;
|
||||
|
||||
void
|
||||
sendTooBusyError() const
|
||||
{
|
||||
if (connection_->upgraded) {
|
||||
connection_->send(
|
||||
boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)), boost::beast::http::status::ok
|
||||
);
|
||||
} else {
|
||||
connection_->send(
|
||||
boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)),
|
||||
boost::beast::http::status::service_unavailable
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Make a response when json parsing fails.
|
||||
*
|
||||
* @return A response with a json parsing error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeJsonParsingError() const;
|
||||
|
||||
void
|
||||
sendJsonParsingError() const
|
||||
{
|
||||
if (connection_->upgraded) {
|
||||
connection_->send(boost::json::serialize(rpc::makeError(rpc::RippledError::rpcBAD_SYNTAX)));
|
||||
} else {
|
||||
connection_->send(
|
||||
fmt::format("Unable to parse JSON from the request"), boost::beast::http::status::bad_request
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Compose an error into json object from a status.
|
||||
*
|
||||
* @param error The status to compose into a json object.
|
||||
* @return The composed json object.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
composeError(rpc::Status const& error) const;
|
||||
|
||||
boost::json::object
|
||||
composeError(auto const& error) const
|
||||
{
|
||||
auto e = rpc::makeError(error);
|
||||
|
||||
if (request_) {
|
||||
auto const appendFieldIfExist = [&](auto const& field) {
|
||||
if (request_->contains(field) and not request_->at(field).is_null())
|
||||
e[field] = request_->at(field);
|
||||
};
|
||||
|
||||
appendFieldIfExist(JS(id));
|
||||
|
||||
if (connection_->upgraded)
|
||||
appendFieldIfExist(JS(api_version));
|
||||
|
||||
e[JS(request)] = request_.value();
|
||||
}
|
||||
|
||||
if (connection_->upgraded) {
|
||||
return e;
|
||||
}
|
||||
return {{JS(result), e}};
|
||||
}
|
||||
/**
|
||||
* @brief Compose an error into json object from a rippled error.
|
||||
*
|
||||
* @param error The rippled error to compose into a json object.
|
||||
* @return The composed json object.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
composeError(rpc::RippledError error) const;
|
||||
};
|
||||
|
||||
} // namespace web::impl
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Errors.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Http.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::impl {
|
||||
|
||||
static auto constexpr kHEALTH_CHECK_HTML = R"html(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test page for Clio</title></head>
|
||||
<body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
|
||||
</html>
|
||||
)html";
|
||||
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
/**
|
||||
* @brief This is the implementation class for http sessions
|
||||
*
|
||||
* @tparam Derived The derived class
|
||||
* @tparam HandlerType The handler class, will be called when a request is received.
|
||||
*/
|
||||
template <template <typename> typename Derived, SomeServerHandler HandlerType>
|
||||
class HttpBase : public ConnectionBase {
|
||||
Derived<HandlerType>&
|
||||
derived()
|
||||
{
|
||||
return static_cast<Derived<HandlerType>&>(*this);
|
||||
}
|
||||
|
||||
// TODO: this should be rewritten using http::message_generator instead
|
||||
struct SendLambda {
|
||||
HttpBase& self;
|
||||
|
||||
explicit SendLambda(HttpBase& self) : self(self)
|
||||
{
|
||||
}
|
||||
|
||||
template <bool IsRequest, typename Body, typename Fields>
|
||||
void
|
||||
operator()(http::message<IsRequest, Body, Fields>&& msg) const
|
||||
{
|
||||
if (self.dead())
|
||||
return;
|
||||
|
||||
// The lifetime of the message has to extend for the duration of the async operation so we use a shared_ptr
|
||||
// to manage it.
|
||||
auto sp = std::make_shared<http::message<IsRequest, Body, Fields>>(std::move(msg));
|
||||
|
||||
// Store a type-erased version of the shared pointer in the class to keep it alive.
|
||||
self.res_ = sp;
|
||||
|
||||
// Write the response
|
||||
http::async_write(
|
||||
self.derived().stream(),
|
||||
*sp,
|
||||
boost::beast::bind_front_handler(&HttpBase::onWrite, self.derived().shared_from_this(), sp->need_eof())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
std::shared_ptr<void> res_;
|
||||
SendLambda sender_;
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
|
||||
|
||||
protected:
|
||||
boost::beast::flat_buffer buffer_;
|
||||
http::request<http::string_body> req_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
void
|
||||
httpFail(boost::beast::error_code ec, char const* what)
|
||||
{
|
||||
// ssl::error::stream_truncated, also known as an SSL "short read",
|
||||
// indicates the peer closed the connection without performing the
|
||||
// required closing handshake (for example, Google does this to
|
||||
// improve performance). Generally this can be a security issue,
|
||||
// but if your communication protocol is self-terminated (as
|
||||
// it is with both HTTP and WebSocket) then you may simply
|
||||
// ignore the lack of close_notify.
|
||||
//
|
||||
// https://github.com/boostorg/beast/issues/38
|
||||
//
|
||||
// https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
|
||||
//
|
||||
// When a short read would cut off the end of an HTTP message,
|
||||
// Beast returns the error boost::beast::http::error::partial_message.
|
||||
// Therefore, if we see a short read here, it has occurred
|
||||
// after the message has been completed, so it is safe to ignore it.
|
||||
|
||||
if (ec == boost::asio::ssl::error::stream_truncated)
|
||||
return;
|
||||
|
||||
if (!ec_ && ec != boost::asio::error::operation_aborted) {
|
||||
ec_ = ec;
|
||||
LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message();
|
||||
boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
HttpBase(
|
||||
std::string const& ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
boost::beast::flat_buffer buffer
|
||||
)
|
||||
: ConnectionBase(tagFactory, ip)
|
||||
, sender_(*this)
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, buffer_(std::move(buffer))
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(std::move(handler))
|
||||
{
|
||||
LOG(perfLog_.debug()) << tag() << "http session created";
|
||||
dosGuard_.get().increment(ip);
|
||||
}
|
||||
|
||||
~HttpBase() override
|
||||
{
|
||||
LOG(perfLog_.debug()) << tag() << "http session closed";
|
||||
if (not upgraded)
|
||||
dosGuard_.get().decrement(this->clientIp);
|
||||
}
|
||||
|
||||
void
|
||||
doRead()
|
||||
{
|
||||
if (dead())
|
||||
return;
|
||||
|
||||
// Make the request empty before reading, otherwise the operation behavior is undefined.
|
||||
req_ = {};
|
||||
|
||||
// Set the timeout.
|
||||
boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
|
||||
|
||||
http::async_read(
|
||||
derived().stream(),
|
||||
buffer_,
|
||||
req_,
|
||||
boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this())
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytesTransferred)
|
||||
{
|
||||
if (ec == http::error::end_of_stream)
|
||||
return derived().doClose();
|
||||
|
||||
if (ec)
|
||||
return httpFail(ec, "read");
|
||||
|
||||
if (req_.method() == http::verb::get and req_.target() == "/health")
|
||||
return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
|
||||
|
||||
// Update isAdmin property of the connection
|
||||
ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);
|
||||
|
||||
if (boost::beast::websocket::is_upgrade(req_)) {
|
||||
if (dosGuard_.get().isOk(this->clientIp)) {
|
||||
// Disable the timeout. The websocket::stream uses its own timeout settings.
|
||||
boost::beast::get_lowest_layer(derived().stream()).expires_never();
|
||||
|
||||
upgraded = true;
|
||||
return derived().upgrade();
|
||||
}
|
||||
|
||||
return sender_(httpResponse(http::status::too_many_requests, "text/html", "Too many requests"));
|
||||
}
|
||||
|
||||
if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin()); response.has_value())
|
||||
return sender_(std::move(response.value()));
|
||||
|
||||
if (req_.method() != http::verb::post) {
|
||||
return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
|
||||
}
|
||||
|
||||
LOG(log_.info()) << tag() << "Received request from ip = " << clientIp;
|
||||
|
||||
try {
|
||||
(*handler_)(req_.body(), derived().shared_from_this());
|
||||
} catch (std::exception const&) {
|
||||
return sender_(httpResponse(
|
||||
http::status::internal_server_error,
|
||||
"application/json",
|
||||
boost::json::serialize(rpc::makeError(rpc::RippledError::rpcINTERNAL))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
sendSlowDown(std::string const&) override
|
||||
{
|
||||
sender_(httpResponse(
|
||||
http::status::service_unavailable,
|
||||
"text/plain",
|
||||
boost::json::serialize(rpc::makeError(rpc::RippledError::rpcSLOW_DOWN))
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a response to the client
|
||||
* The message length will be added to the DOSGuard, if the limit is reached, a warning will be added to the
|
||||
* response
|
||||
*/
|
||||
void
|
||||
send(std::string&& msg, http::status status = http::status::ok) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp, msg.size())) {
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
|
||||
} else {
|
||||
jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
|
||||
}
|
||||
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(jsonResponse);
|
||||
}
|
||||
sender_(httpResponse(status, "application/json", std::move(msg)));
|
||||
}
|
||||
|
||||
SubscriptionContextPtr
|
||||
makeSubscriptionContext(util::TagDecoratorFactory const&) override
|
||||
{
|
||||
ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
void
|
||||
onWrite(bool close, boost::beast::error_code ec, std::size_t bytesTransferred)
|
||||
{
|
||||
boost::ignore_unused(bytesTransferred);
|
||||
|
||||
if (ec)
|
||||
return httpFail(ec, "write");
|
||||
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
if (close)
|
||||
return derived().doClose();
|
||||
|
||||
res_ = nullptr;
|
||||
doRead();
|
||||
}
|
||||
|
||||
private:
|
||||
http::response<http::string_body>
|
||||
httpResponse(http::status status, std::string contentType, std::string message) const
|
||||
{
|
||||
http::response<http::string_body> res{status, req_.version()};
|
||||
res.set(http::field::server, "clio-server-" + util::build::getClioVersionString());
|
||||
res.set(http::field::content_type, contentType);
|
||||
res.keep_alive(req_.keep_alive());
|
||||
res.body() = std::move(message);
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace web::impl
|
||||
@@ -21,12 +21,12 @@
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/Concepts.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/Concepts.hpp"
|
||||
#include "web/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
@@ -49,7 +49,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
class UpgradableConnection : public Connection {
|
||||
public:
|
||||
@@ -229,4 +229,4 @@ using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
|
||||
|
||||
using SslHttpConnection = HttpConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
#include "web/impl/ServerSslContext.hpp"
|
||||
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -94,4 +94,4 @@ makeServerSslContext(std::string const& certData, std::string const& keyData)
|
||||
return ctx;
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -27,7 +27,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
std::expected<std::optional<boost::asio::ssl::context>, std::string>
|
||||
makeServerSslContext(util::config::ClioConfigDefinition const& config);
|
||||
@@ -35,4 +35,4 @@ makeServerSslContext(util::config::ClioConfigDefinition const& config);
|
||||
std::expected<boost::asio::ssl::context, std::string>
|
||||
makeServerSslContext(std::string const& certData, std::string const& keyData);
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -1,318 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Errors.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/SubscriptionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/role.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
#include <boost/beast/websocket/rfc6455.hpp>
|
||||
#include <boost/beast/websocket/stream_base.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::impl {
|
||||
|
||||
/**
|
||||
* @brief Web socket implementation. This class is the base class of the web socket session, it will handle the read and
|
||||
* write operations.
|
||||
*
|
||||
* The write operation is via a queue, each write operation of this session will be sent in order.
|
||||
* The write operation also supports shared_ptr of string, so the caller can keep the string alive until it is sent.
|
||||
* It is useful when we have multiple sessions sending the same content.
|
||||
*
|
||||
* @tparam Derived The derived class
|
||||
* @tparam HandlerType The handler type, will be called when a request is received.
|
||||
*/
|
||||
template <template <typename> typename Derived, SomeServerHandler HandlerType>
|
||||
class WsBase : public ConnectionBase, public std::enable_shared_from_this<WsBase<Derived, HandlerType>> {
|
||||
using std::enable_shared_from_this<WsBase<Derived, HandlerType>>::shared_from_this;
|
||||
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
|
||||
bool sending_ = false;
|
||||
std::queue<std::shared_ptr<std::string>> messages_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
|
||||
SubscriptionContextPtr subscriptionContext_;
|
||||
std::uint32_t maxSendingQueueSize_;
|
||||
|
||||
protected:
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
void
|
||||
wsFail(boost::beast::error_code ec, char const* what)
|
||||
{
|
||||
// Don't log if the WebSocket stream was gracefully closed at both endpoints
|
||||
if (ec != boost::beast::websocket::error::closed)
|
||||
LOG(log_.error()) << tag() << ": " << what << ": " << ec.message() << ": " << ec.value();
|
||||
|
||||
if (!ec_ && ec != boost::asio::error::operation_aborted) {
|
||||
ec_ = ec;
|
||||
boost::beast::get_lowest_layer(derived().ws()).socket().close(ec);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit WsBase(
|
||||
std::string ip,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
boost::beast::flat_buffer&& buffer,
|
||||
std::uint32_t maxSendingQueueSize
|
||||
)
|
||||
: ConnectionBase(tagFactory, ip)
|
||||
, buffer_(std::move(buffer))
|
||||
, dosGuard_(dosGuard)
|
||||
, handler_(handler)
|
||||
, maxSendingQueueSize_(maxSendingQueueSize)
|
||||
{
|
||||
upgraded = true; // NOLINT (cppcoreguidelines-pro-type-member-init)
|
||||
|
||||
LOG(perfLog_.debug()) << tag() << "session created";
|
||||
}
|
||||
|
||||
~WsBase() override
|
||||
{
|
||||
LOG(perfLog_.debug()) << tag() << "session closed";
|
||||
dosGuard_.get().decrement(clientIp);
|
||||
}
|
||||
|
||||
Derived<HandlerType>&
|
||||
derived()
|
||||
{
|
||||
return static_cast<Derived<HandlerType>&>(*this);
|
||||
}
|
||||
|
||||
void
|
||||
doWrite()
|
||||
{
|
||||
sending_ = true;
|
||||
derived().ws().async_write(
|
||||
boost::asio::buffer(messages_.front()->data(), messages_.front()->size()),
|
||||
boost::beast::bind_front_handler(&WsBase::onWrite, derived().shared_from_this())
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
onWrite(boost::system::error_code ec, std::size_t)
|
||||
{
|
||||
messages_.pop();
|
||||
sending_ = false;
|
||||
if (ec) {
|
||||
wsFail(ec, "Failed to write");
|
||||
} else {
|
||||
maybeSendNext();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
maybeSendNext()
|
||||
{
|
||||
if (ec_ || sending_ || messages_.empty())
|
||||
return;
|
||||
|
||||
doWrite();
|
||||
}
|
||||
|
||||
void
|
||||
sendSlowDown(std::string const& request) override
|
||||
{
|
||||
sendError(rpc::RippledError::rpcSLOW_DOWN, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a message to the client
|
||||
* @param msg The message to send, it will keep the string alive until it is sent. It is useful when we have
|
||||
* multiple session sending the same content.
|
||||
* Be aware that the message length will not be added to the DOSGuard from this function.
|
||||
*/
|
||||
void
|
||||
send(std::shared_ptr<std::string> msg) override
|
||||
{
|
||||
// Note: post used instead of dispatch to guarantee async behavior of wsFail and maybeSendNext
|
||||
boost::asio::post(
|
||||
derived().ws().get_executor(),
|
||||
[this, self = derived().shared_from_this(), msg = std::move(msg)]() {
|
||||
if (messages_.size() > maxSendingQueueSize_) {
|
||||
wsFail(boost::asio::error::timed_out, "Client is too slow");
|
||||
return;
|
||||
}
|
||||
|
||||
messages_.push(msg);
|
||||
maybeSendNext();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the subscription context for this connection.
|
||||
*
|
||||
* @param factory Tag TagDecoratorFactory to use to create the context.
|
||||
* @return The subscription context for this connection.
|
||||
*/
|
||||
SubscriptionContextPtr
|
||||
makeSubscriptionContext(util::TagDecoratorFactory const& factory) override
|
||||
{
|
||||
if (subscriptionContext_ == nullptr) {
|
||||
subscriptionContext_ = std::make_shared<SubscriptionContext>(factory, shared_from_this());
|
||||
}
|
||||
return subscriptionContext_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a message to the client
|
||||
* @param msg The message to send
|
||||
* Send this message to the client. The message length will be added to the DOSGuard
|
||||
* If the DOSGuard is triggered, the message will be modified to include a warning
|
||||
*/
|
||||
void
|
||||
send(std::string&& msg, http::status) override
|
||||
{
|
||||
if (!dosGuard_.get().add(clientIp, msg.size())) {
|
||||
auto jsonResponse = boost::json::parse(msg).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
|
||||
} else {
|
||||
jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
|
||||
}
|
||||
|
||||
// Reserialize when we need to include this warning
|
||||
msg = boost::json::serialize(jsonResponse);
|
||||
}
|
||||
auto sharedMsg = std::make_shared<std::string>(std::move(msg));
|
||||
send(std::move(sharedMsg));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Accept the session asynchronously
|
||||
*/
|
||||
void
|
||||
run(http::request<http::string_body> req)
|
||||
{
|
||||
using namespace boost::beast;
|
||||
|
||||
derived().ws().set_option(websocket::stream_base::timeout::suggested(role_type::server));
|
||||
|
||||
// Set a decorator to change the Server of the handshake
|
||||
derived().ws().set_option(websocket::stream_base::decorator([](websocket::response_type& res) {
|
||||
res.set(http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-server-async");
|
||||
}));
|
||||
|
||||
derived().ws().async_accept(req, bind_front_handler(&WsBase::onAccept, this->shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
onAccept(boost::beast::error_code ec)
|
||||
{
|
||||
if (ec)
|
||||
return wsFail(ec, "accept");
|
||||
|
||||
LOG(perfLog_.info()) << tag() << "accepting new connection";
|
||||
|
||||
doRead();
|
||||
}
|
||||
|
||||
void
|
||||
doRead()
|
||||
{
|
||||
if (dead())
|
||||
return;
|
||||
|
||||
// Note: use entirely new buffer so previously used, potentially large, capacity is deallocated
|
||||
buffer_ = boost::beast::flat_buffer{};
|
||||
|
||||
derived().ws().async_read(buffer_, boost::beast::bind_front_handler(&WsBase::onRead, this->shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
onRead(boost::beast::error_code ec, std::size_t bytesTransferred)
|
||||
{
|
||||
boost::ignore_unused(bytesTransferred);
|
||||
|
||||
if (ec)
|
||||
return wsFail(ec, "read");
|
||||
|
||||
LOG(perfLog_.info()) << tag() << "Received request from ip = " << this->clientIp;
|
||||
|
||||
std::string requestStr{static_cast<char const*>(buffer_.data().data()), buffer_.size()};
|
||||
|
||||
try {
|
||||
(*handler_)(requestStr, shared_from_this());
|
||||
} catch (std::exception const&) {
|
||||
sendError(rpc::RippledError::rpcINTERNAL, std::move(requestStr));
|
||||
}
|
||||
|
||||
doRead();
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
sendError(rpc::RippledError error, std::string requestStr)
|
||||
{
|
||||
auto e = rpc::makeError(error);
|
||||
|
||||
try {
|
||||
auto request = boost::json::parse(requestStr);
|
||||
if (request.is_object() && request.as_object().contains("id"))
|
||||
e["id"] = request.as_object().at("id");
|
||||
e["request"] = std::move(request);
|
||||
} catch (std::exception const&) {
|
||||
e["request"] = requestStr;
|
||||
}
|
||||
|
||||
this->send(std::make_shared<std::string>(boost::json::serialize(e)));
|
||||
}
|
||||
};
|
||||
} // namespace web::impl
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/Concepts.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/Concepts.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
@@ -50,7 +50,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng::impl {
|
||||
namespace web::impl {
|
||||
|
||||
class WsConnectionBase : public Connection {
|
||||
public:
|
||||
@@ -192,4 +192,4 @@ makeWsConnection(
|
||||
return connection;
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
} // namespace web::impl
|
||||
@@ -1,393 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "data/BackendInterface.hpp"
|
||||
#include "etlng/ETLServiceInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/Factories.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/impl/APIVersionParser.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Profiler.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/ErrorHandling.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ratio>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief The server handler for RPC requests called by web server.
|
||||
*
|
||||
* Note: see @ref web::SomeServerHandler concept
|
||||
*/
|
||||
template <typename RPCEngineType>
|
||||
class RPCServerHandler {
|
||||
std::shared_ptr<BackendInterface const> const backend_;
|
||||
std::shared_ptr<RPCEngineType> const rpcEngine_;
|
||||
std::shared_ptr<etlng::ETLServiceInterface const> const etl_;
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosguard_;
|
||||
util::TagDecoratorFactory const tagFactory_;
|
||||
rpc::impl::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed
|
||||
|
||||
util::Logger log_{"RPC"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new server handler.
|
||||
*
|
||||
* @param config Clio config to use
|
||||
* @param backend The backend to use
|
||||
* @param rpcEngine The RPC engine to use
|
||||
* @param etl The ETL to use
|
||||
* @param dosguard The DOS guard service to use for request rate limiting
|
||||
*/
|
||||
RPCServerHandler(
|
||||
util::config::ClioConfigDefinition const& config,
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<RPCEngineType> const& rpcEngine,
|
||||
std::shared_ptr<etlng::ETLServiceInterface const> const& etl,
|
||||
dosguard::DOSGuardInterface& dosguard
|
||||
)
|
||||
: backend_(backend)
|
||||
, rpcEngine_(rpcEngine)
|
||||
, etl_(etl)
|
||||
, dosguard_(dosguard)
|
||||
, tagFactory_(config)
|
||||
, apiVersionParser_(config.getObject("api_version"))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The callback when server receives a request.
|
||||
*
|
||||
* @param request The request
|
||||
* @param connectionMetadata The connection metadata
|
||||
* @param subscriptionContext The subscription context
|
||||
* @param yield The yield context
|
||||
* @return The response
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
operator()(
|
||||
Request const& request,
|
||||
ConnectionMetadata const& connectionMetadata,
|
||||
SubscriptionContextPtr subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
if (not dosguard_.get().isOk(connectionMetadata.ip())) {
|
||||
return makeSlowDownResponse(request, std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<Response> response;
|
||||
util::CoroutineGroup coroutineGroup{yield, 1};
|
||||
auto const onTaskComplete = coroutineGroup.registerForeign(yield);
|
||||
ASSERT(onTaskComplete.has_value(), "Coroutine group can't be full");
|
||||
|
||||
bool const postSuccessful = rpcEngine_->post(
|
||||
[this,
|
||||
&request,
|
||||
&response,
|
||||
&onTaskComplete = onTaskComplete.value(),
|
||||
&connectionMetadata,
|
||||
subscriptionContext = std::move(subscriptionContext)](boost::asio::yield_context innerYield) mutable {
|
||||
try {
|
||||
boost::system::error_code ec;
|
||||
auto parsedRequest = boost::json::parse(request.message(), ec);
|
||||
if (ec.failed() or not parsedRequest.is_object()) {
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
response = impl::ErrorHelper{request}.makeJsonParsingError();
|
||||
if (ec.failed()) {
|
||||
LOG(log_.warn())
|
||||
<< "Error parsing JSON: " << ec.message() << ". For request: " << request.message();
|
||||
} else {
|
||||
LOG(log_.warn()) << "Received not a JSON object. For request: " << request.message();
|
||||
}
|
||||
} else {
|
||||
auto parsedObject = std::move(parsedRequest).as_object();
|
||||
|
||||
if (not dosguard_.get().request(connectionMetadata.ip(), parsedObject)) {
|
||||
response = makeSlowDownResponse(request, parsedObject);
|
||||
} else {
|
||||
LOG(perfLog_.debug()) << connectionMetadata.tag() << "Adding to work queue";
|
||||
|
||||
if (not connectionMetadata.wasUpgraded() and shouldReplaceParams(parsedObject))
|
||||
parsedObject[JS(params)] = boost::json::array({boost::json::object{}});
|
||||
|
||||
response = handleRequest(
|
||||
innerYield,
|
||||
request,
|
||||
std::move(parsedObject),
|
||||
connectionMetadata,
|
||||
std::move(subscriptionContext)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (std::exception const& ex) {
|
||||
LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
rpcEngine_->notifyInternalError();
|
||||
response = impl::ErrorHelper{request}.makeInternalError();
|
||||
}
|
||||
|
||||
// notify the coroutine group that the foreign task is done
|
||||
onTaskComplete();
|
||||
},
|
||||
connectionMetadata.ip()
|
||||
);
|
||||
|
||||
if (not postSuccessful) {
|
||||
// onTaskComplete must be called to notify coroutineGroup that the foreign task is done
|
||||
onTaskComplete->operator()();
|
||||
rpcEngine_->notifyTooBusy();
|
||||
return impl::ErrorHelper{request}.makeTooBusyError();
|
||||
}
|
||||
|
||||
// Put the coroutine to sleep until the foreign task is done
|
||||
coroutineGroup.asyncWait(yield);
|
||||
ASSERT(response.has_value(), "Woke up coroutine without setting response");
|
||||
|
||||
if (not dosguard_.get().add(connectionMetadata.ip(), response->message().size())) {
|
||||
response->setMessage(makeLoadWarning(*response));
|
||||
}
|
||||
|
||||
return std::move(response).value();
|
||||
}
|
||||
|
||||
private:
|
||||
Response
|
||||
handleRequest(
|
||||
boost::asio::yield_context yield,
|
||||
Request const& rawRequest,
|
||||
boost::json::object&& request,
|
||||
ConnectionMetadata const& connectionMetadata,
|
||||
SubscriptionContextPtr subscriptionContext
|
||||
)
|
||||
{
|
||||
LOG(log_.info()) << connectionMetadata.tag() << (connectionMetadata.wasUpgraded() ? "ws" : "http")
|
||||
<< " received request from work queue: " << util::removeSecret(request)
|
||||
<< " ip = " << connectionMetadata.ip();
|
||||
|
||||
try {
|
||||
auto const range = backend_->fetchLedgerRange();
|
||||
if (!range) {
|
||||
// for error that happened before the handler, we don't attach any warnings
|
||||
rpcEngine_->notifyNotReady();
|
||||
return impl::ErrorHelper{rawRequest, std::move(request)}.makeNotReadyError();
|
||||
}
|
||||
|
||||
auto const context = [&] {
|
||||
if (connectionMetadata.wasUpgraded()) {
|
||||
ASSERT(subscriptionContext != nullptr, "Subscription context must exist for a WS connection");
|
||||
return rpc::makeWsContext(
|
||||
yield,
|
||||
request,
|
||||
std::move(subscriptionContext),
|
||||
tagFactory_.with(connectionMetadata.tag()),
|
||||
*range,
|
||||
connectionMetadata.ip(),
|
||||
std::cref(apiVersionParser_),
|
||||
connectionMetadata.isAdmin()
|
||||
);
|
||||
}
|
||||
return rpc::makeHttpContext(
|
||||
yield,
|
||||
request,
|
||||
tagFactory_.with(connectionMetadata.tag()),
|
||||
*range,
|
||||
connectionMetadata.ip(),
|
||||
std::cref(apiVersionParser_),
|
||||
connectionMetadata.isAdmin()
|
||||
);
|
||||
}();
|
||||
|
||||
if (!context) {
|
||||
auto const err = context.error();
|
||||
LOG(perfLog_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
|
||||
LOG(log_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
|
||||
|
||||
// we count all those as BadSyntax - as the WS path would.
|
||||
// Although over HTTP these will yield a 400 status with a plain text response (for most).
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
return impl::ErrorHelper(rawRequest, std::move(request)).makeError(err);
|
||||
}
|
||||
|
||||
auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
|
||||
|
||||
auto us = std::chrono::duration<int, std::milli>(timeDiff);
|
||||
rpc::logDuration(request, context->tag(), us);
|
||||
|
||||
boost::json::object response;
|
||||
|
||||
if (!result.response.has_value()) {
|
||||
// note: error statuses are counted/notified in buildResponse itself
|
||||
response = impl::ErrorHelper(rawRequest, request).composeError(result.response.error());
|
||||
auto const responseStr = boost::json::serialize(response);
|
||||
|
||||
LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
|
||||
LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr;
|
||||
} else {
|
||||
// This can still technically be an error. Clio counts forwarded requests as successful.
|
||||
rpcEngine_->notifyComplete(context->method, us);
|
||||
|
||||
auto& json = result.response.value();
|
||||
auto const isForwarded =
|
||||
json.contains("forwarded") && json.at("forwarded").is_bool() && json.at("forwarded").as_bool();
|
||||
|
||||
if (isForwarded)
|
||||
json.erase("forwarded");
|
||||
|
||||
// if the result is forwarded - just use it as is
|
||||
// if forwarded request has error, for http, error should be in "result"; for ws, error should
|
||||
// be at top
|
||||
if (isForwarded && (json.contains(JS(result)) || connectionMetadata.wasUpgraded())) {
|
||||
for (auto const& [k, v] : json)
|
||||
response.insert_or_assign(k, v);
|
||||
} else {
|
||||
response[JS(result)] = json;
|
||||
}
|
||||
|
||||
if (isForwarded)
|
||||
response["forwarded"] = true;
|
||||
|
||||
// for ws there is an additional field "status" in the response,
|
||||
// otherwise the "status" is in the "result" field
|
||||
if (connectionMetadata.wasUpgraded()) {
|
||||
auto const appendFieldIfExist = [&](auto const& field) {
|
||||
if (request.contains(field) and not request.at(field).is_null())
|
||||
response[field] = request.at(field);
|
||||
};
|
||||
|
||||
appendFieldIfExist(JS(id));
|
||||
appendFieldIfExist(JS(api_version));
|
||||
|
||||
if (!response.contains(JS(error)))
|
||||
response[JS(status)] = JS(success);
|
||||
|
||||
response[JS(type)] = JS(response);
|
||||
} else {
|
||||
if (response.contains(JS(result)) && !response[JS(result)].as_object().contains(JS(error)))
|
||||
response[JS(result)].as_object()[JS(status)] = JS(success);
|
||||
}
|
||||
}
|
||||
|
||||
boost::json::array warnings = std::move(result.warnings);
|
||||
warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcClio));
|
||||
|
||||
if (etl_->lastCloseAgeSeconds() >= 60)
|
||||
warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
|
||||
|
||||
response["warnings"] = warnings;
|
||||
return Response{boost::beast::http::status::ok, response, rawRequest};
|
||||
} catch (std::exception const& ex) {
|
||||
// note: while we are catching this in buildResponse too, this is here to make sure
|
||||
// that any other code that may throw is outside of buildResponse is also worked around.
|
||||
LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
LOG(log_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
|
||||
rpcEngine_->notifyInternalError();
|
||||
return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError();
|
||||
}
|
||||
}
|
||||
|
||||
static Response
|
||||
makeSlowDownResponse(Request const& request, std::optional<boost::json::value> requestJson)
|
||||
{
|
||||
auto error = rpc::makeError(rpc::RippledError::rpcSLOW_DOWN);
|
||||
|
||||
if (not request.isHttp()) {
|
||||
try {
|
||||
if (not requestJson.has_value()) {
|
||||
requestJson = boost::json::parse(request.message());
|
||||
}
|
||||
if (requestJson->is_object() && requestJson->as_object().contains("id"))
|
||||
error["id"] = requestJson->as_object().at("id");
|
||||
error["request"] = request.message();
|
||||
} catch (std::exception const&) {
|
||||
error["request"] = request.message();
|
||||
}
|
||||
}
|
||||
return web::ng::Response{boost::beast::http::status::service_unavailable, error, request};
|
||||
}
|
||||
|
||||
static boost::json::object
|
||||
makeLoadWarning(Response const& response)
|
||||
{
|
||||
auto jsonResponse = boost::json::parse(response.message()).as_object();
|
||||
jsonResponse["warning"] = "load";
|
||||
if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
|
||||
jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
|
||||
} else {
|
||||
jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
bool
|
||||
shouldReplaceParams(boost::json::object const& req) const
|
||||
{
|
||||
auto const hasParams = req.contains(JS(params));
|
||||
auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
|
||||
auto const paramsIsEmptyString =
|
||||
hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
|
||||
auto const paramsIsEmptyObject =
|
||||
hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
|
||||
auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
|
||||
auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
|
||||
auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
|
||||
auto const firstArgIsNull = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
|
||||
auto const firstArgIsEmptyString = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_string() and
|
||||
req.at(JS(params)).as_array().at(0).as_string().empty();
|
||||
|
||||
// Note: all this compatibility dance is to match `rippled` as close as possible
|
||||
return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or arrayIsEmpty or
|
||||
firstArgIsEmptyString or firstArgIsNull;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -1,192 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/ConnectionHandler.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief A tag class for server to help identify Server in templated code.
|
||||
*/
|
||||
struct ServerTag {
|
||||
virtual ~ServerTag() = default;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept SomeServer = std::derived_from<T, ServerTag>;
|
||||
|
||||
/**
|
||||
* @brief Web server class.
|
||||
*/
|
||||
class Server : public ServerTag {
|
||||
public:
|
||||
/**
|
||||
* @brief Check to perform for each new client connection. The check takes client ip as input and returns a Response
|
||||
* if the check failed. Response will be sent to the client and the connection will be closed.
|
||||
*/
|
||||
using OnConnectCheck = std::function<std::expected<void, Response>(Connection const&)>;
|
||||
|
||||
/**
|
||||
* @brief Hook called when any connection disconnects
|
||||
*/
|
||||
using OnDisconnectHook = impl::ConnectionHandler::OnDisconnectHook;
|
||||
|
||||
private:
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
std::reference_wrapper<boost::asio::io_context> ctx_;
|
||||
std::optional<boost::asio::ssl::context> sslContext_;
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory_;
|
||||
|
||||
impl::ConnectionHandler connectionHandler_;
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
|
||||
OnConnectCheck onConnectCheck_;
|
||||
|
||||
bool running_{false};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Server object.
|
||||
*
|
||||
* @param ctx The boost::asio::io_context to use.
|
||||
* @param endpoint The endpoint to listen on.
|
||||
* @param sslContext The SSL context to use (optional).
|
||||
* @param processingPolicy The requests processing policy (parallel or sequential).
|
||||
* @param parallelRequestLimit The limit of requests for one connection that can be processed in parallel. Only used
|
||||
* if processingPolicy is parallel.
|
||||
* @param tagDecoratorFactory The tag decorator factory.
|
||||
* @param maxSubscriptionSendQueueSize The maximum size of the subscription send queue.
|
||||
* @param onConnectCheck The check to perform on each connection.
|
||||
* @param onDisconnectHook The hook to call on each disconnection.
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ctx,
|
||||
boost::asio::ip::tcp::endpoint endpoint,
|
||||
std::optional<boost::asio::ssl::context> sslContext,
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize,
|
||||
OnConnectCheck onConnectCheck,
|
||||
OnDisconnectHook onDisconnectHook
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Copy constructor is deleted. The Server couldn't be copied.
|
||||
*/
|
||||
Server(Server const&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Move constructor is deleted because connectionHandler_ contains references to some fields of the Server.
|
||||
*/
|
||||
Server(Server&&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Set handler for GET requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param target The target of the request.
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onGet(std::string const& target, MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Set handler for POST requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param target The target of the request.
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onPost(std::string const& target, MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Set handler for WebSocket requests.
|
||||
* @note This method can't be called after run() is called.
|
||||
*
|
||||
* @param handler The handler to set.
|
||||
*/
|
||||
void
|
||||
onWs(MessageHandler handler);
|
||||
|
||||
/**
|
||||
* @brief Run the server.
|
||||
*
|
||||
* @return std::nullopt if the server started successfully, otherwise an error message.
|
||||
*/
|
||||
std::optional<std::string>
|
||||
run();
|
||||
|
||||
/**
|
||||
* @brief Stop the server. This method will asynchronously sleep unless all the users are disconnected.
|
||||
* @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
|
||||
*
|
||||
* @param yield The coroutine context.
|
||||
*/
|
||||
void
|
||||
stop(boost::asio::yield_context yield);
|
||||
|
||||
private:
|
||||
void
|
||||
handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Create a new Server.
|
||||
*
|
||||
* @param config The configuration.
|
||||
* @param onConnectCheck The check to perform on each client connection.
|
||||
* @param onDisconnectHook The hook to call when client disconnects.
|
||||
* @param context The boost::asio::io_context to use.
|
||||
*
|
||||
* @return The Server or an error message.
|
||||
*/
|
||||
std::expected<Server, std::string>
|
||||
makeServer(
|
||||
util::config::ClioConfigDefinition const& config,
|
||||
Server::OnConnectCheck onConnectCheck,
|
||||
Server::OnDisconnectHook onDisconnectHook,
|
||||
boost::asio::io_context& context
|
||||
);
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -1,100 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "web/ng/SubscriptionContext.hpp"
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
SubscriptionContext::SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
)
|
||||
: web::SubscriptionContextInterface(factory)
|
||||
, connection_(connection)
|
||||
, maxSendQueueSize_(maxSendQueueSize)
|
||||
, tasksGroup_(yield)
|
||||
, yield_(yield)
|
||||
, errorHandler_(std::move(errorHandler))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::send(std::shared_ptr<std::string> message)
|
||||
{
|
||||
if (disconnected_)
|
||||
return;
|
||||
|
||||
if (maxSendQueueSize_.has_value() and tasksGroup_.size() >= *maxSendQueueSize_) {
|
||||
tasksGroup_.spawn(yield_, [this](boost::asio::yield_context innerYield) {
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
disconnected_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
tasksGroup_.spawn(yield_, [this, message = std::move(message)](boost::asio::yield_context innerYield) {
|
||||
auto const maybeError = connection_.get().sendBuffer(boost::asio::buffer(*message), innerYield);
|
||||
if (maybeError.has_value() and errorHandler_(*maybeError, connection_))
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
|
||||
{
|
||||
onDisconnect_.connect(slot);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::setApiSubversion(uint32_t value)
|
||||
{
|
||||
apiSubversion_ = value;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
SubscriptionContext::apiSubversion() const
|
||||
{
|
||||
return apiSubversion_;
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::disconnect(boost::asio::yield_context yield)
|
||||
{
|
||||
onDisconnect_(this);
|
||||
disconnected_ = true;
|
||||
tasksGroup_.asyncWait(yield);
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -1,132 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Implementation of SubscriptionContextInterface.
|
||||
* @note This class is designed to be used with SubscriptionManager. The class is safe to use from multiple threads.
|
||||
* The method disconnect() must be called before the object is destroyed.
|
||||
*/
|
||||
class SubscriptionContext : public web::SubscriptionContextInterface {
|
||||
public:
|
||||
/**
|
||||
* @brief Error handler definition. Error handler returns true if connection should be closed false otherwise.
|
||||
*/
|
||||
using ErrorHandler = std::function<bool(Error const&, Connection const&)>;
|
||||
|
||||
private:
|
||||
std::reference_wrapper<impl::WsConnectionBase> connection_;
|
||||
std::optional<size_t> maxSendQueueSize_;
|
||||
util::CoroutineGroup tasksGroup_;
|
||||
boost::asio::yield_context yield_;
|
||||
ErrorHandler errorHandler_;
|
||||
|
||||
boost::signals2::signal<void(SubscriptionContextInterface*)> onDisconnect_;
|
||||
std::atomic_bool disconnected_{false};
|
||||
|
||||
/**
|
||||
* @brief The API version of the web stream client.
|
||||
* This is used to track the api version of this connection, which mainly is used by subscription. It is different
|
||||
* from the api version in Context, which is only used for the current request.
|
||||
*/
|
||||
std::atomic_uint32_t apiSubversion_ = 0u;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Subscription Context object
|
||||
*
|
||||
* @param factory The tag decorator factory to use to init taggable.
|
||||
* @param connection The connection for which the context is created.
|
||||
* @param maxSendQueueSize The maximum size of the send queue. If the queue is full, the connection will be closed.
|
||||
* @param yield The yield context to spawn sending coroutines.
|
||||
* @param errorHandler The error handler.
|
||||
*/
|
||||
SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Send message to the client
|
||||
* @note This method does nothing after disconnected() was called.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void
|
||||
send(std::shared_ptr<std::string> message) override;
|
||||
|
||||
/**
|
||||
* @brief Connect a slot to onDisconnect connection signal.
|
||||
*
|
||||
* @param slot The slot to connect.
|
||||
*/
|
||||
void
|
||||
onDisconnect(OnDisconnectSlot const& slot) override;
|
||||
|
||||
/**
|
||||
* @brief Set the API subversion.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
void
|
||||
setApiSubversion(uint32_t value) override;
|
||||
|
||||
/**
|
||||
* @brief Get the API subversion.
|
||||
*
|
||||
* @return The API subversion.
|
||||
*/
|
||||
uint32_t
|
||||
apiSubversion() const override;
|
||||
|
||||
/**
|
||||
* @brief Notify the context that related connection is disconnected and wait for all the task to complete.
|
||||
* @note This method must be called before the object is destroyed.
|
||||
*
|
||||
* @param yield The yield context to wait for all the tasks to complete.
|
||||
*/
|
||||
void
|
||||
disconnect(boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -1,114 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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/Errors.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
/**
|
||||
* @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
|
||||
*/
|
||||
class ErrorHelper {
|
||||
std::reference_wrapper<Request const> rawRequest_;
|
||||
std::optional<boost::json::object> request_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Error Helper object
|
||||
*
|
||||
* @param rawRequest The request that caused the error.
|
||||
* @param request The parsed request that caused the error.
|
||||
*/
|
||||
ErrorHelper(Request const& rawRequest, std::optional<boost::json::object> request = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Make an error response from a status.
|
||||
*
|
||||
* @param err The status to make an error response from.
|
||||
* @return
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeError(rpc::Status const& err) const;
|
||||
|
||||
/**
|
||||
* @brief Make an internal error response.
|
||||
*
|
||||
* @return A response with an internal error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeInternalError() const;
|
||||
|
||||
/**
|
||||
* @brief Make a response for when the server is not ready.
|
||||
*
|
||||
* @return A response with a not ready error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeNotReadyError() const;
|
||||
|
||||
/**
|
||||
* @brief Make a response for when the server is too busy.
|
||||
*
|
||||
* @return A response with a too busy error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeTooBusyError() const;
|
||||
|
||||
/**
|
||||
* @brief Make a response when json parsing fails.
|
||||
*
|
||||
* @return A response with a json parsing error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeJsonParsingError() const;
|
||||
|
||||
/**
|
||||
* @brief Compose an error into json object from a status.
|
||||
*
|
||||
* @param error The status to compose into a json object.
|
||||
* @return The composed json object.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
composeError(rpc::Status const& error) const;
|
||||
|
||||
/**
|
||||
* @brief Compose an error into json object from a rippled error.
|
||||
*
|
||||
* @param error The rippled error to compose into a json object.
|
||||
* @return The composed json object.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
composeError(rpc::RippledError error) const;
|
||||
};
|
||||
|
||||
} // namespace web::ng::impl
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/HttpConnection.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
@@ -33,25 +33,25 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
struct MockConnectionMetadataImpl : web::ng::ConnectionMetadata {
|
||||
using web::ng::ConnectionMetadata::ConnectionMetadata;
|
||||
struct MockConnectionMetadataImpl : web::ConnectionMetadata {
|
||||
using web::ConnectionMetadata::ConnectionMetadata;
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
};
|
||||
|
||||
using MockConnectionMetadata = testing::NiceMock<MockConnectionMetadataImpl>;
|
||||
using StrictMockConnectionMetadata = testing::StrictMock<MockConnectionMetadataImpl>;
|
||||
|
||||
struct MockConnectionImpl : web::ng::Connection {
|
||||
using web::ng::Connection::Connection;
|
||||
struct MockConnectionImpl : web::Connection {
|
||||
using web::Connection::Connection;
|
||||
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
|
||||
MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
|
||||
|
||||
using SendReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
|
||||
using SendReturnType = std::optional<web::Error>;
|
||||
MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
|
||||
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
using ReceiveReturnType = std::expected<web::Request, web::Error>;
|
||||
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
|
||||
@@ -21,11 +21,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/HttpConnection.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
@@ -37,15 +37,15 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
|
||||
struct MockHttpConnectionImpl : web::impl::UpgradableConnection {
|
||||
using UpgradableConnection::UpgradableConnection;
|
||||
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
|
||||
MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
|
||||
|
||||
using SendReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
|
||||
using SendReturnType = std::optional<web::Error>;
|
||||
MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(
|
||||
SendReturnType,
|
||||
@@ -54,15 +54,15 @@ struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
|
||||
(override)
|
||||
);
|
||||
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
using ReceiveReturnType = std::expected<web::Request, web::Error>;
|
||||
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
|
||||
|
||||
using IsUpgradeRequestedReturnType = std::expected<bool, web::ng::Error>;
|
||||
using IsUpgradeRequestedReturnType = std::expected<bool, web::Error>;
|
||||
MOCK_METHOD(IsUpgradeRequestedReturnType, isUpgradeRequested, (boost::asio::yield_context), (override));
|
||||
|
||||
using UpgradeReturnType = std::expected<web::ng::ConnectionPtr, web::ng::Error>;
|
||||
using UpgradeReturnType = std::expected<web::ConnectionPtr, web::Error>;
|
||||
using OptionalSslContext = std::optional<boost::asio::ssl::context>;
|
||||
MOCK_METHOD(
|
||||
UpgradeReturnType,
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
@@ -34,22 +34,22 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
struct MockWsConnectionImpl : web::ng::impl::WsConnectionBase {
|
||||
struct MockWsConnectionImpl : web::impl::WsConnectionBase {
|
||||
using WsConnectionBase::WsConnectionBase;
|
||||
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
|
||||
MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
|
||||
|
||||
using SendReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
|
||||
using SendReturnType = std::optional<web::Error>;
|
||||
MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
|
||||
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
using ReceiveReturnType = std::expected<web::Request, web::Error>;
|
||||
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
|
||||
|
||||
using SendBufferReturnType = std::optional<web::ng::Error>;
|
||||
using SendBufferReturnType = std::optional<web::Error>;
|
||||
MOCK_METHOD(SendBufferReturnType, sendBuffer, (boost::asio::const_buffer, boost::asio::yield_context), (override));
|
||||
};
|
||||
|
||||
@@ -182,17 +182,16 @@ target_sources(
|
||||
web/dosguard/IntervalSweepHandlerTests.cpp
|
||||
web/dosguard/WeightsTests.cpp
|
||||
web/dosguard/WhitelistHandlerTests.cpp
|
||||
web/ResponseTests.cpp
|
||||
web/RequestTests.cpp
|
||||
web/RPCServerHandlerTests.cpp
|
||||
web/ServerTests.cpp
|
||||
web/SubscriptionContextTests.cpp
|
||||
web/impl/ConnectionHandlerTests.cpp
|
||||
web/impl/ErrorHandlingTests.cpp
|
||||
web/ng/ResponseTests.cpp
|
||||
web/ng/RequestTests.cpp
|
||||
web/ng/RPCServerHandlerTests.cpp
|
||||
web/ng/ServerTests.cpp
|
||||
web/ng/SubscriptionContextTests.cpp
|
||||
web/ng/impl/ConnectionHandlerTests.cpp
|
||||
web/ng/impl/ErrorHandlingTests.cpp
|
||||
web/ng/impl/HttpConnectionTests.cpp
|
||||
web/ng/impl/ServerSslContextTests.cpp
|
||||
web/ng/impl/WsConnectionTests.cpp
|
||||
web/impl/HttpConnectionTests.cpp
|
||||
web/impl/ServerSslContextTests.cpp
|
||||
web/impl/WsConnectionTests.cpp
|
||||
web/RPCServerHandlerTests.cpp
|
||||
web/ServerTests.cpp
|
||||
web/SubscriptionContextTests.cpp
|
||||
|
||||
@@ -50,7 +50,6 @@ TEST_F(CliArgsTests, Parse_NoArgs)
|
||||
int const returnCode = 123;
|
||||
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
|
||||
EXPECT_EQ(run.configPath, CliArgs::kDEFAULT_CONFIG_PATH);
|
||||
EXPECT_FALSE(run.useNgWebServer);
|
||||
return returnCode;
|
||||
});
|
||||
EXPECT_EQ(
|
||||
@@ -64,29 +63,6 @@ TEST_F(CliArgsTests, Parse_NoArgs)
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(CliArgsTests, Parse_NgWebServer)
|
||||
{
|
||||
for (auto& argv : {std::array{"clio_server", "-w"}, std::array{"clio_server", "--ng-web-server"}}) {
|
||||
auto const action = CliArgs::parse(argv.size(), const_cast<char const**>(argv.data()));
|
||||
|
||||
int const returnCode = 123;
|
||||
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
|
||||
EXPECT_EQ(run.configPath, CliArgs::kDEFAULT_CONFIG_PATH);
|
||||
EXPECT_TRUE(run.useNgWebServer);
|
||||
return returnCode;
|
||||
});
|
||||
EXPECT_EQ(
|
||||
action.apply(
|
||||
onRunMock.AsStdFunction(),
|
||||
onExitMock.AsStdFunction(),
|
||||
onMigrateMock.AsStdFunction(),
|
||||
onVerifyMock.AsStdFunction()
|
||||
),
|
||||
returnCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CliArgsTests, Parse_VersionHelp)
|
||||
{
|
||||
for (auto& argv :
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/MockSubscriptionManager.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "web/ng/Server.hpp"
|
||||
#include "web/Server.hpp"
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
@@ -62,7 +62,7 @@ TEST_F(StopperTest, stopCalledMultipleTimes)
|
||||
}
|
||||
|
||||
struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
|
||||
struct ServerMock : web::ng::ServerTag {
|
||||
struct ServerMock : web::ServerTag {
|
||||
MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
|
||||
};
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/MockConnection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardMock.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MockConnection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
@@ -108,7 +108,7 @@ struct MetricsHandlerTests : util::prometheus::WithPrometheus, SyncAsioContextTe
|
||||
};
|
||||
|
||||
MetricsHandler metricsHandler{adminVerifier};
|
||||
web::ng::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
|
||||
web::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
|
||||
};
|
||||
|
||||
TEST_F(MetricsHandlerTests, Call)
|
||||
@@ -122,7 +122,7 @@ TEST_F(MetricsHandlerTests, Call)
|
||||
}
|
||||
|
||||
struct HealthCheckHandlerTests : SyncAsioContextTest, WebHandlersTest {
|
||||
web::ng::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
web::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
HealthCheckHandler healthCheckHandler;
|
||||
};
|
||||
|
||||
@@ -142,19 +142,19 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
|
||||
|
||||
struct RpcHandlerMock {
|
||||
MOCK_METHOD(
|
||||
web::ng::Response,
|
||||
web::Response,
|
||||
call,
|
||||
(web::ng::Request const&,
|
||||
web::ng::ConnectionMetadata const&,
|
||||
(web::Request const&,
|
||||
web::ConnectionMetadata const&,
|
||||
web::SubscriptionContextPtr,
|
||||
boost::asio::yield_context),
|
||||
()
|
||||
);
|
||||
|
||||
web::ng::Response
|
||||
web::Response
|
||||
operator()(
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata const& connectionMetadata,
|
||||
web::Request const& request,
|
||||
web::ConnectionMetadata const& connectionMetadata,
|
||||
web::SubscriptionContextPtr subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
@@ -170,7 +170,7 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
|
||||
|
||||
TEST_F(RequestHandlerTest, RpcHandlerThrows)
|
||||
{
|
||||
web::ng::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
|
||||
EXPECT_CALL(*adminVerifier, isAdmin).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(rpcHandler, call).WillOnce(testing::Throw(std::runtime_error{"some error"}));
|
||||
@@ -191,9 +191,9 @@ TEST_F(RequestHandlerTest, RpcHandlerThrows)
|
||||
|
||||
TEST_F(RequestHandlerTest, NoErrors)
|
||||
{
|
||||
web::ng::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
web::ng::Response const response{http::status::ok, "some response", request};
|
||||
auto const httpResponse = web::ng::Response{response}.intoHttpResponse();
|
||||
web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
web::Response const response{http::status::ok, "some response", request};
|
||||
auto const httpResponse = web::Response{response}.intoHttpResponse();
|
||||
|
||||
EXPECT_CALL(*adminVerifier, isAdmin).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(rpcHandler, call).WillOnce(testing::Return(response));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/Request.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
@@ -31,7 +31,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng;
|
||||
using namespace web;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct RequestTest : public ::testing::Test {
|
||||
@@ -23,9 +23,9 @@
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/ng/MockConnection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/MockConnection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
@@ -42,7 +42,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng;
|
||||
using namespace web;
|
||||
namespace http = boost::beast::http;
|
||||
using namespace util::config;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,62 +17,156 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/SubscriptionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBaseMock.hpp"
|
||||
#include "web/impl/MockWsConnection.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using namespace web;
|
||||
using namespace util::config;
|
||||
|
||||
struct SubscriptionContextTests : NoLoggerFixture {
|
||||
struct SubscriptionContextTests : SyncAsioContextTest {
|
||||
SubscriptionContext
|
||||
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
|
||||
{
|
||||
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
|
||||
}
|
||||
|
||||
protected:
|
||||
util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
|
||||
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
|
||||
}};
|
||||
ConnectionBaseStrictMockPtr connection_ =
|
||||
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, "some ip");
|
||||
|
||||
SubscriptionContext subscriptionContext_{tagFactory_, connection_};
|
||||
testing::StrictMock<testing::MockFunction<void(SubscriptionContextInterface*)>> callbackMock_;
|
||||
MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
|
||||
testing::StrictMock<testing::MockFunction<bool(web::Error const&, Connection const&)>> errorHandler_;
|
||||
};
|
||||
|
||||
TEST_F(SubscriptionContextTests, send)
|
||||
TEST_F(SubscriptionContextTests, Send)
|
||||
{
|
||||
auto message = std::make_shared<std::string>("message");
|
||||
EXPECT_CALL(*connection_, send(message));
|
||||
subscriptionContext_.send(message);
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message = std::make_shared<std::string>("some message");
|
||||
|
||||
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return std::nullopt;
|
||||
});
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, sendConnectionExpired)
|
||||
TEST_F(SubscriptionContextTests, SendOrder)
|
||||
{
|
||||
auto message = std::make_shared<std::string>("message");
|
||||
connection_.reset();
|
||||
subscriptionContext_.send(message);
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message1 = std::make_shared<std::string>("message1");
|
||||
auto const message2 = std::make_shared<std::string>("message2");
|
||||
|
||||
testing::Sequence const sequence;
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.InSequence(sequence)
|
||||
.WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.InSequence(sequence)
|
||||
.WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
subscriptionContext.send(message1);
|
||||
subscriptionContext.send(message2);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, onDisconnect)
|
||||
TEST_F(SubscriptionContextTests, SendFailed)
|
||||
{
|
||||
auto localContext = std::make_unique<SubscriptionContext>(tagFactory_, connection_);
|
||||
localContext->onDisconnect(callbackMock_.AsStdFunction());
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message = std::make_shared<std::string>("some message");
|
||||
|
||||
EXPECT_CALL(callbackMock_, Call(localContext.get()));
|
||||
localContext.reset();
|
||||
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return boost::system::errc::make_error_code(boost::system::errc::not_supported);
|
||||
});
|
||||
EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(connection_, close);
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, setApiSubversion)
|
||||
TEST_F(SubscriptionContextTests, SendTooManySubscriptions)
|
||||
{
|
||||
EXPECT_EQ(subscriptionContext_.apiSubversion(), 0);
|
||||
subscriptionContext_.setApiSubversion(42);
|
||||
EXPECT_EQ(subscriptionContext_.apiSubversion(), 42);
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield, 1);
|
||||
auto const message = std::make_shared<std::string>("message1");
|
||||
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
|
||||
boost::asio::post(innerYield); // simulate send is slow by switching to another coroutine
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(connection_, close);
|
||||
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, SendAfterDisconnect)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message = std::make_shared<std::string>("some message");
|
||||
subscriptionContext.disconnect(yield);
|
||||
subscriptionContext.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, OnDisconnect)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
|
||||
EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, SetApiSubversion)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
subscriptionContext.setApiSubversion(42);
|
||||
EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/Connection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/ProcessingPolicy.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/ConnectionHandler.hpp"
|
||||
#include "web/ng/impl/MockHttpConnection.hpp"
|
||||
#include "web/ng/impl/MockWsConnection.hpp"
|
||||
#include "web/impl/ConnectionHandler.hpp"
|
||||
#include "web/impl/MockHttpConnection.hpp"
|
||||
#include "web/impl/MockWsConnection.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
@@ -57,8 +57,8 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng::impl;
|
||||
using namespace web::ng;
|
||||
using namespace web::impl;
|
||||
using namespace web;
|
||||
using namespace util;
|
||||
using testing::Return;
|
||||
namespace beast = boost::beast;
|
||||
@@ -20,101 +20,213 @@
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/impl/ErrorHandling.hpp"
|
||||
#include "web/interface/ConnectionBaseMock.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
using namespace web::impl;
|
||||
using namespace web;
|
||||
using namespace util::config;
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct ErrorHandlingTests : NoLoggerFixture {
|
||||
protected:
|
||||
util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
|
||||
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
|
||||
}};
|
||||
std::string const clientIp_ = "some ip";
|
||||
ConnectionBaseStrictMockPtr connection_ =
|
||||
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, clientIp_);
|
||||
static Request
|
||||
makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
|
||||
{
|
||||
if (isHttp)
|
||||
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
|
||||
static Request::HttpHeaders const kHEADERS;
|
||||
return Request{body.value_or(""), kHEADERS};
|
||||
}
|
||||
};
|
||||
|
||||
struct ErrorHandlingComposeErrorTestBundle {
|
||||
struct ErrorHandlingMakeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool connectionUpgraded;
|
||||
std::optional<boost::json::object> request;
|
||||
bool isHttp;
|
||||
rpc::Status status;
|
||||
std::string expectedMessage;
|
||||
boost::beast::http::status expectedStatus;
|
||||
};
|
||||
|
||||
struct ErrorHandlingMakeErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingMakeErrorTestBundle> {};
|
||||
|
||||
TEST_P(ErrorHandlingMakeErrorTest, MakeError)
|
||||
{
|
||||
auto const request = makeRequest(GetParam().isHttp);
|
||||
ErrorHelper const errorHelper{request};
|
||||
|
||||
auto response = errorHelper.makeError(GetParam().status);
|
||||
EXPECT_EQ(response.message(), GetParam().expectedMessage);
|
||||
if (GetParam().isHttp) {
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
|
||||
|
||||
std::string expectedContentType = "text/html";
|
||||
if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
|
||||
expectedContentType = "application/json";
|
||||
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ErrorHandlingMakeErrorTestGroup,
|
||||
ErrorHandlingMakeErrorTest,
|
||||
testing::ValuesIn({
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"WsRequest",
|
||||
false,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
|
||||
boost::beast::http::status::ok
|
||||
},
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_InvalidApiVersion",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
|
||||
"invalid_API_version",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandIsMissing",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcCommandIsMissing},
|
||||
"Null method",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandIsEmpty",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
|
||||
"method is empty",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandNotString",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcCommandNotString},
|
||||
"method is not string",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_ParamsUnparsable",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcParamsUnparsable},
|
||||
"params unparsable",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_RippledError",
|
||||
true,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
}),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
struct ErrorHandlingMakeInternalErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
std::optional<std::string> request;
|
||||
boost::json::object expectedResult;
|
||||
};
|
||||
|
||||
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
|
||||
struct ErrorHandlingMakeInternalErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingMakeInternalErrorTestBundle> {};
|
||||
|
||||
TEST_P(ErrorHandlingComposeErrorTest, composeError)
|
||||
TEST_P(ErrorHandlingMakeInternalErrorTest, ComposeError)
|
||||
{
|
||||
connection_->upgraded = GetParam().connectionUpgraded;
|
||||
ErrorHelper const errorHelper{connection_, GetParam().request};
|
||||
auto const result = errorHelper.composeError(rpc::RippledError::rpcNOT_READY);
|
||||
EXPECT_EQ(boost::json::serialize(result), boost::json::serialize(GetParam().expectedResult));
|
||||
auto const request = makeRequest(GetParam().isHttp, GetParam().request);
|
||||
std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
|
||||
? std::make_optional(boost::json::parse(*GetParam().request).as_object())
|
||||
: std::nullopt;
|
||||
ErrorHelper const errorHelper{request, requestJson};
|
||||
|
||||
auto response = errorHelper.makeInternalError();
|
||||
|
||||
EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
|
||||
if (GetParam().isHttp) {
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ErrorHandlingComposeErrorTestGroup,
|
||||
ErrorHandlingComposeErrorTest,
|
||||
ErrorHandlingMakeInternalErrorTest,
|
||||
testing::ValuesIn(
|
||||
{ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_UpgradedConnection",
|
||||
true,
|
||||
{ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"NoRequest_WebsocketConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_NotUpgradedConnection",
|
||||
false,
|
||||
ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"NoRequest_HttpConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
{{"result",
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}}}
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_UpgradedConnection",
|
||||
true,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_WebsocketConnection",
|
||||
false,
|
||||
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"api_version", 2},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_NotUpgradedConnection",
|
||||
ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_WebsocketConnection_NoId",
|
||||
false,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
std::string{R"JSON({"api_version": 2})JSON"},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"api_version", 2},
|
||||
{"request", {{"api_version", 2}}}}
|
||||
},
|
||||
ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_HttpConnection",
|
||||
true,
|
||||
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
|
||||
{{"result",
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
@@ -124,169 +236,122 @@ INSTANTIATE_TEST_CASE_P(
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
struct ErrorHandlingSendErrorTestBundle {
|
||||
TEST_F(ErrorHandlingTests, MakeNotReadyError)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeNotReadyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
|
||||
}
|
||||
);
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
|
||||
{
|
||||
auto const request = makeRequest(false);
|
||||
auto response = ErrorHelper{request}.makeTooBusyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendTooBusyError_HttpConnection)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeTooBusyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
|
||||
}
|
||||
);
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
|
||||
{
|
||||
auto const request = makeRequest(false);
|
||||
auto response = ErrorHelper{request}.makeJsonParsingError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, makeJsonParsingError_HttpConnection)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeJsonParsingError();
|
||||
EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
|
||||
}
|
||||
|
||||
struct ErrorHandlingComposeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool connectionUpgraded;
|
||||
rpc::Status status;
|
||||
bool isHttp;
|
||||
std::optional<boost::json::object> request;
|
||||
std::string expectedMessage;
|
||||
boost::beast::http::status expectedStatus;
|
||||
};
|
||||
|
||||
struct ErrorHandlingSendErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingSendErrorTestBundle> {};
|
||||
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
|
||||
|
||||
TEST_P(ErrorHandlingSendErrorTest, sendError)
|
||||
TEST_P(ErrorHandlingComposeErrorTest, ComposeError)
|
||||
{
|
||||
connection_->upgraded = GetParam().connectionUpgraded;
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
|
||||
EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus));
|
||||
errorHelper.sendError(GetParam().status);
|
||||
auto const request = makeRequest(GetParam().isHttp);
|
||||
ErrorHelper const errorHelper{request, GetParam().request};
|
||||
auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
|
||||
EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ErrorHandlingSendErrorTestGroup,
|
||||
ErrorHandlingSendErrorTest,
|
||||
testing::ValuesIn({
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"UpgradedConnection",
|
||||
true,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
|
||||
boost::beast::http::status::ok
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_InvalidApiVersion",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
|
||||
"invalid_API_version",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_CommandIsMissing",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::RpcCommandIsMissing},
|
||||
"Null method",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_CommandIsEmpty",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
|
||||
"method is empty",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_CommandNotString",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::RpcCommandNotString},
|
||||
"method is not string",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_ParamsUnparsable",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::RpcParamsUnparsable},
|
||||
"params unparsable",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_RippledError",
|
||||
false,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
}),
|
||||
ErrorHandlingComposeErrorTestGroup,
|
||||
ErrorHandlingComposeErrorTest,
|
||||
testing::ValuesIn(
|
||||
{ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_WebsocketConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_HttpConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_WebsocketConnection",
|
||||
false,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})JSON",
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_WebsocketConnection_NoId",
|
||||
false,
|
||||
boost::json::object{{"api_version", 2}},
|
||||
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})JSON",
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_HttpConnection",
|
||||
true,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})JSON"
|
||||
}}
|
||||
),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendInternalError)
|
||||
{
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
|
||||
},
|
||||
boost::beast::http::status::internal_server_error
|
||||
)
|
||||
);
|
||||
errorHelper.sendInternalError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendNotReadyError)
|
||||
{
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
|
||||
},
|
||||
boost::beast::http::status::ok
|
||||
)
|
||||
);
|
||||
errorHelper.sendNotReadyError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendTooBusyError_UpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = true;
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
|
||||
},
|
||||
boost::beast::http::status::ok
|
||||
)
|
||||
);
|
||||
errorHelper.sendTooBusyError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendTooBusyError_NotUpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = false;
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
|
||||
},
|
||||
boost::beast::http::status::service_unavailable
|
||||
)
|
||||
);
|
||||
errorHelper.sendTooBusyError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendJsonParsingError_UpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = true;
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
|
||||
},
|
||||
boost::beast::http::status::ok
|
||||
)
|
||||
);
|
||||
errorHelper.sendJsonParsingError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendJsonParsingError_NotUpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = false;
|
||||
ErrorHelper const errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(std::string{"Unable to parse JSON from the request"}, boost::beast::http::status::bad_request)
|
||||
);
|
||||
errorHelper.sendJsonParsingError();
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/HttpConnection.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
@@ -46,8 +46,8 @@
|
||||
#include <ranges>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng::impl;
|
||||
using namespace web::ng;
|
||||
using namespace web::impl;
|
||||
using namespace web;
|
||||
using namespace util::config;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "util/config/ConfigFileJson.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
#include "web/impl/ServerSslContext.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
@@ -33,7 +33,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using namespace web::ng::impl;
|
||||
using namespace web::impl;
|
||||
using namespace util::config;
|
||||
|
||||
struct MakeServerSslContextFromConfigTestBundle {
|
||||
@@ -25,11 +25,11 @@
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
#include "web/Error.hpp"
|
||||
#include "web/Request.hpp"
|
||||
#include "web/Response.hpp"
|
||||
#include "web/impl/HttpConnection.hpp"
|
||||
#include "web/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
@@ -51,8 +51,8 @@
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng::impl;
|
||||
using namespace web::ng;
|
||||
using namespace web::impl;
|
||||
using namespace web;
|
||||
using namespace util;
|
||||
|
||||
struct WebWsConnectionTests : SyncAsioContextTest {
|
||||
@@ -1,611 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "rpc/Errors.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/MockBackendTestFixture.hpp"
|
||||
#include "util/MockETLService.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/MockRPCEngine.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardMock.hpp"
|
||||
#include "web/ng/MockConnection.hpp"
|
||||
#include "web/ng/RPCServerHandler.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng;
|
||||
using testing::Return;
|
||||
using testing::StrictMock;
|
||||
using namespace util::config;
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct NgRpcServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest {
|
||||
ClioConfigDefinition config{ClioConfigDefinition{
|
||||
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
|
||||
{"api_version.min", ConfigValue{ConfigType::Integer}.defaultValue(1)},
|
||||
{"api_version.max", ConfigValue{ConfigType::Integer}.defaultValue(2)},
|
||||
{"api_version.default", ConfigValue{ConfigType::Integer}.defaultValue(1)}
|
||||
}};
|
||||
|
||||
protected:
|
||||
std::shared_ptr<testing::StrictMock<MockRPCEngine>> rpcEngine_ =
|
||||
std::make_shared<testing::StrictMock<MockRPCEngine>>();
|
||||
std::shared_ptr<StrictMock<MockETLService>> etl_ = std::make_shared<StrictMock<MockETLService>>();
|
||||
DOSGuardStrictMock dosguard_;
|
||||
RPCServerHandler<MockRPCEngine> rpcServerHandler_{config, backend_, rpcEngine_, etl_, dosguard_};
|
||||
|
||||
util::TagDecoratorFactory tagFactory_{config};
|
||||
std::string const ip_ = "some ip";
|
||||
StrictMockConnectionMetadata connectionMetadata_{ip_, tagFactory_};
|
||||
Request::HttpHeaders const httpHeaders_;
|
||||
|
||||
static Request
|
||||
makeHttpRequest(std::string_view body)
|
||||
{
|
||||
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body}};
|
||||
}
|
||||
|
||||
Request
|
||||
makeWsRequest(std::string body)
|
||||
{
|
||||
return Request{std::move(body), httpHeaders_};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedHttpRequest)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("some message");
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const responseHttp = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
|
||||
|
||||
auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
|
||||
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsRequest)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const requestStr = "some message";
|
||||
auto const request = makeWsRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse());
|
||||
|
||||
auto const responseJson = boost::json::parse(responseWs).as_object();
|
||||
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
|
||||
EXPECT_EQ(responseJson.at("request").as_string(), requestStr);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsJsonRequest)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const requestStr = R"JSON({"request": "some message", "id": "some id"})JSON";
|
||||
auto const request = makeWsRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse());
|
||||
|
||||
auto const responseJson = boost::json::parse(responseWs).as_object();
|
||||
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
|
||||
EXPECT_EQ(responseJson.at("request").as_string(), requestStr);
|
||||
EXPECT_EQ(responseJson.at("id").as_string(), "some id");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, PostToRpcEngineFailed)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("some message");
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce(Return(false));
|
||||
EXPECT_CALL(*rpcEngine_, notifyTooBusy());
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::service_unavailable);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, CoroutineSleepsUntilRpcEngineFinishes)
|
||||
{
|
||||
StrictMock<testing::MockFunction<void()>> rpcServerHandlerDone;
|
||||
StrictMock<testing::MockFunction<void()>> rpcEngineDone;
|
||||
testing::Expectation const expectedRpcEngineDone = EXPECT_CALL(rpcEngineDone, Call);
|
||||
EXPECT_CALL(rpcServerHandlerDone, Call).After(expectedRpcEngineDone);
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("some message");
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
boost::asio::spawn(
|
||||
ctx_,
|
||||
[this, &rpcEngineDone, fn = std::forward<decltype(fn)>(fn)](boost::asio::yield_context yield) {
|
||||
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
|
||||
fn(yield);
|
||||
rpcEngineDone.Call();
|
||||
}
|
||||
);
|
||||
return true;
|
||||
});
|
||||
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
rpcServerHandlerDone.Call();
|
||||
|
||||
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, JsonParseFailed)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("not a json");
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedParsedRequest)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = "{}";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(false));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
auto const responseHttp = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
|
||||
|
||||
auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
|
||||
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, DosguardAddsLoadWarning)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = "{}";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(false));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(false));
|
||||
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
auto const responseHttp = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
|
||||
|
||||
auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
|
||||
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
|
||||
|
||||
EXPECT_EQ(responseJson.at("warning").as_string(), "load");
|
||||
EXPECT_EQ(responseJson.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcRateLimit);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, GotNotJsonObject)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("[]");
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_NoRangeFromBackend)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = "{}";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillOnce(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, notifyNotReady);
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "notReady");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_ContextCreationFailed)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = "{}";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
|
||||
EXPECT_EQ(httpResponse.body(), "Null method");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseFailed)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{rpc::Status{rpc::ClioError::RpcUnknownOption}}));
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "unknownOption");
|
||||
|
||||
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1);
|
||||
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseThrewAnException)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse).WillOnce([](auto&&) -> rpc::Result {
|
||||
throw std::runtime_error("some error");
|
||||
});
|
||||
EXPECT_CALL(*rpcEngine_, notifyInternalError);
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::internal_server_error);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
|
||||
EXPECT_CALL(*rpcEngine_, notifyComplete);
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
|
||||
EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
|
||||
|
||||
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
|
||||
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_OutdatedWarning)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
|
||||
EXPECT_CALL(*rpcEngine_, notifyComplete);
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(61));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
|
||||
|
||||
std::unordered_set<int64_t> warningCodes;
|
||||
std::ranges::transform(
|
||||
jsonResponse.at("warnings").as_array(),
|
||||
std::inserter(warningCodes, warningCodes.end()),
|
||||
[](auto const& w) { return w.as_object().at("id").as_int64(); }
|
||||
);
|
||||
|
||||
EXPECT_EQ(warningCodes.size(), 2);
|
||||
EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcClio));
|
||||
EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcOutdated));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_Forwarded)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{
|
||||
{"result", boost::json::object{{"some key", "some value"}}}, {"forwarded", true}
|
||||
}}}));
|
||||
EXPECT_CALL(*rpcEngine_, notifyComplete);
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
|
||||
EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
|
||||
EXPECT_EQ(jsonResponse.at("forwarded").as_bool(), true);
|
||||
|
||||
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
|
||||
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_HasError)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
|
||||
auto const request = makeHttpRequest(requestStr);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{
|
||||
rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
|
||||
}));
|
||||
EXPECT_CALL(*rpcEngine_, notifyComplete);
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
|
||||
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
|
||||
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
|
||||
|
||||
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
|
||||
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
|
||||
});
|
||||
}
|
||||
|
||||
struct NgRpcServerHandlerWsTest : NgRpcServerHandlerTest {
|
||||
struct MockSubscriptionContext : web::SubscriptionContextInterface {
|
||||
using web::SubscriptionContextInterface::SubscriptionContextInterface;
|
||||
|
||||
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
|
||||
MOCK_METHOD(void, onDisconnect, (web::SubscriptionContextInterface::OnDisconnectSlot const&), (override));
|
||||
MOCK_METHOD(void, setApiSubversion, (uint32_t), (override));
|
||||
MOCK_METHOD(uint32_t, apiSubversion, (), (const, override));
|
||||
};
|
||||
using StrictMockSubscriptionContext = testing::StrictMock<MockSubscriptionContext>;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<StrictMockSubscriptionContext> subscriptionContext_ =
|
||||
std::make_shared<StrictMockSubscriptionContext>(tagFactory_);
|
||||
};
|
||||
|
||||
TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
Request::HttpHeaders const headers;
|
||||
std::string const requestStr = R"JSON({"method":"some_method", "id": 1234, "api_version": 1})JSON";
|
||||
auto const request = Request(requestStr, headers);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
|
||||
EXPECT_CALL(*rpcEngine_, notifyComplete);
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(response.message()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
|
||||
EXPECT_EQ(jsonResponse.at("status").as_string(), "success");
|
||||
|
||||
EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
|
||||
EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
|
||||
EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
|
||||
|
||||
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
|
||||
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest_HasError)
|
||||
{
|
||||
backend_->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
Request::HttpHeaders const headers;
|
||||
std::string const requestStr = R"JSON({"method":"some_method", "id": 1234, "api_version": 1})JSON";
|
||||
auto const request = Request(requestStr, headers);
|
||||
|
||||
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
|
||||
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
|
||||
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
|
||||
EXPECT_CALL(*rpcEngine_, buildResponse)
|
||||
.WillOnce(Return(rpc::Result{
|
||||
rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
|
||||
}));
|
||||
EXPECT_CALL(*rpcEngine_, notifyComplete);
|
||||
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
|
||||
fn(yield);
|
||||
return true;
|
||||
});
|
||||
auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
|
||||
|
||||
auto const jsonResponse = boost::json::parse(response.message()).as_object();
|
||||
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
|
||||
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
|
||||
|
||||
EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
|
||||
EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
|
||||
EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
|
||||
|
||||
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
|
||||
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
|
||||
});
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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/AsioContextTestFixture.hpp"
|
||||
#include "util/AssignRandomPort.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/TestHttpClient.hpp"
|
||||
#include "util/TestWebSocketClient.hpp"
|
||||
#include "util/config/ConfigConstraints.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigFileJson.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/Server.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address_v4.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
using namespace web::ng;
|
||||
using namespace util::config;
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct MakeServerTestBundle {
|
||||
std::string testName;
|
||||
std::string configJson;
|
||||
bool expectSuccess;
|
||||
};
|
||||
|
||||
struct MakeServerTest : NoLoggerFixture, testing::WithParamInterface<MakeServerTestBundle> {
|
||||
protected:
|
||||
boost::asio::io_context ioContext_;
|
||||
};
|
||||
|
||||
TEST_P(MakeServerTest, Make)
|
||||
{
|
||||
ConfigFileJson const json{boost::json::parse(GetParam().configJson).as_object()};
|
||||
|
||||
util::config::ClioConfigDefinition config{
|
||||
{"server.ip", ConfigValue{ConfigType::String}.optional()},
|
||||
{"server.port", ConfigValue{ConfigType::Integer}.optional()},
|
||||
{"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
|
||||
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
|
||||
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
|
||||
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
|
||||
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
|
||||
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()}
|
||||
|
||||
};
|
||||
auto const errors = config.parse(json);
|
||||
ASSERT_TRUE(!errors.has_value());
|
||||
|
||||
auto const expectedServer =
|
||||
makeServer(config, [](auto&&) -> std::expected<void, Response> { return {}; }, [](auto&&) {}, ioContext_);
|
||||
EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
MakeServerTests,
|
||||
MakeServerTest,
|
||||
testing::Values(
|
||||
MakeServerTestBundle{
|
||||
"BadEndpoint",
|
||||
R"JSON(
|
||||
{
|
||||
"server": {"ip": "wrong", "port": 12345}
|
||||
}
|
||||
)JSON",
|
||||
false
|
||||
},
|
||||
MakeServerTestBundle{
|
||||
"BadSslConfig",
|
||||
R"JSON(
|
||||
{
|
||||
"server": {"ip": "127.0.0.1", "port": 12345},
|
||||
"ssl_cert_file": "some_file"
|
||||
}
|
||||
)JSON",
|
||||
false
|
||||
},
|
||||
MakeServerTestBundle{
|
||||
"BadProcessingPolicy",
|
||||
R"JSON(
|
||||
{
|
||||
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "wrong"}
|
||||
}
|
||||
)JSON",
|
||||
false
|
||||
},
|
||||
MakeServerTestBundle{
|
||||
"CorrectConfig_ParallelPolicy",
|
||||
R"JSON(
|
||||
{
|
||||
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "parallel"}
|
||||
}
|
||||
)JSON",
|
||||
true
|
||||
},
|
||||
MakeServerTestBundle{
|
||||
"CorrectConfig_SequentPolicy",
|
||||
R"JSON(
|
||||
{
|
||||
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "sequent"}
|
||||
}
|
||||
)JSON",
|
||||
true
|
||||
}
|
||||
),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
struct ServerTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
|
||||
ServerTest()
|
||||
{
|
||||
[&]() { ASSERT_TRUE(server_.has_value()); }();
|
||||
server_->onGet("/", getHandler_.AsStdFunction());
|
||||
server_->onPost("/", postHandler_.AsStdFunction());
|
||||
server_->onWs(wsHandler_.AsStdFunction());
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t const serverPort_ = tests::util::generateFreePort();
|
||||
|
||||
ClioConfigDefinition const config_{
|
||||
{"server.ip", ConfigValue{ConfigType::String}.defaultValue("127.0.0.1").withConstraint(gValidateIp)},
|
||||
{"server.port", ConfigValue{ConfigType::Integer}.defaultValue(serverPort_).withConstraint(gValidatePort)},
|
||||
{"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
|
||||
{"server.admin_password", ConfigValue{ConfigType::String}.optional()},
|
||||
{"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
|
||||
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
|
||||
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
|
||||
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
|
||||
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
|
||||
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()}
|
||||
};
|
||||
|
||||
Server::OnConnectCheck emptyOnConnectCheck_ = [](auto&&) -> std::expected<void, Response> { return {}; };
|
||||
std::expected<Server, std::string> server_ = makeServer(config_, emptyOnConnectCheck_, [](auto&&) {}, ctx_);
|
||||
|
||||
std::string requestMessage_ = "some request";
|
||||
std::string const headerName_ = "Some-header";
|
||||
std::string const headerValue_ = "some value";
|
||||
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
getHandler_;
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
postHandler_;
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandler_;
|
||||
};
|
||||
|
||||
TEST_F(ServerTest, BadEndpoint)
|
||||
{
|
||||
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0};
|
||||
util::TagDecoratorFactory const tagDecoratorFactory{
|
||||
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
|
||||
};
|
||||
Server server{
|
||||
ctx_,
|
||||
endpoint,
|
||||
std::nullopt,
|
||||
ProcessingPolicy::Sequential,
|
||||
std::nullopt,
|
||||
tagDecoratorFactory,
|
||||
std::nullopt,
|
||||
emptyOnConnectCheck_,
|
||||
[](auto&&) {}
|
||||
};
|
||||
|
||||
auto maybeError = server.run();
|
||||
ASSERT_TRUE(maybeError.has_value());
|
||||
EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
|
||||
}
|
||||
|
||||
struct ServerHttpTestBundle {
|
||||
std::string testName;
|
||||
http::verb method;
|
||||
|
||||
Request::Method
|
||||
expectedMethod() const
|
||||
{
|
||||
switch (method) {
|
||||
case http::verb::get:
|
||||
return Request::Method::Get;
|
||||
case http::verb::post:
|
||||
return Request::Method::Post;
|
||||
default:
|
||||
return Request::Method::Unsupported;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct ServerHttpTest : ServerTest, testing::WithParamInterface<ServerHttpTestBundle> {};
|
||||
|
||||
TEST_F(ServerHttpTest, ClientDisconnects)
|
||||
{
|
||||
HttpAsyncClient client{ctx_};
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
client.disconnect();
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server_->run();
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerHttpTest, OnConnectCheck)
|
||||
{
|
||||
auto const serverPort = tests::util::generateFreePort();
|
||||
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
|
||||
util::TagDecoratorFactory const tagDecoratorFactory{
|
||||
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
|
||||
};
|
||||
|
||||
testing::StrictMock<testing::MockFunction<std::expected<void, Response>(Connection const&)>> onConnectCheck;
|
||||
|
||||
Server server{
|
||||
ctx_,
|
||||
endpoint,
|
||||
std::nullopt,
|
||||
ProcessingPolicy::Sequential,
|
||||
std::nullopt,
|
||||
tagDecoratorFactory,
|
||||
std::nullopt,
|
||||
onConnectCheck.AsStdFunction(),
|
||||
[](auto&&) {}
|
||||
};
|
||||
|
||||
HttpAsyncClient client{ctx_};
|
||||
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
boost::asio::steady_timer timer{yield.get_executor()};
|
||||
|
||||
EXPECT_CALL(onConnectCheck, Call)
|
||||
.WillOnce([&timer](Connection const& connection) -> std::expected<void, Response> {
|
||||
EXPECT_EQ(connection.ip(), "127.0.0.1");
|
||||
timer.cancel();
|
||||
return {};
|
||||
});
|
||||
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
|
||||
client.send(
|
||||
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
|
||||
yield,
|
||||
std::chrono::milliseconds{100}
|
||||
);
|
||||
|
||||
// Wait for the onConnectCheck to be called
|
||||
timer.expires_after(std::chrono::milliseconds{100});
|
||||
boost::system::error_code error; // Unused
|
||||
timer.async_wait(yield[error]);
|
||||
|
||||
client.gracefulShutdown();
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server.run();
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerHttpTest, OnConnectCheckFailed)
|
||||
{
|
||||
auto const serverPort = tests::util::generateFreePort();
|
||||
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
|
||||
util::TagDecoratorFactory const tagDecoratorFactory{
|
||||
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
|
||||
};
|
||||
|
||||
testing::StrictMock<testing::MockFunction<std::expected<void, Response>(Connection const&)>> onConnectCheck;
|
||||
|
||||
Server server{
|
||||
ctx_,
|
||||
endpoint,
|
||||
std::nullopt,
|
||||
ProcessingPolicy::Sequential,
|
||||
std::nullopt,
|
||||
tagDecoratorFactory,
|
||||
std::nullopt,
|
||||
onConnectCheck.AsStdFunction(),
|
||||
[](auto&&) {}
|
||||
};
|
||||
|
||||
HttpAsyncClient client{ctx_};
|
||||
|
||||
EXPECT_CALL(onConnectCheck, Call).WillOnce([](Connection const& connection) {
|
||||
EXPECT_EQ(connection.ip(), "127.0.0.1");
|
||||
return std::unexpected{
|
||||
Response{http::status::too_many_requests, boost::json::object{{"error", "some error"}}, connection}
|
||||
};
|
||||
});
|
||||
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
|
||||
client.send(
|
||||
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
|
||||
yield,
|
||||
std::chrono::milliseconds{100}
|
||||
);
|
||||
|
||||
auto const response = client.receive(yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_TRUE(response.has_value()) << response.error().message(); }();
|
||||
EXPECT_EQ(response->result(), http::status::too_many_requests);
|
||||
EXPECT_EQ(response->body(), R"JSON({"error":"some error"})JSON");
|
||||
EXPECT_EQ(response->version(), 11);
|
||||
|
||||
client.gracefulShutdown();
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server.run();
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerHttpTest, OnDisconnectHook)
|
||||
{
|
||||
auto const serverPort = tests::util::generateFreePort();
|
||||
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
|
||||
util::TagDecoratorFactory const tagDecoratorFactory{
|
||||
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
|
||||
};
|
||||
|
||||
testing::StrictMock<testing::MockFunction<void(Connection const&)>> onDisconnectHookMock;
|
||||
|
||||
Server server{
|
||||
ctx_,
|
||||
endpoint,
|
||||
std::nullopt,
|
||||
ProcessingPolicy::Sequential,
|
||||
std::nullopt,
|
||||
tagDecoratorFactory,
|
||||
std::nullopt,
|
||||
emptyOnConnectCheck_,
|
||||
onDisconnectHookMock.AsStdFunction()
|
||||
};
|
||||
|
||||
HttpAsyncClient client{ctx_};
|
||||
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
boost::asio::steady_timer timer{ctx_.get_executor(), std::chrono::milliseconds{100}};
|
||||
|
||||
EXPECT_CALL(onDisconnectHookMock, Call).WillOnce([&timer](auto&&) { timer.cancel(); });
|
||||
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
client.send(
|
||||
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
|
||||
yield,
|
||||
std::chrono::milliseconds{100}
|
||||
);
|
||||
|
||||
client.gracefulShutdown();
|
||||
|
||||
// Wait for OnDisconnectHook is called
|
||||
boost::system::error_code error;
|
||||
timer.async_wait(yield[error]);
|
||||
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server.run();
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerHttpTest, ClientIsDisconnectedIfServerStopped)
|
||||
{
|
||||
HttpAsyncClient client{ctx_};
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
|
||||
maybeError = client.send(
|
||||
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
|
||||
yield,
|
||||
std::chrono::milliseconds{100}
|
||||
);
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
auto message = client.receive(yield, std::chrono::milliseconds{100});
|
||||
EXPECT_TRUE(message.has_value()) << message.error().message();
|
||||
EXPECT_EQ(message->result(), http::status::service_unavailable);
|
||||
EXPECT_EQ(message->body(), "This Clio node is shutting down. Please try another node.");
|
||||
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server_->run();
|
||||
runSyncOperation([this](auto yield) { server_->stop(yield); });
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_P(ServerHttpTest, RequestResponse)
|
||||
{
|
||||
HttpAsyncClient client{ctx_};
|
||||
|
||||
http::request<http::string_body> request{GetParam().method, "/", 11, requestMessage_};
|
||||
request.set(headerName_, headerValue_);
|
||||
|
||||
Response const response{http::status::ok, "some response", Request{request}};
|
||||
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
|
||||
maybeError = client.send(request, yield, std::chrono::milliseconds{100});
|
||||
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
|
||||
|
||||
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
|
||||
EXPECT_EQ(expectedResponse->result(), http::status::ok);
|
||||
EXPECT_EQ(expectedResponse->body(), response.message());
|
||||
}
|
||||
|
||||
client.gracefulShutdown();
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
auto& handler = GetParam().method == http::verb::get ? getHandler_ : postHandler_;
|
||||
|
||||
EXPECT_CALL(handler, Call)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
|
||||
EXPECT_TRUE(receivedRequest.isHttp());
|
||||
EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod());
|
||||
EXPECT_EQ(receivedRequest.message(), request.body());
|
||||
EXPECT_EQ(receivedRequest.target(), request.target());
|
||||
EXPECT_EQ(receivedRequest.headerValue(headerName_), request.at(headerName_));
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
server_->run();
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
ServerHttpTests,
|
||||
ServerHttpTest,
|
||||
testing::Values(ServerHttpTestBundle{"GET", http::verb::get}, ServerHttpTestBundle{"POST", http::verb::post}),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
TEST_F(ServerTest, WsClientDisconnects)
|
||||
{
|
||||
WebSocketAsyncClient client{ctx_};
|
||||
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
client.close();
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server_->run();
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, WsRequestResponse)
|
||||
{
|
||||
WebSocketAsyncClient client{ctx_};
|
||||
|
||||
Request::HttpHeaders const headers{};
|
||||
Response const response{http::status::ok, "some response", Request{requestMessage_, headers}};
|
||||
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
|
||||
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
|
||||
maybeError = client.send(yield, requestMessage_, std::chrono::milliseconds{100});
|
||||
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
|
||||
|
||||
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
|
||||
EXPECT_EQ(expectedResponse.value(), response.message());
|
||||
}
|
||||
|
||||
client.gracefulClose(yield, std::chrono::milliseconds{100});
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
EXPECT_CALL(wsHandler_, Call)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
|
||||
EXPECT_FALSE(receivedRequest.isHttp());
|
||||
EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket);
|
||||
EXPECT_EQ(receivedRequest.message(), requestMessage_);
|
||||
EXPECT_EQ(receivedRequest.target(), std::nullopt);
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
server_->run();
|
||||
|
||||
runContext();
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, WsClientIsDisconnectedIfServerStopped)
|
||||
{
|
||||
WebSocketAsyncClient client{ctx_};
|
||||
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
|
||||
auto maybeError =
|
||||
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
|
||||
EXPECT_TRUE(maybeError.has_value());
|
||||
EXPECT_EQ(maybeError.value().value(), static_cast<int>(boost::beast::websocket::error::upgrade_declined));
|
||||
|
||||
ctx_.stop();
|
||||
});
|
||||
|
||||
server_->run();
|
||||
runSyncOperation([this](auto yield) { server_->stop(yield); });
|
||||
runContext();
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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/AsioContextTestFixture.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/ConfigDefinition.hpp"
|
||||
#include "util/config/ConfigValue.hpp"
|
||||
#include "util/config/Types.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/SubscriptionContext.hpp"
|
||||
#include "web/ng/impl/MockWsConnection.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using namespace web::ng;
|
||||
using namespace util::config;
|
||||
|
||||
struct NgSubscriptionContextTests : SyncAsioContextTest {
|
||||
SubscriptionContext
|
||||
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
|
||||
{
|
||||
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
|
||||
}
|
||||
|
||||
protected:
|
||||
util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
|
||||
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
|
||||
}};
|
||||
MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
|
||||
testing::StrictMock<testing::MockFunction<bool(web::ng::Error const&, Connection const&)>> errorHandler_;
|
||||
};
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, Send)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message = std::make_shared<std::string>("some message");
|
||||
|
||||
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return std::nullopt;
|
||||
});
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, SendOrder)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message1 = std::make_shared<std::string>("message1");
|
||||
auto const message2 = std::make_shared<std::string>("message2");
|
||||
|
||||
testing::Sequence const sequence;
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.InSequence(sequence)
|
||||
.WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.InSequence(sequence)
|
||||
.WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
subscriptionContext.send(message1);
|
||||
subscriptionContext.send(message2);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, SendFailed)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message = std::make_shared<std::string>("some message");
|
||||
|
||||
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return boost::system::errc::make_error_code(boost::system::errc::not_supported);
|
||||
});
|
||||
EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(connection_, close);
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, SendTooManySubscriptions)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield, 1);
|
||||
auto const message = std::make_shared<std::string>("message1");
|
||||
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
|
||||
boost::asio::post(innerYield); // simulate send is slow by switching to another coroutine
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(connection_, close);
|
||||
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, SendAfterDisconnect)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
auto const message = std::make_shared<std::string>("some message");
|
||||
subscriptionContext.disconnect(yield);
|
||||
subscriptionContext.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, OnDisconnect)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
|
||||
EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(NgSubscriptionContextTests, SetApiSubversion)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
subscriptionContext.setApiSubversion(42);
|
||||
EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
|
||||
});
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, 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 "rpc/Errors.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/impl/ErrorHandling.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
using namespace web::ng::impl;
|
||||
using namespace web::ng;
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct NgErrorHandlingTests : NoLoggerFixture {
|
||||
static Request
|
||||
makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
|
||||
{
|
||||
if (isHttp)
|
||||
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
|
||||
static Request::HttpHeaders const kHEADERS;
|
||||
return Request{body.value_or(""), kHEADERS};
|
||||
}
|
||||
};
|
||||
|
||||
struct NgErrorHandlingMakeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
rpc::Status status;
|
||||
std::string expectedMessage;
|
||||
boost::beast::http::status expectedStatus;
|
||||
};
|
||||
|
||||
struct NgErrorHandlingMakeErrorTest : NgErrorHandlingTests,
|
||||
testing::WithParamInterface<NgErrorHandlingMakeErrorTestBundle> {};
|
||||
|
||||
TEST_P(NgErrorHandlingMakeErrorTest, MakeError)
|
||||
{
|
||||
auto const request = makeRequest(GetParam().isHttp);
|
||||
ErrorHelper const errorHelper{request};
|
||||
|
||||
auto response = errorHelper.makeError(GetParam().status);
|
||||
EXPECT_EQ(response.message(), GetParam().expectedMessage);
|
||||
if (GetParam().isHttp) {
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
|
||||
|
||||
std::string expectedContentType = "text/html";
|
||||
if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
|
||||
expectedContentType = "application/json";
|
||||
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ng_ErrorHandlingMakeErrorTestGroup,
|
||||
NgErrorHandlingMakeErrorTest,
|
||||
testing::ValuesIn({
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"WsRequest",
|
||||
false,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
|
||||
boost::beast::http::status::ok
|
||||
},
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_InvalidApiVersion",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
|
||||
"invalid_API_version",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandIsMissing",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcCommandIsMissing},
|
||||
"Null method",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandIsEmpty",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
|
||||
"method is empty",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandNotString",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcCommandNotString},
|
||||
"method is not string",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_ParamsUnparsable",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::RpcParamsUnparsable},
|
||||
"params unparsable",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
NgErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_RippledError",
|
||||
true,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
}),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
struct NgErrorHandlingMakeInternalErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
std::optional<std::string> request;
|
||||
boost::json::object expectedResult;
|
||||
};
|
||||
|
||||
struct NgErrorHandlingMakeInternalErrorTest : NgErrorHandlingTests,
|
||||
testing::WithParamInterface<NgErrorHandlingMakeInternalErrorTestBundle> {
|
||||
};
|
||||
|
||||
TEST_P(NgErrorHandlingMakeInternalErrorTest, ComposeError)
|
||||
{
|
||||
auto const request = makeRequest(GetParam().isHttp, GetParam().request);
|
||||
std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
|
||||
? std::make_optional(boost::json::parse(*GetParam().request).as_object())
|
||||
: std::nullopt;
|
||||
ErrorHelper const errorHelper{request, requestJson};
|
||||
|
||||
auto response = errorHelper.makeInternalError();
|
||||
|
||||
EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
|
||||
if (GetParam().isHttp) {
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ng_ErrorHandlingComposeErrorTestGroup,
|
||||
NgErrorHandlingMakeInternalErrorTest,
|
||||
testing::ValuesIn(
|
||||
{NgErrorHandlingMakeInternalErrorTestBundle{
|
||||
"NoRequest_WebsocketConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}
|
||||
},
|
||||
NgErrorHandlingMakeInternalErrorTestBundle{
|
||||
"NoRequest_HttpConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
{{"result",
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}}}
|
||||
},
|
||||
NgErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_WebsocketConnection",
|
||||
false,
|
||||
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"api_version", 2},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}
|
||||
},
|
||||
NgErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_WebsocketConnection_NoId",
|
||||
false,
|
||||
std::string{R"JSON({"api_version": 2})JSON"},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"api_version", 2},
|
||||
{"request", {{"api_version", 2}}}}
|
||||
},
|
||||
NgErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_HttpConnection",
|
||||
true,
|
||||
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
|
||||
{{"result",
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}}}
|
||||
}}
|
||||
),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
|
||||
TEST_F(NgErrorHandlingTests, MakeNotReadyError)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeNotReadyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
|
||||
}
|
||||
);
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::ok);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
|
||||
}
|
||||
|
||||
TEST_F(NgErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
|
||||
{
|
||||
auto const request = makeRequest(false);
|
||||
auto response = ErrorHelper{request}.makeTooBusyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(NgErrorHandlingTests, sendTooBusyError_HttpConnection)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeTooBusyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
|
||||
}
|
||||
);
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
|
||||
}
|
||||
|
||||
TEST_F(NgErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
|
||||
{
|
||||
auto const request = makeRequest(false);
|
||||
auto response = ErrorHelper{request}.makeJsonParsingError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(NgErrorHandlingTests, makeJsonParsingError_HttpConnection)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeJsonParsingError();
|
||||
EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
|
||||
EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
|
||||
}
|
||||
|
||||
struct NgErrorHandlingComposeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
std::optional<boost::json::object> request;
|
||||
std::string expectedMessage;
|
||||
};
|
||||
|
||||
struct NgErrorHandlingComposeErrorTest : NgErrorHandlingTests,
|
||||
testing::WithParamInterface<NgErrorHandlingComposeErrorTestBundle> {};
|
||||
|
||||
TEST_P(NgErrorHandlingComposeErrorTest, ComposeError)
|
||||
{
|
||||
auto const request = makeRequest(GetParam().isHttp);
|
||||
ErrorHelper const errorHelper{request, GetParam().request};
|
||||
auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
|
||||
EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ng_ErrorHandlingComposeErrorTestGroup,
|
||||
NgErrorHandlingComposeErrorTest,
|
||||
testing::ValuesIn(
|
||||
{NgErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_WebsocketConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
|
||||
},
|
||||
NgErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_HttpConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
|
||||
},
|
||||
NgErrorHandlingComposeErrorTestBundle{
|
||||
"Request_WebsocketConnection",
|
||||
false,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})JSON",
|
||||
},
|
||||
NgErrorHandlingComposeErrorTestBundle{
|
||||
"Request_WebsocketConnection_NoId",
|
||||
false,
|
||||
boost::json::object{{"api_version", 2}},
|
||||
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})JSON",
|
||||
},
|
||||
NgErrorHandlingComposeErrorTestBundle{
|
||||
"Request_HttpConnection",
|
||||
true,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})JSON"
|
||||
}}
|
||||
),
|
||||
tests::util::kNAME_GENERATOR
|
||||
);
|
||||
Reference in New Issue
Block a user