chore: Revert "feat: Use new web server by default (#2182)" (#2187)

There is an issue found in the new web server, so we couldn't use it by
default for now.
This reverts commit b3f3259b14.
This commit is contained in:
Sergey Kuznetsov
2025-06-05 17:35:21 +01:00
committed by GitHub
parent b57618a211
commit 837a547849
66 changed files with 6824 additions and 2180 deletions

View File

@@ -47,6 +47,7 @@ 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")
@@ -92,7 +93,8 @@ 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)}};
return Action{Action::Run{.configPath = std::move(configPath), .useNgWebServer = parsed.count("ng-web-server") != 0}
};
}
} // namespace app

View File

@@ -45,6 +45,7 @@ public:
/** @brief Run action. */
struct Run {
std::string configPath; ///< Configuration file path.
bool useNgWebServer; ///< Whether to use a ng web server
};
/** @brief Exit action. */

View File

@@ -49,6 +49,8 @@
#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>
@@ -94,7 +96,7 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
}
int
ClioApplication::run()
ClioApplication::run(bool const useNgWebServer)
{
auto const threads = config_.get<uint16_t>("io_threads");
LOG(util::LogService::info()) << "Number of io threads = " << threads;
@@ -168,37 +170,51 @@ ClioApplication::run()
auto const rpcEngine =
RPCEngineType::makeRPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
web::RPCServerHandler<RPCEngineType> handler{config_, backend, rpcEngine, etl, dosGuard};
if (useNgWebServer or config_.get<bool>("server.__ng_web_server")) {
web::ng::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::makeServer(config_, OnConnectCheck{dosGuard}, DisconnectHook{dosGuard}, ioc);
auto httpServer = web::ng::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;
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;
}
httpServer->onGet("/metrics", MetricsHandler{adminVerifier});
httpServer->onGet("/health", HealthCheckHandler{});
auto requestHandler = RequestHandler{adminVerifier, handler};
httpServer->onPost("/", requestHandler);
httpServer->onWs(std::move(requestHandler));
// Init the web server
auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(config_, backend, rpcEngine, etl, dosGuard);
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)
);
auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler);
// Blocks until stopped.
// When stopped, shared_ptrs fall out of scope

View File

@@ -44,10 +44,12 @@ public:
/**
* @brief Run the application
*
* @param useNgWebServer Whether to use the new web server
*
* @return exit code
*/
int
run();
run(bool useNgWebServer);
};
} // namespace app

View File

@@ -25,7 +25,7 @@
#include "feed/SubscriptionManagerInterface.hpp"
#include "util/CoroutineGroup.hpp"
#include "util/log/Logger.hpp"
#include "web/Server.hpp"
#include "web/ng/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::SomeServer ServerType>
template <web::ng::SomeServer ServerType>
static std::function<void(boost::asio::yield_context)>
makeOnStopCallback(
ServerType& server,

View File

@@ -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::Response>
OnConnectCheck::operator()(web::Connection const& connection)
std::expected<void, web::ng::Response>
OnConnectCheck::operator()(web::ng::Connection const& connection)
{
dosguard_.get().increment(connection.ip());
if (not dosguard_.get().isOk(connection.ip())) {
return std::unexpected{
web::Response{boost::beast::http::status::too_many_requests, "Too many requests", connection}
web::ng::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::Connection const& connection)
DisconnectHook::operator()(web::ng::Connection const& connection)
{
dosguard_.get().decrement(connection.ip());
}
@@ -69,10 +69,10 @@ MetricsHandler::MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> a
{
}
web::Response
web::ng::Response
MetricsHandler::operator()(
web::Request const& request,
web::ConnectionMetadata& connectionMetadata,
web::ng::Request const& request,
web::ng::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::Response{std::move(maybeResponse).value(), request};
return web::ng::Response{std::move(maybeResponse).value(), request};
}
web::Response
web::ng::Response
HealthCheckHandler::operator()(
web::Request const& request,
web::ConnectionMetadata&,
web::ng::Request const& request,
web::ng::ConnectionMetadata&,
web::SubscriptionContextPtr,
boost::asio::yield_context
)
@@ -105,7 +105,7 @@ HealthCheckHandler::operator()(
</html>
)html";
return web::Response{boost::beast::http::status::ok, kHEALTH_CHECK_HTML, request};
return web::ng::Response{boost::beast::http::status::ok, kHEALTH_CHECK_HTML, request};
}
} // namespace app

View File

@@ -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::Response>
operator()(web::Connection const& connection);
std::expected<void, web::ng::Response>
operator()(web::ng::Connection const& connection);
};
/**
@@ -84,7 +84,7 @@ public:
* @param connection The connection which has disconnected.
*/
void
operator()(web::Connection const& connection);
operator()(web::ng::Connection const& connection);
};
/**
@@ -108,10 +108,10 @@ public:
* @param connectionMetadata The connection metadata.
* @return The response to the request.
*/
web::Response
web::ng::Response
operator()(
web::Request const& request,
web::ConnectionMetadata& connectionMetadata,
web::ng::Request const& request,
web::ng::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::Response
web::ng::Response
operator()(
web::Request const& request,
web::ConnectionMetadata&,
web::ng::Request const& request,
web::ng::ConnectionMetadata&,
web::SubscriptionContextPtr,
boost::asio::yield_context
);
@@ -169,10 +169,10 @@ public:
* @param yield The yield context.
* @return The response to the request.
*/
web::Response
web::ng::Response
operator()(
web::Request const& request,
web::ConnectionMetadata& connectionMetadata,
web::ng::Request const& request,
web::ng::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::Response{
return web::ng::Response{
boost::beast::http::status::internal_server_error,
rpc::makeError(rpc::RippledError::rpcINTERNAL),
request

View File

@@ -57,7 +57,7 @@ try {
return EXIT_FAILURE;
}
app::ClioApplication clio{gClioConfig};
return clio.run();
return clio.run(run.useNgWebServer);
},
[](app::CliArgs::Action::Migrate const& migrate) {
if (not app::parseConfig(migrate.configPath))

View File

@@ -338,6 +338,7 @@ 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)},

View File

@@ -7,14 +7,14 @@ target_sources(
dosguard/IntervalSweepHandler.cpp
dosguard/Weights.cpp
dosguard/WhitelistHandler.cpp
Connection.cpp
impl/ErrorHandling.cpp
impl/ConnectionHandler.cpp
impl/ServerSslContext.cpp
Request.cpp
Response.cpp
Server.cpp
SubscriptionContext.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
Resolver.cpp
SubscriptionContext.cpp
)

144
src/web/HttpSession.hpp Normal file
View File

@@ -0,0 +1,144 @@
//------------------------------------------------------------------------------
/*
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

204
src/web/PlainWsSession.hpp Normal file
View File

@@ -0,0 +1,204 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -1,67 +1,15 @@
# Web Server Subsystem
# Web server subsystem
This folder contains all of the classes for running Clio's web server.
## Overview
This folder contains all of the classes for running the web server.
The web server subsystem:
- 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
- Handles JSON-RPC and websocket requests.
## Key Components
- Supports SSL if a cert and key file are specified in the config.
### Core Components
- Handles all types of requests on a single port.
- **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
Each request is handled asynchronously using [Boost Asio](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio.html).
### 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
Much of this code was originally copied from Boost beast example code.

View File

@@ -26,23 +26,17 @@
#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>
@@ -54,8 +48,8 @@
#include <exception>
#include <functional>
#include <memory>
#include <optional>
#include <ratio>
#include <stdexcept>
#include <string>
#include <utility>
@@ -71,9 +65,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"};
@@ -93,14 +87,14 @@ public:
std::shared_ptr<BackendInterface const> const& backend,
std::shared_ptr<RPCEngineType> const& rpcEngine,
std::shared_ptr<etlng::ETLServiceInterface const> const& etl,
dosguard::DOSGuardInterface& dosguard
web::dosguard::DOSGuardInterface& dosguard
)
: backend_(backend)
, rpcEngine_(rpcEngine)
, etl_(etl)
, dosguard_(dosguard)
, tagFactory_(config)
, apiVersionParser_(config.getObject("api_version"))
, dosguard_(dosguard)
{
}
@@ -108,165 +102,123 @@ public:
* @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
* @param connection The connection
*/
[[nodiscard]] Response
operator()(
Request const& request,
ConnectionMetadata const& connectionMetadata,
SubscriptionContextPtr subscriptionContext,
boost::asio::yield_context yield
)
void
operator()(std::string const& request, std::shared_ptr<web::ConnectionBase> const& connection)
{
if (not dosguard_.get().isOk(connectionMetadata.ip())) {
return makeSlowDownResponse(request, std::nullopt);
if (not dosguard_.get().isOk(connection->clientIp)) {
connection->sendSlowDown(request);
return;
}
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");
try {
auto req = boost::json::parse(request).as_object();
LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue";
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 connection->upgraded and shouldReplaceParams(req))
req[JS(params)] = boost::json::array({boost::json::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 dosguard_.get().request(connection->clientIp, req)) {
connection->sendSlowDown(request);
return;
}
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();
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;
}
// 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
void
handleRequest(
boost::asio::yield_context yield,
Request const& rawRequest,
boost::json::object&& request,
ConnectionMetadata const& connectionMetadata,
SubscriptionContextPtr subscriptionContext
std::shared_ptr<web::ConnectionBase> const& connection
)
{
LOG(log_.info()) << connectionMetadata.tag() << (connectionMetadata.wasUpgraded() ? "ws" : "http")
LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
<< " received request from work queue: " << util::removeSecret(request)
<< " ip = " << connectionMetadata.ip();
<< " ip = " << connection->clientIp;
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();
web::impl::ErrorHelper(connection, std::move(request)).sendNotReadyError();
return;
}
auto const context = [&] {
if (connectionMetadata.wasUpgraded()) {
ASSERT(subscriptionContext != nullptr, "Subscription context must exist for a WS connection");
if (connection->upgraded) {
return rpc::makeWsContext(
yield,
request,
std::move(subscriptionContext),
tagFactory_.with(connectionMetadata.tag()),
connection->makeSubscriptionContext(tagFactory_),
tagFactory_.with(connection->tag()),
*range,
connectionMetadata.ip(),
connection->clientIp,
std::cref(apiVersionParser_),
connectionMetadata.isAdmin()
connection->isAdmin()
);
}
return rpc::makeHttpContext(
yield,
request,
tagFactory_.with(connectionMetadata.tag()),
tagFactory_.with(connection->tag()),
*range,
connectionMetadata.ip(),
connection->clientIp,
std::cref(apiVersionParser_),
connectionMetadata.isAdmin()
connection->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;
LOG(perfLog_.warn()) << connection->tag() << "Could not create Web context: " << err;
LOG(log_.warn()) << connection->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);
web::impl::ErrorHelper(connection, std::move(request)).sendError(err);
return;
}
auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
auto us = std::chrono::duration<int, std::milli>(timeDiff);
auto const 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());
response = web::impl::ErrorHelper(connection, request).composeError(result.response.error());
auto const responseStr = boost::json::serialize(response);
LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
@@ -285,7 +237,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)) || connectionMetadata.wasUpgraded())) {
if (isForwarded && (json.contains(JS(result)) || connection->upgraded)) {
for (auto const& [k, v] : json)
response.insert_or_assign(k, v);
} else {
@@ -297,7 +249,7 @@ private:
// for ws there is an additional field "status" in the response,
// otherwise the "status" is in the "result" field
if (connectionMetadata.wasUpgraded()) {
if (connection->upgraded) {
auto const appendFieldIfExist = [&](auto const& field) {
if (request.contains(field) and not request.at(field).is_null())
response[field] = request.at(field);
@@ -323,49 +275,18 @@ private:
warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
response["warnings"] = warnings;
return Response{boost::beast::http::status::ok, response, rawRequest};
connection->send(boost::json::serialize(response));
} 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();
LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
LOG(log_.error()) << connection->tag() << "Caught exception: " << ex.what();
rpcEngine_->notifyInternalError();
return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError();
}
}
web::impl::ErrorHelper(connection, std::move(request)).sendInternalError();
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;
}
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

View File

@@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
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
@@ -20,173 +20,363 @@
#pragma once
#include "util/Taggable.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/log/Logger.hpp"
#include "web/Connection.hpp"
#include "web/MessageHandler.hpp"
#include "web/ProcessingPolicy.hpp"
#include "web/Response.hpp"
#include "web/impl/ConnectionHandler.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 <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/socket_base.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 <concepts>
#include <cstddef>
#include <chrono>
#include <cstdint>
#include <exception>
#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 A tag class for server to help identify Server in templated code.
* @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
*/
struct ServerTag {
virtual ~ServerTag() = default;
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();
}
};
template <typename T>
concept SomeServer = std::derived_from<T, ServerTag>;
/**
* @brief Web server class.
* @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.
*/
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&)>;
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;
/**
* @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};
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_;
public:
/**
* @brief Construct a new Server object.
* @brief Create a new instance of the web server.
*
* @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.
* @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
*/
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
);
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;
/**
* @brief Copy constructor is deleted. The Server couldn't be copied.
*/
Server(Server const&) = delete;
acceptor_.open(endpoint.protocol(), ec);
if (ec)
return;
/**
* @brief Move constructor is deleted because connectionHandler_ contains references to some fields of the Server.
*/
Server(Server&&) = delete;
acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec);
if (ec)
return;
/**
* @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.
*/
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. */
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);
run()
{
doAccept();
}
private:
void
handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
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();
}
};
/** @brief The final type of the HttpServer used by Clio. */
template <typename HandlerType>
using HttpServer = Server<HttpSession, SslHttpSession, HandlerType>;
/**
* @brief Create a new Server.
* @brief A factory function that spawns a ready to use HTTP 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.
* @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
*/
std::expected<Server, std::string>
makeServer(
template <typename HandlerType>
static std::shared_ptr<HttpServer<HandlerType>>
makeHttpServer(
util::config::ClioConfigDefinition const& config,
Server::OnConnectCheck onConnectCheck,
Server::OnDisconnectHook onDisconnectHook,
boost::asio::io_context& context
);
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;
}
} // namespace web

188
src/web/SslHttpSession.hpp Normal file
View File

@@ -0,0 +1,188 @@
//------------------------------------------------------------------------------
/*
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

208
src/web/SslWsSession.hpp Normal file
View File

@@ -0,0 +1,208 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -21,14 +21,10 @@
#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>
@@ -36,39 +32,22 @@ namespace web {
SubscriptionContext::SubscriptionContext(
util::TagDecoratorFactory const& factory,
impl::WsConnectionBase& connection,
std::optional<size_t> maxSendQueueSize,
boost::asio::yield_context yield,
ErrorHandler errorHandler
std::shared_ptr<ConnectionBase> connection
)
: web::SubscriptionContextInterface(factory)
, connection_(connection)
, maxSendQueueSize_(maxSendQueueSize)
, tasksGroup_(yield)
, yield_(yield)
, errorHandler_(std::move(errorHandler))
: SubscriptionContextInterface{factory}, connection_{connection}
{
}
SubscriptionContext::~SubscriptionContext()
{
onDisconnect_(this);
}
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);
});
if (auto connection = connection_.lock(); connection != nullptr)
connection->send(std::move(message));
}
void
@@ -80,21 +59,13 @@ SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
void
SubscriptionContext::setApiSubversion(uint32_t value)
{
apiSubversion_ = 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);
return apiSubVersion_;
}
} // namespace web

View File

@@ -19,55 +19,32 @@
#pragma once
#include "util/CoroutineGroup.hpp"
#include "util/Taggable.hpp"
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/impl/WsConnection.hpp"
#include "web/interface/Concepts.hpp"
#include "web/interface/ConnectionBase.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 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.
* @brief A context of a WsBase connection for subscriptions.
*/
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_;
class SubscriptionContext : public SubscriptionContextInterface {
std::weak_ptr<ConnectionBase> connection_;
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;
std::atomic_uint32_t apiSubVersion_ = 0;
public:
/**
@@ -75,21 +52,17 @@ 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,
impl::WsConnectionBase& connection,
std::optional<size_t> maxSendQueueSize,
boost::asio::yield_context yield,
ErrorHandler errorHandler
);
SubscriptionContext(util::TagDecoratorFactory const& factory, std::shared_ptr<ConnectionBase> connection);
/**
* @brief Destroy the Subscription Context object
*/
~SubscriptionContext() override;
/**
* @brief Send message to the client
* @note This method does nothing after disconnected() was called.
* @note This method will not do anything if the related connection got disconnected.
*
* @param message The message to send.
*/
@@ -118,15 +91,6 @@ 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

View File

@@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
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
@@ -20,8 +20,9 @@
#pragma once
#include "rpc/Errors.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "rpc/JS.hpp"
#include "util/Assert.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/beast/http/status.hpp>
#include <boost/json/object.hpp>
@@ -30,8 +31,11 @@
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <variant>
namespace web::impl {
@@ -39,76 +43,137 @@ namespace web::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::shared_ptr<web::ConnectionBase> connection_;
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);
ErrorHelper(
std::shared_ptr<web::ConnectionBase> const& connection,
std::optional<boost::json::object> request = std::nullopt
)
: connection_{connection}, request_{std::move(request)}
{
}
/**
* @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;
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 internal error response.
*
* @return A response with an internal error.
*/
[[nodiscard]] Response
makeInternalError() 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 a response for when the server is not ready.
*
* @return A response with a not ready error.
*/
[[nodiscard]] Response
makeNotReadyError() 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 too busy.
*
* @return A response with a too busy error.
*/
[[nodiscard]] Response
makeTooBusyError() const;
void
sendNotReadyError() const
{
connection_->send(
boost::json::serialize(composeError(rpc::RippledError::rpcNOT_READY)), boost::beast::http::status::ok
);
}
/**
* @brief Make a response when json parsing fails.
*
* @return A response with a json parsing error.
*/
[[nodiscard]] Response
makeJsonParsingError() 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 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;
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 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;
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}};
}
};
} // namespace web::impl

327
src/web/impl/HttpBase.hpp Normal file
View File

@@ -0,0 +1,327 @@
//------------------------------------------------------------------------------
/*
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

318
src/web/impl/WsBase.hpp Normal file
View File

@@ -0,0 +1,318 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -17,7 +17,7 @@
*/
//==============================================================================
#include "web/Connection.hpp"
#include "web/ng/Connection.hpp"
#include "util/Taggable.hpp"
@@ -26,7 +26,7 @@
#include <string>
#include <utility>
namespace web {
namespace web::ng {
ConnectionMetadata::ConnectionMetadata(std::string ip, util::TagDecoratorFactory const& tagDecoratorFactory)
: util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}
@@ -54,4 +54,4 @@ Connection::Connection(
{
}
} // namespace web
} // namespace web::ng

View File

@@ -20,9 +20,9 @@
#pragma once
#include "util/Taggable.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/flat_buffer.hpp>
@@ -35,7 +35,7 @@
#include <optional>
#include <string>
namespace web {
namespace web::ng {
/**
* @brief An interface for a connection metadata class.
@@ -159,4 +159,4 @@ public:
*/
using ConnectionPtr = std::unique_ptr<Connection>;
} // namespace web
} // namespace web::ng

View File

@@ -21,11 +21,11 @@
#include <boost/system/detail/error_code.hpp>
namespace web {
namespace web::ng {
/**
* @brief Error of any async operation.
*/
using Error = boost::system::error_code;
} // namespace web
} // namespace web::ng

View File

@@ -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 {
namespace web::ng {
/**
* @brief Handler for messages.
@@ -36,4 +36,4 @@ namespace web {
using MessageHandler =
std::function<Response(Request const&, ConnectionMetadata&, SubscriptionContextPtr, boost::asio::yield_context)>;
} // namespace web
} // namespace web::ng

View File

@@ -19,11 +19,11 @@
#pragma once
namespace web {
namespace web::ng {
/**
* @brief Requests processing policy.
*/
enum class ProcessingPolicy { Sequential, Parallel };
} // namespace web
} // namespace web::ng

View File

@@ -0,0 +1,393 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -17,7 +17,7 @@
*/
//==============================================================================
#include "web/Request.hpp"
#include "web/ng/Request.hpp"
#include "util/OverloadSet.hpp"
@@ -33,7 +33,7 @@
#include <utility>
#include <variant>
namespace web {
namespace web::ng {
namespace {
@@ -142,4 +142,4 @@ Request::httpRequest() const
return std::get<HttpRequest>(data_);
}
} // namespace web
} // namespace web::ng

View File

@@ -29,7 +29,7 @@
#include <string_view>
#include <variant>
namespace web {
namespace web::ng {
/**
* @brief Represents an HTTP or WebSocket request.
@@ -150,4 +150,4 @@ private:
httpRequest() const;
};
} // namespace web
} // namespace web::ng

View File

@@ -17,13 +17,13 @@
*/
//==============================================================================
#include "web/Response.hpp"
#include "web/ng/Response.hpp"
#include "util/Assert.hpp"
#include "util/OverloadSet.hpp"
#include "util/build/Build.hpp"
#include "web/Connection.hpp"
#include "web/Request.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Request.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/beast/http/field.hpp>
@@ -43,7 +43,7 @@
namespace http = boost::beast::http;
namespace web {
namespace web::ng {
namespace {
@@ -193,4 +193,4 @@ Response::asWsResponse() const&
return boost::asio::buffer(message.data(), message.size());
}
} // namespace web
} // namespace web::ng

View File

@@ -19,7 +19,7 @@
#pragma once
#include "web/Request.hpp"
#include "web/ng/Request.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/beast/http/message.hpp>
@@ -30,7 +30,7 @@
#include <string>
#include <variant>
namespace web {
namespace web::ng {
class Connection;
@@ -133,4 +133,4 @@ public:
asWsResponse() const&;
};
} // namespace web
} // namespace web::ng

View File

@@ -17,19 +17,19 @@
*/
//==============================================================================
#include "web/Server.hpp"
#include "web/ng/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/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 "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 <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
@@ -54,7 +54,7 @@
#include <string>
#include <utility>
namespace web {
namespace web::ng {
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::impl::ConnectionHandler::stopConnection(*connection, yield);
web::ng::impl::ConnectionHandler::stopConnection(*connection, yield);
}
);
return;
@@ -382,4 +382,4 @@ makeServer(
};
}
} // namespace web
} // namespace web::ng

192
src/web/ng/Server.hpp Normal file
View File

@@ -0,0 +1,192 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -0,0 +1,100 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -0,0 +1,132 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -24,7 +24,7 @@
#include <type_traits>
namespace web::impl {
namespace web::ng::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::impl
} // namespace web::ng::impl

View File

@@ -17,20 +17,20 @@
*/
//==============================================================================
#include "web/impl/ConnectionHandler.hpp"
#include "web/ng/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::impl {
namespace web::ng::impl {
namespace {
@@ -387,4 +387,4 @@ ConnectionHandler::handleRequest(
}
}
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -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::impl {
namespace web::ng::impl {
class ConnectionHandler {
public:
@@ -161,4 +161,4 @@ private:
);
};
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -17,13 +17,13 @@
*/
//==============================================================================
#include "web/impl/ErrorHandling.hpp"
#include "web/ng/impl/ErrorHandling.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "util/Assert.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/beast/http/status.hpp>
#include <boost/json/object.hpp>
@@ -37,7 +37,7 @@
namespace http = boost::beast::http;
namespace web::impl {
namespace web::ng::impl {
namespace {
@@ -161,4 +161,4 @@ ErrorHelper::composeError(rpc::RippledError error) const
return composeErrorImpl(error, rawRequest_, request_);
}
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -0,0 +1,114 @@
//------------------------------------------------------------------------------
/*
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

View File

@@ -21,12 +21,12 @@
#include "util/Assert.hpp"
#include "util/Taggable.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 "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 <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
@@ -49,7 +49,7 @@
#include <string>
#include <utility>
namespace web::impl {
namespace web::ng::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::impl
} // namespace web::ng::impl

View File

@@ -17,7 +17,7 @@
*/
//==============================================================================
#include "web/impl/ServerSslContext.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include "util/config/ConfigDefinition.hpp"
@@ -33,7 +33,7 @@
#include <string>
#include <utility>
namespace web::impl {
namespace web::ng::impl {
namespace {
@@ -94,4 +94,4 @@ makeServerSslContext(std::string const& certData, std::string const& keyData)
return ctx;
}
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -27,7 +27,7 @@
#include <optional>
#include <string>
namespace web::impl {
namespace web::ng::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::impl
} // namespace web::ng::impl

View File

@@ -21,11 +21,11 @@
#include "util/Taggable.hpp"
#include "util/build/Build.hpp"
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/Concepts.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 <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
@@ -50,7 +50,7 @@
#include <string>
#include <utility>
namespace web::impl {
namespace web::ng::impl {
class WsConnectionBase : public Connection {
public:
@@ -192,4 +192,4 @@ makeWsConnection(
return connection;
}
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -19,11 +19,11 @@
#pragma once
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/HttpConnection.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 <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
@@ -33,25 +33,25 @@
#include <memory>
#include <optional>
struct MockConnectionMetadataImpl : web::ConnectionMetadata {
using web::ConnectionMetadata::ConnectionMetadata;
struct MockConnectionMetadataImpl : web::ng::ConnectionMetadata {
using web::ng::ConnectionMetadata::ConnectionMetadata;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
};
using MockConnectionMetadata = testing::NiceMock<MockConnectionMetadataImpl>;
using StrictMockConnectionMetadata = testing::StrictMock<MockConnectionMetadataImpl>;
struct MockConnectionImpl : web::Connection {
using web::Connection::Connection;
struct MockConnectionImpl : web::ng::Connection {
using web::ng::Connection::Connection;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
using SendReturnType = std::optional<web::Error>;
MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
using ReceiveReturnType = std::expected<web::Request, web::Error>;
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));

View File

@@ -21,11 +21,11 @@
#pragma once
#include "util/Taggable.hpp"
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/HttpConnection.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 <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
@@ -37,15 +37,15 @@
#include <memory>
#include <optional>
struct MockHttpConnectionImpl : web::impl::UpgradableConnection {
struct MockHttpConnectionImpl : web::ng::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::Error>;
MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
MOCK_METHOD(
SendReturnType,
@@ -54,15 +54,15 @@ struct MockHttpConnectionImpl : web::impl::UpgradableConnection {
(override)
);
using ReceiveReturnType = std::expected<web::Request, web::Error>;
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::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::Error>;
using IsUpgradeRequestedReturnType = std::expected<bool, web::ng::Error>;
MOCK_METHOD(IsUpgradeRequestedReturnType, isUpgradeRequested, (boost::asio::yield_context), (override));
using UpgradeReturnType = std::expected<web::ConnectionPtr, web::Error>;
using UpgradeReturnType = std::expected<web::ng::ConnectionPtr, web::ng::Error>;
using OptionalSslContext = std::optional<boost::asio::ssl::context>;
MOCK_METHOD(
UpgradeReturnType,

View File

@@ -19,11 +19,11 @@
#pragma once
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/WsConnection.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/WsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/spawn.hpp>
@@ -34,22 +34,22 @@
#include <memory>
#include <optional>
struct MockWsConnectionImpl : web::impl::WsConnectionBase {
struct MockWsConnectionImpl : web::ng::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::Error>;
MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
using ReceiveReturnType = std::expected<web::Request, web::Error>;
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
using SendBufferReturnType = std::optional<web::Error>;
using SendBufferReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(SendBufferReturnType, sendBuffer, (boost::asio::const_buffer, boost::asio::yield_context), (override));
};

View File

@@ -183,16 +183,17 @@ 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/impl/HttpConnectionTests.cpp
web/impl/ServerSslContextTests.cpp
web/impl/WsConnectionTests.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/RPCServerHandlerTests.cpp
web/ServerTests.cpp
web/SubscriptionContextTests.cpp

View File

@@ -50,6 +50,7 @@ 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(
@@ -63,6 +64,29 @@ 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 :

View File

@@ -25,7 +25,7 @@
#include "util/MockPrometheus.hpp"
#include "util/MockSubscriptionManager.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "web/Server.hpp"
#include "web/ng/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::ServerTag {
struct ServerMock : web::ng::ServerTag {
MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
};

View File

@@ -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::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
web::ng::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::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::ng::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::Response,
web::ng::Response,
call,
(web::Request const&,
web::ConnectionMetadata const&,
(web::ng::Request const&,
web::ng::ConnectionMetadata const&,
web::SubscriptionContextPtr,
boost::asio::yield_context),
()
);
web::Response
web::ng::Response
operator()(
web::Request const& request,
web::ConnectionMetadata const& connectionMetadata,
web::ng::Request const& request,
web::ng::ConnectionMetadata const& connectionMetadata,
web::SubscriptionContextPtr subscriptionContext,
boost::asio::yield_context yield
)
@@ -170,7 +170,7 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
TEST_F(RequestHandlerTest, RpcHandlerThrows)
{
web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::ng::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::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();
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();
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

File diff suppressed because it is too large Load Diff

View File

@@ -17,156 +17,62 @@
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/LoggerFixtures.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/impl/MockWsConnection.hpp"
#include "web/interface/ConnectionBaseMock.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 : SyncAsioContextTest {
SubscriptionContext
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
{
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
}
struct SubscriptionContextTests : NoLoggerFixture {
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::Error const&, Connection const&)>> errorHandler_;
ConnectionBaseStrictMockPtr connection_ =
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, "some ip");
SubscriptionContext subscriptionContext_{tagFactory_, connection_};
testing::StrictMock<testing::MockFunction<void(SubscriptionContextInterface*)>> callbackMock_;
};
TEST_F(SubscriptionContextTests, Send)
TEST_F(SubscriptionContextTests, 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);
});
auto message = std::make_shared<std::string>("message");
EXPECT_CALL(*connection_, send(message));
subscriptionContext_.send(message);
}
TEST_F(SubscriptionContextTests, SendOrder)
TEST_F(SubscriptionContextTests, sendConnectionExpired)
{
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);
});
auto message = std::make_shared<std::string>("message");
connection_.reset();
subscriptionContext_.send(message);
}
TEST_F(SubscriptionContextTests, SendFailed)
TEST_F(SubscriptionContextTests, onDisconnect)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
auto localContext = std::make_unique<SubscriptionContext>(tagFactory_, connection_);
localContext->onDisconnect(callbackMock_.AsStdFunction());
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);
});
EXPECT_CALL(callbackMock_, Call(localContext.get()));
localContext.reset();
}
TEST_F(SubscriptionContextTests, SendTooManySubscriptions)
TEST_F(SubscriptionContextTests, setApiSubversion)
{
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);
});
EXPECT_EQ(subscriptionContext_.apiSubversion(), 0);
subscriptionContext_.setApiSubversion(42);
EXPECT_EQ(subscriptionContext_.apiSubversion(), 42);
}

View File

@@ -20,213 +20,101 @@
#include "rpc/Errors.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "web/Request.hpp"
#include "util/Taggable.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.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;
namespace http = boost::beast::http;
using namespace util::config;
struct ErrorHandlingTests : 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};
}
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_);
};
struct ErrorHandlingMakeErrorTestBundle {
struct ErrorHandlingComposeErrorTestBundle {
std::string testName;
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;
bool connectionUpgraded;
std::optional<boost::json::object> request;
boost::json::object expectedResult;
};
struct ErrorHandlingMakeInternalErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingMakeInternalErrorTestBundle> {};
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
TEST_P(ErrorHandlingMakeInternalErrorTest, ComposeError)
TEST_P(ErrorHandlingComposeErrorTest, 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");
}
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));
}
INSTANTIATE_TEST_CASE_P(
ErrorHandlingComposeErrorTestGroup,
ErrorHandlingMakeInternalErrorTest,
ErrorHandlingComposeErrorTest,
testing::ValuesIn(
{ErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
{ErrorHandlingComposeErrorTestBundle{
"NoRequest_UpgradedConnection",
true,
std::nullopt,
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"}}
},
ErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_HttpConnection",
true,
ErrorHandlingComposeErrorTestBundle{
"NoRequest_NotUpgradedConnection",
false,
std::nullopt,
{{"result",
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"}}}}
},
ErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection",
false,
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
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."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"api_version", 2},
{"request", {{"id", 1}, {"api_version", 2}}}}
},
ErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection_NoId",
ErrorHandlingComposeErrorTestBundle{
"Request_NotUpgradedConnection",
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}}}}
},
ErrorHandlingMakeInternalErrorTestBundle{
"Request_HttpConnection",
true,
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
boost::json::object{{"id", 1}, {"api_version", 2}},
{{"result",
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"},
{"id", 1},
@@ -236,122 +124,169 @@ INSTANTIATE_TEST_CASE_P(
tests::util::kNAME_GENERATOR
);
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 {
struct ErrorHandlingSendErrorTestBundle {
std::string testName;
bool isHttp;
std::optional<boost::json::object> request;
bool connectionUpgraded;
rpc::Status status;
std::string expectedMessage;
boost::beast::http::status expectedStatus;
};
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
struct ErrorHandlingSendErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingSendErrorTestBundle> {};
TEST_P(ErrorHandlingComposeErrorTest, ComposeError)
TEST_P(ErrorHandlingSendErrorTest, sendError)
{
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);
connection_->upgraded = GetParam().connectionUpgraded;
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus));
errorHelper.sendError(GetParam().status);
}
INSTANTIATE_TEST_CASE_P(
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"
}}
),
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
},
}),
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();
}

View File

@@ -0,0 +1,611 @@
//------------------------------------------------------------------------------
/*
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);
});
}

View File

@@ -18,7 +18,7 @@
//==============================================================================
#include "util/NameGenerator.hpp"
#include "web/Request.hpp"
#include "web/ng/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;
using namespace web::ng;
namespace http = boost::beast::http;
struct RequestTest : public ::testing::Test {

View File

@@ -23,9 +23,9 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/MockConnection.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/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;
using namespace web::ng;
namespace http = boost::beast::http;
using namespace util::config;

View File

@@ -0,0 +1,583 @@
//------------------------------------------------------------------------------
/*
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();
}

View File

@@ -0,0 +1,172 @@
//------------------------------------------------------------------------------
/*
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);
});
}

View File

@@ -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/impl/ConnectionHandler.hpp"
#include "web/impl/MockHttpConnection.hpp"
#include "web/impl/MockWsConnection.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 <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
@@ -57,8 +57,8 @@
#include <string>
#include <utility>
using namespace web::impl;
using namespace web;
using namespace web::ng::impl;
using namespace web::ng;
using namespace util;
using testing::Return;
namespace beast = boost::beast;

View File

@@ -0,0 +1,358 @@
//------------------------------------------------------------------------------
/*
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
);

View File

@@ -25,9 +25,9 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/HttpConnection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/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::impl;
using namespace web;
using namespace web::ng::impl;
using namespace web::ng;
using namespace util::config;
namespace http = boost::beast::http;

View File

@@ -22,7 +22,7 @@
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/impl/ServerSslContext.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
@@ -33,7 +33,7 @@
#include <optional>
#include <string>
using namespace web::impl;
using namespace web::ng::impl;
using namespace util::config;
struct MakeServerSslContextFromConfigTestBundle {

View File

@@ -25,11 +25,11 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/HttpConnection.hpp"
#include "web/impl/WsConnection.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 <boost/asio/error.hpp>
#include <boost/asio/executor_work_guard.hpp>
@@ -51,8 +51,8 @@
#include <thread>
#include <utility>
using namespace web::impl;
using namespace web;
using namespace web::ng::impl;
using namespace web::ng;
using namespace util;
struct WebWsConnectionTests : SyncAsioContextTest {