mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	@@ -47,7 +47,6 @@ CliArgs::parse(int argc, char const* argv[])
 | 
			
		||||
        ("help,h", "Print help message and exit")
 | 
			
		||||
        ("version,v", "Print version and exit")
 | 
			
		||||
        ("conf,c", po::value<std::string>()->default_value(kDEFAULT_CONFIG_PATH), "Configuration file")
 | 
			
		||||
        ("ng-web-server,w", "Use ng-web-server")
 | 
			
		||||
        ("migrate", po::value<std::string>(), "Start migration helper")
 | 
			
		||||
        ("verify", "Checks the validity of config values")
 | 
			
		||||
        ("config-description,d", po::value<std::string>(), "Generate config description markdown file")
 | 
			
		||||
@@ -93,8 +92,7 @@ CliArgs::parse(int argc, char const* argv[])
 | 
			
		||||
    if (parsed.count("verify") != 0u)
 | 
			
		||||
        return Action{Action::VerifyConfig{.configPath = std::move(configPath)}};
 | 
			
		||||
 | 
			
		||||
    return Action{Action::Run{.configPath = std::move(configPath), .useNgWebServer = parsed.count("ng-web-server") != 0}
 | 
			
		||||
    };
 | 
			
		||||
    return Action{Action::Run{.configPath = std::move(configPath)}};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace app
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,6 @@ public:
 | 
			
		||||
        /** @brief Run action. */
 | 
			
		||||
        struct Run {
 | 
			
		||||
            std::string configPath;  ///< Configuration file path.
 | 
			
		||||
            bool useNgWebServer;     ///< Whether to use a ng web server
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /** @brief Exit action. */
 | 
			
		||||
 
 | 
			
		||||
@@ -49,8 +49,6 @@
 | 
			
		||||
#include "web/dosguard/IntervalSweepHandler.hpp"
 | 
			
		||||
#include "web/dosguard/Weights.hpp"
 | 
			
		||||
#include "web/dosguard/WhitelistHandler.hpp"
 | 
			
		||||
#include "web/ng/RPCServerHandler.hpp"
 | 
			
		||||
#include "web/ng/Server.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
 | 
			
		||||
@@ -96,7 +94,7 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int
 | 
			
		||||
ClioApplication::run(bool const useNgWebServer)
 | 
			
		||||
ClioApplication::run()
 | 
			
		||||
{
 | 
			
		||||
    auto const threads = config_.get<uint16_t>("io_threads");
 | 
			
		||||
    LOG(util::LogService::info()) << "Number of io threads = " << threads;
 | 
			
		||||
@@ -170,51 +168,37 @@ ClioApplication::run(bool const useNgWebServer)
 | 
			
		||||
    auto const rpcEngine =
 | 
			
		||||
        RPCEngineType::makeRPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
 | 
			
		||||
 | 
			
		||||
    if (useNgWebServer or config_.get<bool>("server.__ng_web_server")) {
 | 
			
		||||
        web::ng::RPCServerHandler<RPCEngineType> handler{config_, backend, rpcEngine, etl, dosGuard};
 | 
			
		||||
    web::RPCServerHandler<RPCEngineType> handler{config_, backend, rpcEngine, etl, dosGuard};
 | 
			
		||||
 | 
			
		||||
        auto expectedAdminVerifier = web::makeAdminVerificationStrategy(config_);
 | 
			
		||||
        if (not expectedAdminVerifier.has_value()) {
 | 
			
		||||
            LOG(util::LogService::error()) << "Error creating admin verifier: " << expectedAdminVerifier.error();
 | 
			
		||||
            return EXIT_FAILURE;
 | 
			
		||||
        }
 | 
			
		||||
        auto const adminVerifier = std::move(expectedAdminVerifier).value();
 | 
			
		||||
    auto expectedAdminVerifier = web::makeAdminVerificationStrategy(config_);
 | 
			
		||||
    if (not expectedAdminVerifier.has_value()) {
 | 
			
		||||
        LOG(util::LogService::error()) << "Error creating admin verifier: " << expectedAdminVerifier.error();
 | 
			
		||||
        return EXIT_FAILURE;
 | 
			
		||||
    }
 | 
			
		||||
    auto const adminVerifier = std::move(expectedAdminVerifier).value();
 | 
			
		||||
 | 
			
		||||
        auto httpServer = web::ng::makeServer(config_, OnConnectCheck{dosGuard}, DisconnectHook{dosGuard}, ioc);
 | 
			
		||||
    auto httpServer = web::makeServer(config_, OnConnectCheck{dosGuard}, DisconnectHook{dosGuard}, ioc);
 | 
			
		||||
 | 
			
		||||
        if (not httpServer.has_value()) {
 | 
			
		||||
            LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error();
 | 
			
		||||
            return EXIT_FAILURE;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        httpServer->onGet("/metrics", MetricsHandler{adminVerifier});
 | 
			
		||||
        httpServer->onGet("/health", HealthCheckHandler{});
 | 
			
		||||
        auto requestHandler = RequestHandler{adminVerifier, handler};
 | 
			
		||||
        httpServer->onPost("/", requestHandler);
 | 
			
		||||
        httpServer->onWs(std::move(requestHandler));
 | 
			
		||||
 | 
			
		||||
        auto const maybeError = httpServer->run();
 | 
			
		||||
        if (maybeError.has_value()) {
 | 
			
		||||
            LOG(util::LogService::error()) << "Error starting web server: " << *maybeError;
 | 
			
		||||
            return EXIT_FAILURE;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        appStopper_.setOnStop(
 | 
			
		||||
            Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Blocks until stopped.
 | 
			
		||||
        // When stopped, shared_ptrs fall out of scope
 | 
			
		||||
        // Calls destructors on all resources, and destructs in order
 | 
			
		||||
        start(ioc, threads);
 | 
			
		||||
 | 
			
		||||
        return EXIT_SUCCESS;
 | 
			
		||||
    if (not httpServer.has_value()) {
 | 
			
		||||
        LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error();
 | 
			
		||||
        return EXIT_FAILURE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Init the web server
 | 
			
		||||
    auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(config_, backend, rpcEngine, etl, dosGuard);
 | 
			
		||||
    httpServer->onGet("/metrics", MetricsHandler{adminVerifier});
 | 
			
		||||
    httpServer->onGet("/health", HealthCheckHandler{});
 | 
			
		||||
    auto requestHandler = RequestHandler{adminVerifier, handler};
 | 
			
		||||
    httpServer->onPost("/", requestHandler);
 | 
			
		||||
    httpServer->onWs(std::move(requestHandler));
 | 
			
		||||
 | 
			
		||||
    auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler);
 | 
			
		||||
    auto const maybeError = httpServer->run();
 | 
			
		||||
    if (maybeError.has_value()) {
 | 
			
		||||
        LOG(util::LogService::error()) << "Error starting web server: " << *maybeError;
 | 
			
		||||
        return EXIT_FAILURE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    appStopper_.setOnStop(
 | 
			
		||||
        Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, ioc)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Blocks until stopped.
 | 
			
		||||
    // When stopped, shared_ptrs fall out of scope
 | 
			
		||||
 
 | 
			
		||||
@@ -44,12 +44,10 @@ public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Run the application
 | 
			
		||||
     *
 | 
			
		||||
     * @param useNgWebServer Whether to use the new web server
 | 
			
		||||
     *
 | 
			
		||||
     * @return exit code
 | 
			
		||||
     */
 | 
			
		||||
    int
 | 
			
		||||
    run(bool useNgWebServer);
 | 
			
		||||
    run();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace app
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
#include "feed/SubscriptionManagerInterface.hpp"
 | 
			
		||||
#include "util/CoroutineGroup.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/ng/Server.hpp"
 | 
			
		||||
#include "web/Server.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/executor_work_guard.hpp>
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
@@ -74,7 +74,7 @@ public:
 | 
			
		||||
     * @param ioc The io_context to stop.
 | 
			
		||||
     * @return The callback to be called on application stop.
 | 
			
		||||
     */
 | 
			
		||||
    template <web::ng::SomeServer ServerType>
 | 
			
		||||
    template <web::SomeServer ServerType>
 | 
			
		||||
    static std::function<void(boost::asio::yield_context)>
 | 
			
		||||
    makeOnStopCallback(
 | 
			
		||||
        ServerType& server,
 | 
			
		||||
 
 | 
			
		||||
@@ -22,11 +22,11 @@
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/prometheus/Http.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
@@ -41,13 +41,13 @@ OnConnectCheck::OnConnectCheck(web::dosguard::DOSGuardInterface& dosguard) : dos
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::expected<void, web::ng::Response>
 | 
			
		||||
OnConnectCheck::operator()(web::ng::Connection const& connection)
 | 
			
		||||
std::expected<void, web::Response>
 | 
			
		||||
OnConnectCheck::operator()(web::Connection const& connection)
 | 
			
		||||
{
 | 
			
		||||
    dosguard_.get().increment(connection.ip());
 | 
			
		||||
    if (not dosguard_.get().isOk(connection.ip())) {
 | 
			
		||||
        return std::unexpected{
 | 
			
		||||
            web::ng::Response{boost::beast::http::status::too_many_requests, "Too many requests", connection}
 | 
			
		||||
            web::Response{boost::beast::http::status::too_many_requests, "Too many requests", connection}
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +59,7 @@ DisconnectHook::DisconnectHook(web::dosguard::DOSGuardInterface& dosguard) : dos
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
DisconnectHook::operator()(web::ng::Connection const& connection)
 | 
			
		||||
DisconnectHook::operator()(web::Connection const& connection)
 | 
			
		||||
{
 | 
			
		||||
    dosguard_.get().decrement(connection.ip());
 | 
			
		||||
}
 | 
			
		||||
@@ -69,10 +69,10 @@ MetricsHandler::MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> a
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
web::ng::Response
 | 
			
		||||
web::Response
 | 
			
		||||
MetricsHandler::operator()(
 | 
			
		||||
    web::ng::Request const& request,
 | 
			
		||||
    web::ng::ConnectionMetadata& connectionMetadata,
 | 
			
		||||
    web::Request const& request,
 | 
			
		||||
    web::ConnectionMetadata& connectionMetadata,
 | 
			
		||||
    web::SubscriptionContextPtr,
 | 
			
		||||
    boost::asio::yield_context
 | 
			
		||||
)
 | 
			
		||||
@@ -86,13 +86,13 @@ MetricsHandler::operator()(
 | 
			
		||||
        httpRequest, adminVerifier_->isAdmin(httpRequest, connectionMetadata.ip())
 | 
			
		||||
    );
 | 
			
		||||
    ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus");
 | 
			
		||||
    return web::ng::Response{std::move(maybeResponse).value(), request};
 | 
			
		||||
    return web::Response{std::move(maybeResponse).value(), request};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
web::ng::Response
 | 
			
		||||
web::Response
 | 
			
		||||
HealthCheckHandler::operator()(
 | 
			
		||||
    web::ng::Request const& request,
 | 
			
		||||
    web::ng::ConnectionMetadata&,
 | 
			
		||||
    web::Request const& request,
 | 
			
		||||
    web::ConnectionMetadata&,
 | 
			
		||||
    web::SubscriptionContextPtr,
 | 
			
		||||
    boost::asio::yield_context
 | 
			
		||||
)
 | 
			
		||||
@@ -105,7 +105,7 @@ HealthCheckHandler::operator()(
 | 
			
		||||
    </html>
 | 
			
		||||
)html";
 | 
			
		||||
 | 
			
		||||
    return web::ng::Response{boost::beast::http::status::ok, kHEALTH_CHECK_HTML, request};
 | 
			
		||||
    return web::Response{boost::beast::http::status::ok, kHEALTH_CHECK_HTML, request};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace app
 | 
			
		||||
 
 | 
			
		||||
@@ -22,11 +22,11 @@
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
@@ -60,8 +60,8 @@ public:
 | 
			
		||||
     * @param connection The connection to check.
 | 
			
		||||
     * @return A response if the connection is not allowed to proceed or void otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    std::expected<void, web::ng::Response>
 | 
			
		||||
    operator()(web::ng::Connection const& connection);
 | 
			
		||||
    std::expected<void, web::Response>
 | 
			
		||||
    operator()(web::Connection const& connection);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -84,7 +84,7 @@ public:
 | 
			
		||||
     * @param connection The connection which has disconnected.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    operator()(web::ng::Connection const& connection);
 | 
			
		||||
    operator()(web::Connection const& connection);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -108,10 +108,10 @@ public:
 | 
			
		||||
     * @param connectionMetadata The connection metadata.
 | 
			
		||||
     * @return The response to the request.
 | 
			
		||||
     */
 | 
			
		||||
    web::ng::Response
 | 
			
		||||
    web::Response
 | 
			
		||||
    operator()(
 | 
			
		||||
        web::ng::Request const& request,
 | 
			
		||||
        web::ng::ConnectionMetadata& connectionMetadata,
 | 
			
		||||
        web::Request const& request,
 | 
			
		||||
        web::ConnectionMetadata& connectionMetadata,
 | 
			
		||||
        web::SubscriptionContextPtr,
 | 
			
		||||
        boost::asio::yield_context
 | 
			
		||||
    );
 | 
			
		||||
@@ -128,10 +128,10 @@ public:
 | 
			
		||||
     * @param request The request to handle.
 | 
			
		||||
     * @return The response to the request
 | 
			
		||||
     */
 | 
			
		||||
    web::ng::Response
 | 
			
		||||
    web::Response
 | 
			
		||||
    operator()(
 | 
			
		||||
        web::ng::Request const& request,
 | 
			
		||||
        web::ng::ConnectionMetadata&,
 | 
			
		||||
        web::Request const& request,
 | 
			
		||||
        web::ConnectionMetadata&,
 | 
			
		||||
        web::SubscriptionContextPtr,
 | 
			
		||||
        boost::asio::yield_context
 | 
			
		||||
    );
 | 
			
		||||
@@ -169,10 +169,10 @@ public:
 | 
			
		||||
     * @param yield The yield context.
 | 
			
		||||
     * @return The response to the request.
 | 
			
		||||
     */
 | 
			
		||||
    web::ng::Response
 | 
			
		||||
    web::Response
 | 
			
		||||
    operator()(
 | 
			
		||||
        web::ng::Request const& request,
 | 
			
		||||
        web::ng::ConnectionMetadata& connectionMetadata,
 | 
			
		||||
        web::Request const& request,
 | 
			
		||||
        web::ConnectionMetadata& connectionMetadata,
 | 
			
		||||
        web::SubscriptionContextPtr subscriptionContext,
 | 
			
		||||
        boost::asio::yield_context yield
 | 
			
		||||
    )
 | 
			
		||||
@@ -188,7 +188,7 @@ public:
 | 
			
		||||
        try {
 | 
			
		||||
            return rpcHandler_(request, connectionMetadata, std::move(subscriptionContext), yield);
 | 
			
		||||
        } catch (std::exception const&) {
 | 
			
		||||
            return web::ng::Response{
 | 
			
		||||
            return web::Response{
 | 
			
		||||
                boost::beast::http::status::internal_server_error,
 | 
			
		||||
                rpc::makeError(rpc::RippledError::rpcINTERNAL),
 | 
			
		||||
                request
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ try {
 | 
			
		||||
                return EXIT_FAILURE;
 | 
			
		||||
            }
 | 
			
		||||
            app::ClioApplication clio{gClioConfig};
 | 
			
		||||
            return clio.run(run.useNgWebServer);
 | 
			
		||||
            return clio.run();
 | 
			
		||||
        },
 | 
			
		||||
        [](app::CliArgs::Action::Migrate const& migrate) {
 | 
			
		||||
            if (not app::parseConfig(migrate.configPath))
 | 
			
		||||
 
 | 
			
		||||
@@ -338,7 +338,6 @@ static ClioConfigDefinition gClioConfig = ClioConfigDefinition{
 | 
			
		||||
     {"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional().withConstraint(gValidateUint16)},
 | 
			
		||||
     {"server.ws_max_sending_queue_size",
 | 
			
		||||
      ConfigValue{ConfigType::Integer}.defaultValue(1500).withConstraint(gValidateUint32)},
 | 
			
		||||
     {"server.__ng_web_server", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
 | 
			
		||||
 | 
			
		||||
     {"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
 | 
			
		||||
     {"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,14 @@ target_sources(
 | 
			
		||||
          dosguard/IntervalSweepHandler.cpp
 | 
			
		||||
          dosguard/Weights.cpp
 | 
			
		||||
          dosguard/WhitelistHandler.cpp
 | 
			
		||||
          ng/Connection.cpp
 | 
			
		||||
          ng/impl/ErrorHandling.cpp
 | 
			
		||||
          ng/impl/ConnectionHandler.cpp
 | 
			
		||||
          ng/impl/ServerSslContext.cpp
 | 
			
		||||
          ng/Request.cpp
 | 
			
		||||
          ng/Response.cpp
 | 
			
		||||
          ng/Server.cpp
 | 
			
		||||
          ng/SubscriptionContext.cpp
 | 
			
		||||
          Connection.cpp
 | 
			
		||||
          impl/ErrorHandling.cpp
 | 
			
		||||
          impl/ConnectionHandler.cpp
 | 
			
		||||
          impl/ServerSslContext.cpp
 | 
			
		||||
          Request.cpp
 | 
			
		||||
          Response.cpp
 | 
			
		||||
          Server.cpp
 | 
			
		||||
          SubscriptionContext.cpp
 | 
			
		||||
          Resolver.cpp
 | 
			
		||||
          SubscriptionContext.cpp
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
ConnectionMetadata::ConnectionMetadata(std::string ip, util::TagDecoratorFactory const& tagDecoratorFactory)
 | 
			
		||||
    : util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}
 | 
			
		||||
@@ -54,4 +54,4 @@ Connection::Connection(
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -20,9 +20,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief An interface for a connection metadata class.
 | 
			
		||||
@@ -159,4 +159,4 @@ public:
 | 
			
		||||
 */
 | 
			
		||||
using ConnectionPtr = std::unique_ptr<Connection>;
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -21,11 +21,11 @@
 | 
			
		||||
 | 
			
		||||
#include <boost/system/detail/error_code.hpp>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Error of any async operation.
 | 
			
		||||
 */
 | 
			
		||||
using Error = boost::system::error_code;
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -1,144 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/PlainWsSession.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/impl/HttpBase.hpp"
 | 
			
		||||
#include "web/interface/Concepts.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
using tcp = boost::asio::ip::tcp;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Represents a HTTP connection established by a client.
 | 
			
		||||
 *
 | 
			
		||||
 * It will handle the upgrade to websocket, pass the ownership of the socket to the upgrade session.
 | 
			
		||||
 * Otherwise, it will pass control to the base class.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam HandlerType The type of the server handler to use
 | 
			
		||||
 */
 | 
			
		||||
template <SomeServerHandler HandlerType>
 | 
			
		||||
class HttpSession : public impl::HttpBase<HttpSession, HandlerType>,
 | 
			
		||||
                    public std::enable_shared_from_this<HttpSession<HandlerType>> {
 | 
			
		||||
    boost::beast::tcp_stream stream_;
 | 
			
		||||
    std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
 | 
			
		||||
    std::uint32_t maxWsSendingQueueSize_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new session.
 | 
			
		||||
     *
 | 
			
		||||
     * @param socket The socket. Ownership is transferred to HttpSession
 | 
			
		||||
     * @param ip Client's IP address
 | 
			
		||||
     * @param adminVerification The admin verification strategy to use
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param buffer Buffer with initial data received from the peer
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    explicit HttpSession(
 | 
			
		||||
        tcp::socket&& socket,
 | 
			
		||||
        std::string const& ip,
 | 
			
		||||
        std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> const& handler,
 | 
			
		||||
        boost::beast::flat_buffer buffer,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : impl::HttpBase<HttpSession, HandlerType>(
 | 
			
		||||
              ip,
 | 
			
		||||
              tagFactory,
 | 
			
		||||
              adminVerification,
 | 
			
		||||
              dosGuard,
 | 
			
		||||
              handler,
 | 
			
		||||
              std::move(buffer)
 | 
			
		||||
          )
 | 
			
		||||
        , stream_(std::move(socket))
 | 
			
		||||
        , tagFactory_(tagFactory)
 | 
			
		||||
        , maxWsSendingQueueSize_(maxWsSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~HttpSession() override = default;
 | 
			
		||||
 | 
			
		||||
    /** @return The TCP stream */
 | 
			
		||||
    boost::beast::tcp_stream&
 | 
			
		||||
    stream()
 | 
			
		||||
    {
 | 
			
		||||
        return stream_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Starts reading from the stream. */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        boost::asio::dispatch(
 | 
			
		||||
            stream_.get_executor(),
 | 
			
		||||
            boost::beast::bind_front_handler(
 | 
			
		||||
                &impl::HttpBase<HttpSession, HandlerType>::doRead, this->shared_from_this()
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Closes the underlying socket. */
 | 
			
		||||
    void
 | 
			
		||||
    doClose()
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::error_code ec;
 | 
			
		||||
        stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Upgrade to WebSocket connection. */
 | 
			
		||||
    void
 | 
			
		||||
    upgrade()
 | 
			
		||||
    {
 | 
			
		||||
        std::make_shared<WsUpgrader<HandlerType>>(
 | 
			
		||||
            std::move(stream_),
 | 
			
		||||
            this->clientIp,
 | 
			
		||||
            tagFactory_,
 | 
			
		||||
            this->dosGuard_,
 | 
			
		||||
            this->handler_,
 | 
			
		||||
            std::move(this->buffer_),
 | 
			
		||||
            std::move(this->req_),
 | 
			
		||||
            ConnectionBase::isAdmin(),
 | 
			
		||||
            maxWsSendingQueueSize_
 | 
			
		||||
        )
 | 
			
		||||
            ->run();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -19,16 +19,16 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
 | 
			
		||||
#include <functional>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Handler for messages.
 | 
			
		||||
@@ -36,4 +36,4 @@ namespace web::ng {
 | 
			
		||||
using MessageHandler =
 | 
			
		||||
    std::function<Response(Request const&, ConnectionMetadata&, SubscriptionContextPtr, boost::asio::yield_context)>;
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -1,204 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/impl/WsBase.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/stream_traits.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/parser.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream.hpp>
 | 
			
		||||
#include <boost/optional/optional.hpp>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Represents a non-secure websocket session.
 | 
			
		||||
 *
 | 
			
		||||
 * Majority of the operations are handled by the base class.
 | 
			
		||||
 */
 | 
			
		||||
template <SomeServerHandler HandlerType>
 | 
			
		||||
class PlainWsSession : public impl::WsBase<PlainWsSession, HandlerType> {
 | 
			
		||||
    using StreamType = boost::beast::websocket::stream<boost::beast::tcp_stream>;
 | 
			
		||||
    StreamType ws_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new non-secure websocket session.
 | 
			
		||||
     *
 | 
			
		||||
     * @param socket The socket. Ownership is transferred
 | 
			
		||||
     * @param ip Client's IP address
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param buffer Buffer with initial data received from the peer
 | 
			
		||||
     * @param isAdmin Whether the connection has admin privileges,
 | 
			
		||||
     * @param maxSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    explicit PlainWsSession(
 | 
			
		||||
        boost::asio::ip::tcp::socket&& socket,
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> const& handler,
 | 
			
		||||
        boost::beast::flat_buffer&& buffer,
 | 
			
		||||
        bool isAdmin,
 | 
			
		||||
        std::uint32_t maxSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : impl::WsBase<PlainWsSession, HandlerType>(
 | 
			
		||||
              ip,
 | 
			
		||||
              tagFactory,
 | 
			
		||||
              dosGuard,
 | 
			
		||||
              handler,
 | 
			
		||||
              std::move(buffer),
 | 
			
		||||
              maxSendingQueueSize
 | 
			
		||||
          )
 | 
			
		||||
        , ws_(std::move(socket))
 | 
			
		||||
    {
 | 
			
		||||
        ConnectionBase::isAdmin_ = isAdmin;  // NOLINT(cppcoreguidelines-prefer-member-initializer)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~PlainWsSession() override = default;
 | 
			
		||||
 | 
			
		||||
    /** @return The websocket stream. */
 | 
			
		||||
    StreamType&
 | 
			
		||||
    ws()
 | 
			
		||||
    {
 | 
			
		||||
        return ws_;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The websocket upgrader class, upgrade from an HTTP session to a non-secure websocket session.
 | 
			
		||||
 *
 | 
			
		||||
 * Pass the socket to the session class after upgrade.
 | 
			
		||||
 */
 | 
			
		||||
template <SomeServerHandler HandlerType>
 | 
			
		||||
class WsUpgrader : public std::enable_shared_from_this<WsUpgrader<HandlerType>> {
 | 
			
		||||
    using std::enable_shared_from_this<WsUpgrader<HandlerType>>::shared_from_this;
 | 
			
		||||
 | 
			
		||||
    boost::beast::tcp_stream http_;
 | 
			
		||||
    boost::optional<http::request_parser<http::string_body>> parser_;
 | 
			
		||||
    boost::beast::flat_buffer buffer_;
 | 
			
		||||
    std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
 | 
			
		||||
    http::request<http::string_body> req_;
 | 
			
		||||
    std::string ip_;
 | 
			
		||||
    std::shared_ptr<HandlerType> const handler_;
 | 
			
		||||
    bool isAdmin_;
 | 
			
		||||
    std::uint32_t maxWsSendingQueueSize_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new upgrader to non-secure websocket.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stream The TCP stream. Ownership is transferred
 | 
			
		||||
     * @param ip Client's IP address
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param buffer Buffer with initial data received from the peer. Ownership is transferred
 | 
			
		||||
     * @param request The request. Ownership is transferred
 | 
			
		||||
     * @param isAdmin Whether the connection has admin privileges
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    WsUpgrader(
 | 
			
		||||
        boost::beast::tcp_stream&& stream,
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> const& handler,
 | 
			
		||||
        boost::beast::flat_buffer&& buffer,
 | 
			
		||||
        http::request<http::string_body> request,
 | 
			
		||||
        bool isAdmin,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : http_(std::move(stream))
 | 
			
		||||
        , buffer_(std::move(buffer))
 | 
			
		||||
        , tagFactory_(tagFactory)
 | 
			
		||||
        , dosGuard_(dosGuard)
 | 
			
		||||
        , req_(std::move(request))
 | 
			
		||||
        , ip_(std::move(ip))
 | 
			
		||||
        , handler_(handler)
 | 
			
		||||
        , isAdmin_(isAdmin)
 | 
			
		||||
        , maxWsSendingQueueSize_(maxWsSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Initiate the upgrade. */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        boost::asio::dispatch(
 | 
			
		||||
            http_.get_executor(),
 | 
			
		||||
            boost::beast::bind_front_handler(&WsUpgrader<HandlerType>::doUpgrade, shared_from_this())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    doUpgrade()
 | 
			
		||||
    {
 | 
			
		||||
        parser_.emplace();
 | 
			
		||||
 | 
			
		||||
        static constexpr auto kMAX_BODY_SIZE = 10000;
 | 
			
		||||
        parser_->body_limit(kMAX_BODY_SIZE);
 | 
			
		||||
 | 
			
		||||
        boost::beast::get_lowest_layer(http_).expires_after(std::chrono::seconds(30));
 | 
			
		||||
        onUpgrade();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onUpgrade()
 | 
			
		||||
    {
 | 
			
		||||
        if (!boost::beast::websocket::is_upgrade(req_))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // Disable the timeout. The websocket::stream uses its own timeout settings.
 | 
			
		||||
        boost::beast::get_lowest_layer(http_).expires_never();
 | 
			
		||||
 | 
			
		||||
        std::make_shared<PlainWsSession<HandlerType>>(
 | 
			
		||||
            http_.release_socket(),
 | 
			
		||||
            ip_,
 | 
			
		||||
            tagFactory_,
 | 
			
		||||
            dosGuard_,
 | 
			
		||||
            handler_,
 | 
			
		||||
            std::move(buffer_),
 | 
			
		||||
            isAdmin_,
 | 
			
		||||
            maxWsSendingQueueSize_
 | 
			
		||||
        )
 | 
			
		||||
            ->run(std::move(req_));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -19,11 +19,11 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Requests processing policy.
 | 
			
		||||
 */
 | 
			
		||||
enum class ProcessingPolicy { Sequential, Parallel };
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -1,15 +1,67 @@
 | 
			
		||||
# Web server subsystem
 | 
			
		||||
# Web Server Subsystem
 | 
			
		||||
 | 
			
		||||
This folder contains all of the classes for running the web server.
 | 
			
		||||
This folder contains all of the classes for running Clio's web server.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
The web server subsystem:
 | 
			
		||||
 | 
			
		||||
- Handles JSON-RPC and websocket requests.
 | 
			
		||||
- Handles JSON-RPC requests over HTTP and WebSocket connections
 | 
			
		||||
- Supports SSL/TLS encryption if certificate and key files are specified in the config
 | 
			
		||||
- Processes all types of requests on a single port
 | 
			
		||||
- Implements asynchronous request handling using [Boost Asio](https://www.boost.org/doc/libs/1_83_0/doc/html/boost_asio.html)
 | 
			
		||||
- Provides request rate limiting through a built-in Denial-of-Service (DoS) Guard mechanism
 | 
			
		||||
- Supports both sequential and parallel request processing policies
 | 
			
		||||
 | 
			
		||||
- Supports SSL if a cert and key file are specified in the config.
 | 
			
		||||
## Key Components
 | 
			
		||||
 | 
			
		||||
- Handles all types of requests on a single port.
 | 
			
		||||
### Core Components
 | 
			
		||||
 | 
			
		||||
Each request is handled asynchronously using [Boost Asio](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio.html).
 | 
			
		||||
- **Server** (`Server.hpp/cpp`): The main web server class that manages connections and routes requests
 | 
			
		||||
- **Connection** (`Connection.hpp/cpp`): Represents a client connection and provides an abstraction layer over HTTP and WebSocket connections
 | 
			
		||||
- **Request/Response** (`Request.hpp/cpp`, `Response.hpp/cpp`): Classes for handling HTTP requests and responses
 | 
			
		||||
- **MessageHandler** (`MessageHandler.hpp`): An interface for handling different types of messages (e.g., HTTP GET/POST, WebSocket)
 | 
			
		||||
- **RPCServerHandler** (`RPCServerHandler.hpp`): Handles RPC requests and integrates with the RPC engine
 | 
			
		||||
 | 
			
		||||
Much of this code was originally copied from Boost beast example code.
 | 
			
		||||
### Connection Processing
 | 
			
		||||
 | 
			
		||||
- **ConnectionHandler** (`impl/ConnectionHandler.hpp/cpp`): Manages the lifecycle of connections and processes requests
 | 
			
		||||
- **ProcessingPolicy** (`ProcessingPolicy.hpp`): Defines whether requests are processed sequentially or in parallel
 | 
			
		||||
- **HttpConnection/WsConnection** (`impl/HttpConnection.hpp`, `impl/WsConnection.hpp`): Concrete implementations for HTTP and WebSocket connections
 | 
			
		||||
 | 
			
		||||
### Security Features
 | 
			
		||||
 | 
			
		||||
- **DOSGuard** (`dosguard/DOSGuard.hpp/cpp`): Denial-of-Service protection system that implements rate limiting
 | 
			
		||||
- **IntervalSweepHandler** (`dosguard/IntervalSweepHandler.hpp/cpp`): Periodically clears DoS guard state
 | 
			
		||||
- **WhitelistHandler** (`dosguard/WhitelistHandler.hpp/cpp`): Manages IP address whitelisting for bypass of rate limits
 | 
			
		||||
- **AdminVerificationStrategy** (`AdminVerificationStrategy.hpp/cpp`): Handles verification of admin privileges
 | 
			
		||||
 | 
			
		||||
### Subscription
 | 
			
		||||
 | 
			
		||||
- **SubscriptionContext** (`SubscriptionContext.hpp/cpp`): Manages WebSocket subscriptions for streaming updates
 | 
			
		||||
 | 
			
		||||
## Architecture
 | 
			
		||||
 | 
			
		||||
The server design uses the following patterns:
 | 
			
		||||
 | 
			
		||||
- **RAII**: Resource management through C++ RAII principles
 | 
			
		||||
- **Dependency Injection**: Components accept their dependencies through constructor parameters
 | 
			
		||||
- **Interface-based design**: Components depend on interfaces rather than concrete implementations
 | 
			
		||||
- **Asynchronous programming**: Uses Boost Asio for non-blocking I/O operations with coroutines
 | 
			
		||||
 | 
			
		||||
Each incoming request is handled asynchronously, with the processing being dispatched to appropriate handlers based on the request type (GET, POST, WebSocket). The server supports both secure (SSL/TLS) and non-secure connections based on configuration.
 | 
			
		||||
 | 
			
		||||
## SSL Support
 | 
			
		||||
 | 
			
		||||
The server creates an SSL context if certificate and key files are specified in the configuration. When SSL is enabled, all connections are encrypted.
 | 
			
		||||
 | 
			
		||||
## Request Flow
 | 
			
		||||
 | 
			
		||||
1. Client connects to the server
 | 
			
		||||
2. Server performs security checks (e.g., DoS Guard, admin verification if needed)
 | 
			
		||||
3. Server reads the request asynchronously
 | 
			
		||||
4. Request is routed to appropriate handler based on HTTP method and target
 | 
			
		||||
5. Handler processes the request and generates a response
 | 
			
		||||
6. Response is sent back to the client
 | 
			
		||||
7. For persistent connections, the server returns to step 3
 | 
			
		||||
8. When the client disconnects or an error occurs, the connection is closed
 | 
			
		||||
 
 | 
			
		||||
@@ -26,17 +26,23 @@
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/impl/APIVersionParser.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/CoroutineGroup.hpp"
 | 
			
		||||
#include "util/JsonUtils.hpp"
 | 
			
		||||
#include "util/Profiler.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/impl/ErrorHandling.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/steady_timer.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
@@ -48,8 +54,8 @@
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <ratio>
 | 
			
		||||
#include <stdexcept>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
@@ -65,9 +71,9 @@ class RPCServerHandler {
 | 
			
		||||
    std::shared_ptr<BackendInterface const> const backend_;
 | 
			
		||||
    std::shared_ptr<RPCEngineType> const rpcEngine_;
 | 
			
		||||
    std::shared_ptr<etlng::ETLServiceInterface const> const etl_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosguard_;
 | 
			
		||||
    util::TagDecoratorFactory const tagFactory_;
 | 
			
		||||
    rpc::impl::ProductionAPIVersionParser apiVersionParser_;  // can be injected if needed
 | 
			
		||||
    std::reference_wrapper<web::dosguard::DOSGuardInterface> dosguard_;
 | 
			
		||||
 | 
			
		||||
    util::Logger log_{"RPC"};
 | 
			
		||||
    util::Logger perfLog_{"Performance"};
 | 
			
		||||
@@ -87,14 +93,14 @@ public:
 | 
			
		||||
        std::shared_ptr<BackendInterface const> const& backend,
 | 
			
		||||
        std::shared_ptr<RPCEngineType> const& rpcEngine,
 | 
			
		||||
        std::shared_ptr<etlng::ETLServiceInterface const> const& etl,
 | 
			
		||||
        web::dosguard::DOSGuardInterface& dosguard
 | 
			
		||||
        dosguard::DOSGuardInterface& dosguard
 | 
			
		||||
    )
 | 
			
		||||
        : backend_(backend)
 | 
			
		||||
        , rpcEngine_(rpcEngine)
 | 
			
		||||
        , etl_(etl)
 | 
			
		||||
        , dosguard_(dosguard)
 | 
			
		||||
        , tagFactory_(config)
 | 
			
		||||
        , apiVersionParser_(config.getObject("api_version"))
 | 
			
		||||
        , dosguard_(dosguard)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -102,123 +108,165 @@ public:
 | 
			
		||||
     * @brief The callback when server receives a request.
 | 
			
		||||
     *
 | 
			
		||||
     * @param request The request
 | 
			
		||||
     * @param connection The connection
 | 
			
		||||
     * @param connectionMetadata The connection metadata
 | 
			
		||||
     * @param subscriptionContext The subscription context
 | 
			
		||||
     * @param yield The yield context
 | 
			
		||||
     * @return The response
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    operator()(std::string const& request, std::shared_ptr<web::ConnectionBase> const& connection)
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    operator()(
 | 
			
		||||
        Request const& request,
 | 
			
		||||
        ConnectionMetadata const& connectionMetadata,
 | 
			
		||||
        SubscriptionContextPtr subscriptionContext,
 | 
			
		||||
        boost::asio::yield_context yield
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (not dosguard_.get().isOk(connection->clientIp)) {
 | 
			
		||||
            connection->sendSlowDown(request);
 | 
			
		||||
            return;
 | 
			
		||||
        if (not dosguard_.get().isOk(connectionMetadata.ip())) {
 | 
			
		||||
            return makeSlowDownResponse(request, std::nullopt);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            auto req = boost::json::parse(request).as_object();
 | 
			
		||||
            LOG(perfLog_.debug()) << connection->tag() << "Adding to work queue";
 | 
			
		||||
        std::optional<Response> response;
 | 
			
		||||
        util::CoroutineGroup coroutineGroup{yield, 1};
 | 
			
		||||
        auto const onTaskComplete = coroutineGroup.registerForeign(yield);
 | 
			
		||||
        ASSERT(onTaskComplete.has_value(), "Coroutine group can't be full");
 | 
			
		||||
 | 
			
		||||
            if (not connection->upgraded and shouldReplaceParams(req))
 | 
			
		||||
                req[JS(params)] = boost::json::array({boost::json::object{}});
 | 
			
		||||
        bool const postSuccessful = rpcEngine_->post(
 | 
			
		||||
            [this,
 | 
			
		||||
             &request,
 | 
			
		||||
             &response,
 | 
			
		||||
             &onTaskComplete = onTaskComplete.value(),
 | 
			
		||||
             &connectionMetadata,
 | 
			
		||||
             subscriptionContext = std::move(subscriptionContext)](boost::asio::yield_context innerYield) mutable {
 | 
			
		||||
                try {
 | 
			
		||||
                    boost::system::error_code ec;
 | 
			
		||||
                    auto parsedRequest = boost::json::parse(request.message(), ec);
 | 
			
		||||
                    if (ec.failed() or not parsedRequest.is_object()) {
 | 
			
		||||
                        rpcEngine_->notifyBadSyntax();
 | 
			
		||||
                        response = impl::ErrorHelper{request}.makeJsonParsingError();
 | 
			
		||||
                        if (ec.failed()) {
 | 
			
		||||
                            LOG(log_.warn())
 | 
			
		||||
                                << "Error parsing JSON: " << ec.message() << ". For request: " << request.message();
 | 
			
		||||
                        } else {
 | 
			
		||||
                            LOG(log_.warn()) << "Received not a JSON object. For request: " << request.message();
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        auto parsedObject = std::move(parsedRequest).as_object();
 | 
			
		||||
 | 
			
		||||
            if (not dosguard_.get().request(connection->clientIp, req)) {
 | 
			
		||||
                connection->sendSlowDown(request);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
                        if (not dosguard_.get().request(connectionMetadata.ip(), parsedObject)) {
 | 
			
		||||
                            response = makeSlowDownResponse(request, parsedObject);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            LOG(perfLog_.debug()) << connectionMetadata.tag() << "Adding to work queue";
 | 
			
		||||
 | 
			
		||||
            if (!rpcEngine_->post(
 | 
			
		||||
                    [this, request = std::move(req), connection](boost::asio::yield_context yield) mutable {
 | 
			
		||||
                        handleRequest(yield, std::move(request), connection);
 | 
			
		||||
                    },
 | 
			
		||||
                    connection->clientIp
 | 
			
		||||
                )) {
 | 
			
		||||
                rpcEngine_->notifyTooBusy();
 | 
			
		||||
                web::impl::ErrorHelper(connection).sendTooBusyError();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (boost::system::system_error const& ex) {
 | 
			
		||||
            // system_error thrown when json parsing failed
 | 
			
		||||
            rpcEngine_->notifyBadSyntax();
 | 
			
		||||
            web::impl::ErrorHelper(connection).sendJsonParsingError();
 | 
			
		||||
            LOG(log_.warn()) << "Error parsing JSON: " << ex.what() << ". For request: " << request;
 | 
			
		||||
        } catch (std::invalid_argument const& ex) {
 | 
			
		||||
            // thrown when json parses something that is not an object at top level
 | 
			
		||||
            rpcEngine_->notifyBadSyntax();
 | 
			
		||||
            LOG(log_.warn()) << "Invalid argument error: " << ex.what() << ". For request: " << request;
 | 
			
		||||
            web::impl::ErrorHelper(connection).sendJsonParsingError();
 | 
			
		||||
        } catch (std::exception const& ex) {
 | 
			
		||||
            LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
 | 
			
		||||
            rpcEngine_->notifyInternalError();
 | 
			
		||||
            throw;
 | 
			
		||||
                            if (not connectionMetadata.wasUpgraded() and shouldReplaceParams(parsedObject))
 | 
			
		||||
                                parsedObject[JS(params)] = boost::json::array({boost::json::object{}});
 | 
			
		||||
 | 
			
		||||
                            response = handleRequest(
 | 
			
		||||
                                innerYield,
 | 
			
		||||
                                request,
 | 
			
		||||
                                std::move(parsedObject),
 | 
			
		||||
                                connectionMetadata,
 | 
			
		||||
                                std::move(subscriptionContext)
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (std::exception const& ex) {
 | 
			
		||||
                    LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
 | 
			
		||||
                    rpcEngine_->notifyInternalError();
 | 
			
		||||
                    response = impl::ErrorHelper{request}.makeInternalError();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // notify the coroutine group that the foreign task is done
 | 
			
		||||
                onTaskComplete();
 | 
			
		||||
            },
 | 
			
		||||
            connectionMetadata.ip()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (not postSuccessful) {
 | 
			
		||||
            // onTaskComplete must be called to notify coroutineGroup that the foreign task is done
 | 
			
		||||
            onTaskComplete->operator()();
 | 
			
		||||
            rpcEngine_->notifyTooBusy();
 | 
			
		||||
            return impl::ErrorHelper{request}.makeTooBusyError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Put the coroutine to sleep until the foreign task is done
 | 
			
		||||
        coroutineGroup.asyncWait(yield);
 | 
			
		||||
        ASSERT(response.has_value(), "Woke up coroutine without setting response");
 | 
			
		||||
 | 
			
		||||
        if (not dosguard_.get().add(connectionMetadata.ip(), response->message().size())) {
 | 
			
		||||
            response->setMessage(makeLoadWarning(*response));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return std::move(response).value();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    Response
 | 
			
		||||
    handleRequest(
 | 
			
		||||
        boost::asio::yield_context yield,
 | 
			
		||||
        Request const& rawRequest,
 | 
			
		||||
        boost::json::object&& request,
 | 
			
		||||
        std::shared_ptr<web::ConnectionBase> const& connection
 | 
			
		||||
        ConnectionMetadata const& connectionMetadata,
 | 
			
		||||
        SubscriptionContextPtr subscriptionContext
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        LOG(log_.info()) << connection->tag() << (connection->upgraded ? "ws" : "http")
 | 
			
		||||
        LOG(log_.info()) << connectionMetadata.tag() << (connectionMetadata.wasUpgraded() ? "ws" : "http")
 | 
			
		||||
                         << " received request from work queue: " << util::removeSecret(request)
 | 
			
		||||
                         << " ip = " << connection->clientIp;
 | 
			
		||||
                         << " ip = " << connectionMetadata.ip();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            auto const range = backend_->fetchLedgerRange();
 | 
			
		||||
            if (!range) {
 | 
			
		||||
                // for error that happened before the handler, we don't attach any warnings
 | 
			
		||||
                rpcEngine_->notifyNotReady();
 | 
			
		||||
                web::impl::ErrorHelper(connection, std::move(request)).sendNotReadyError();
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
                return impl::ErrorHelper{rawRequest, std::move(request)}.makeNotReadyError();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto const context = [&] {
 | 
			
		||||
                if (connection->upgraded) {
 | 
			
		||||
                if (connectionMetadata.wasUpgraded()) {
 | 
			
		||||
                    ASSERT(subscriptionContext != nullptr, "Subscription context must exist for a WS connection");
 | 
			
		||||
                    return rpc::makeWsContext(
 | 
			
		||||
                        yield,
 | 
			
		||||
                        request,
 | 
			
		||||
                        connection->makeSubscriptionContext(tagFactory_),
 | 
			
		||||
                        tagFactory_.with(connection->tag()),
 | 
			
		||||
                        std::move(subscriptionContext),
 | 
			
		||||
                        tagFactory_.with(connectionMetadata.tag()),
 | 
			
		||||
                        *range,
 | 
			
		||||
                        connection->clientIp,
 | 
			
		||||
                        connectionMetadata.ip(),
 | 
			
		||||
                        std::cref(apiVersionParser_),
 | 
			
		||||
                        connection->isAdmin()
 | 
			
		||||
                        connectionMetadata.isAdmin()
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                return rpc::makeHttpContext(
 | 
			
		||||
                    yield,
 | 
			
		||||
                    request,
 | 
			
		||||
                    tagFactory_.with(connection->tag()),
 | 
			
		||||
                    tagFactory_.with(connectionMetadata.tag()),
 | 
			
		||||
                    *range,
 | 
			
		||||
                    connection->clientIp,
 | 
			
		||||
                    connectionMetadata.ip(),
 | 
			
		||||
                    std::cref(apiVersionParser_),
 | 
			
		||||
                    connection->isAdmin()
 | 
			
		||||
                    connectionMetadata.isAdmin()
 | 
			
		||||
                );
 | 
			
		||||
            }();
 | 
			
		||||
 | 
			
		||||
            if (!context) {
 | 
			
		||||
                auto const err = context.error();
 | 
			
		||||
                LOG(perfLog_.warn()) << connection->tag() << "Could not create Web context: " << err;
 | 
			
		||||
                LOG(log_.warn()) << connection->tag() << "Could not create Web context: " << err;
 | 
			
		||||
                LOG(perfLog_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
 | 
			
		||||
                LOG(log_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
 | 
			
		||||
 | 
			
		||||
                // we count all those as BadSyntax - as the WS path would.
 | 
			
		||||
                // Although over HTTP these will yield a 400 status with a plain text response (for most).
 | 
			
		||||
                rpcEngine_->notifyBadSyntax();
 | 
			
		||||
                web::impl::ErrorHelper(connection, std::move(request)).sendError(err);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
                return impl::ErrorHelper(rawRequest, std::move(request)).makeError(err);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
 | 
			
		||||
 | 
			
		||||
            auto const us = std::chrono::duration<int, std::milli>(timeDiff);
 | 
			
		||||
            auto us = std::chrono::duration<int, std::milli>(timeDiff);
 | 
			
		||||
            rpc::logDuration(request, context->tag(), us);
 | 
			
		||||
 | 
			
		||||
            boost::json::object response;
 | 
			
		||||
 | 
			
		||||
            if (!result.response.has_value()) {
 | 
			
		||||
                // note: error statuses are counted/notified in buildResponse itself
 | 
			
		||||
                response = web::impl::ErrorHelper(connection, request).composeError(result.response.error());
 | 
			
		||||
                response = impl::ErrorHelper(rawRequest, request).composeError(result.response.error());
 | 
			
		||||
                auto const responseStr = boost::json::serialize(response);
 | 
			
		||||
 | 
			
		||||
                LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
 | 
			
		||||
@@ -237,7 +285,7 @@ private:
 | 
			
		||||
                // if the result is forwarded - just use it as is
 | 
			
		||||
                // if forwarded request has error, for http, error should be in "result"; for ws, error should
 | 
			
		||||
                // be at top
 | 
			
		||||
                if (isForwarded && (json.contains(JS(result)) || connection->upgraded)) {
 | 
			
		||||
                if (isForwarded && (json.contains(JS(result)) || connectionMetadata.wasUpgraded())) {
 | 
			
		||||
                    for (auto const& [k, v] : json)
 | 
			
		||||
                        response.insert_or_assign(k, v);
 | 
			
		||||
                } else {
 | 
			
		||||
@@ -249,7 +297,7 @@ private:
 | 
			
		||||
 | 
			
		||||
                // for ws there is an additional field "status" in the response,
 | 
			
		||||
                // otherwise the "status" is in the "result" field
 | 
			
		||||
                if (connection->upgraded) {
 | 
			
		||||
                if (connectionMetadata.wasUpgraded()) {
 | 
			
		||||
                    auto const appendFieldIfExist = [&](auto const& field) {
 | 
			
		||||
                        if (request.contains(field) and not request.at(field).is_null())
 | 
			
		||||
                            response[field] = request.at(field);
 | 
			
		||||
@@ -275,20 +323,51 @@ private:
 | 
			
		||||
                warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
 | 
			
		||||
 | 
			
		||||
            response["warnings"] = warnings;
 | 
			
		||||
            connection->send(boost::json::serialize(response));
 | 
			
		||||
            return Response{boost::beast::http::status::ok, response, rawRequest};
 | 
			
		||||
        } catch (std::exception const& ex) {
 | 
			
		||||
            // note: while we are catching this in buildResponse too, this is here to make sure
 | 
			
		||||
            // that any other code that may throw is outside of buildResponse is also worked around.
 | 
			
		||||
            LOG(perfLog_.error()) << connection->tag() << "Caught exception: " << ex.what();
 | 
			
		||||
            LOG(log_.error()) << connection->tag() << "Caught exception: " << ex.what();
 | 
			
		||||
            LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
 | 
			
		||||
            LOG(log_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
 | 
			
		||||
 | 
			
		||||
            rpcEngine_->notifyInternalError();
 | 
			
		||||
            web::impl::ErrorHelper(connection, std::move(request)).sendInternalError();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
            return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static Response
 | 
			
		||||
    makeSlowDownResponse(Request const& request, std::optional<boost::json::value> requestJson)
 | 
			
		||||
    {
 | 
			
		||||
        auto error = rpc::makeError(rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
 | 
			
		||||
        if (not request.isHttp()) {
 | 
			
		||||
            try {
 | 
			
		||||
                if (not requestJson.has_value()) {
 | 
			
		||||
                    requestJson = boost::json::parse(request.message());
 | 
			
		||||
                }
 | 
			
		||||
                if (requestJson->is_object() && requestJson->as_object().contains("id"))
 | 
			
		||||
                    error["id"] = requestJson->as_object().at("id");
 | 
			
		||||
                error["request"] = request.message();
 | 
			
		||||
            } catch (std::exception const&) {
 | 
			
		||||
                error["request"] = request.message();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return web::Response{boost::beast::http::status::service_unavailable, error, request};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static boost::json::object
 | 
			
		||||
    makeLoadWarning(Response const& response)
 | 
			
		||||
    {
 | 
			
		||||
        auto jsonResponse = boost::json::parse(response.message()).as_object();
 | 
			
		||||
        jsonResponse["warning"] = "load";
 | 
			
		||||
        if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
 | 
			
		||||
            jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
 | 
			
		||||
        } else {
 | 
			
		||||
            jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
 | 
			
		||||
        }
 | 
			
		||||
        return jsonResponse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bool
 | 
			
		||||
    shouldReplaceParams(boost::json::object const& req) const
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/OverloadSet.hpp"
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -142,4 +142,4 @@ Request::httpRequest() const
 | 
			
		||||
    return std::get<HttpRequest>(data_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -29,7 +29,7 @@
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Represents an HTTP or WebSocket request.
 | 
			
		||||
@@ -150,4 +150,4 @@ private:
 | 
			
		||||
    httpRequest() const;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -17,13 +17,13 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/OverloadSet.hpp"
 | 
			
		||||
#include "util/build/Build.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
@@ -43,7 +43,7 @@
 | 
			
		||||
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -193,4 +193,4 @@ Response::asWsResponse() const&
 | 
			
		||||
    return boost::asio::buffer(message.data(), message.size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -19,7 +19,7 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
class Connection;
 | 
			
		||||
 | 
			
		||||
@@ -133,4 +133,4 @@ public:
 | 
			
		||||
    asWsResponse() const&;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -17,19 +17,19 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Server.hpp"
 | 
			
		||||
#include "web/Server.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ObjectView.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/MessageHandler.hpp"
 | 
			
		||||
#include "web/ng/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/ng/impl/ServerSslContext.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/MessageHandler.hpp"
 | 
			
		||||
#include "web/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/impl/ServerSslContext.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/detached.hpp>
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
@@ -54,7 +54,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -316,7 +316,7 @@ Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield
 | 
			
		||||
        boost::asio::spawn(
 | 
			
		||||
            ctx_.get(),
 | 
			
		||||
            [connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) {
 | 
			
		||||
                web::ng::impl::ConnectionHandler::stopConnection(*connection, yield);
 | 
			
		||||
                web::impl::ConnectionHandler::stopConnection(*connection, yield);
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
@@ -382,4 +382,4 @@ makeServer(
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
@@ -20,363 +20,173 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/HttpSession.hpp"
 | 
			
		||||
#include "web/SslHttpSession.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/interface/Concepts.hpp"
 | 
			
		||||
#include "web/ng/impl/ServerSslContext.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/MessageHandler.hpp"
 | 
			
		||||
#include "web/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/ConnectionHandler.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/ip/address.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/socket_base.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
#include <boost/asio/ssl/error.hpp>
 | 
			
		||||
#include <boost/asio/strand.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/stream_traits.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <fmt/core.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <concepts>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <stdexcept>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief This namespace implements the web server and related components.
 | 
			
		||||
 *
 | 
			
		||||
 * The web server is leveraging the power of `boost::asio` with it's coroutine support thru `boost::asio::yield_context`
 | 
			
		||||
 * and `boost::asio::spawn`.
 | 
			
		||||
 *
 | 
			
		||||
 * Majority of the code is based on examples that came with boost.
 | 
			
		||||
 */
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The Detector class to detect if the connection is a ssl or not.
 | 
			
		||||
 *
 | 
			
		||||
 * If it is an SSL connection, the Detector will pass the ownership of the socket to SslSessionType, otherwise to
 | 
			
		||||
 * PlainSessionType.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam PlainSessionType The plain session type
 | 
			
		||||
 * @tparam SslSessionType The SSL session type
 | 
			
		||||
 * @tparam HandlerType The executor to handle the requests
 | 
			
		||||
 * @brief A tag class for server to help identify Server in templated code.
 | 
			
		||||
 */
 | 
			
		||||
template <
 | 
			
		||||
    template <typename> class PlainSessionType,
 | 
			
		||||
    template <typename> class SslSessionType,
 | 
			
		||||
    SomeServerHandler HandlerType>
 | 
			
		||||
class Detector : public std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>> {
 | 
			
		||||
    using std::enable_shared_from_this<Detector<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
 | 
			
		||||
 | 
			
		||||
    util::Logger log_{"WebServer"};
 | 
			
		||||
    boost::beast::tcp_stream stream_;
 | 
			
		||||
    std::optional<std::reference_wrapper<boost::asio::ssl::context>> ctx_;
 | 
			
		||||
    std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> const dosGuard_;
 | 
			
		||||
    std::shared_ptr<HandlerType> const handler_;
 | 
			
		||||
    boost::beast::flat_buffer buffer_;
 | 
			
		||||
    std::shared_ptr<AdminVerificationStrategy> const adminVerification_;
 | 
			
		||||
    std::uint32_t maxWsSendingQueueSize_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new detector.
 | 
			
		||||
     *
 | 
			
		||||
     * @param socket The socket. Ownership is transferred
 | 
			
		||||
     * @param ctx The SSL context if any
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param adminVerification The admin verification strategy to use
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    Detector(
 | 
			
		||||
        tcp::socket&& socket,
 | 
			
		||||
        std::optional<std::reference_wrapper<boost::asio::ssl::context>> ctx,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> handler,
 | 
			
		||||
        std::shared_ptr<AdminVerificationStrategy> adminVerification,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : stream_(std::move(socket))
 | 
			
		||||
        , ctx_(ctx)
 | 
			
		||||
        , tagFactory_(std::cref(tagFactory))
 | 
			
		||||
        , dosGuard_(dosGuard)
 | 
			
		||||
        , handler_(std::move(handler))
 | 
			
		||||
        , adminVerification_(std::move(adminVerification))
 | 
			
		||||
        , maxWsSendingQueueSize_(maxWsSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A helper function that is called when any error occurs.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ec The error code
 | 
			
		||||
     * @param message The message to include in the log
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    fail(boost::system::error_code ec, char const* message)
 | 
			
		||||
    {
 | 
			
		||||
        if (ec == boost::asio::ssl::error::stream_truncated)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        LOG(log_.info()) << "Detector failed (" << message << "): " << ec.message();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Initiate the detection. */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
 | 
			
		||||
        async_detect_ssl(stream_, buffer_, boost::beast::bind_front_handler(&Detector::onDetect, shared_from_this()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Handles detection result.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ec The error code
 | 
			
		||||
     * @param result true if SSL is detected; false otherwise
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onDetect(boost::beast::error_code ec, bool result)
 | 
			
		||||
    {
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return fail(ec, "detect");
 | 
			
		||||
 | 
			
		||||
        std::string ip;
 | 
			
		||||
        try {
 | 
			
		||||
            ip = stream_.socket().remote_endpoint().address().to_string();
 | 
			
		||||
        } catch (std::exception const&) {
 | 
			
		||||
            return fail(ec, "cannot get remote endpoint");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (result) {
 | 
			
		||||
            if (!ctx_)
 | 
			
		||||
                return fail(ec, "SSL is not supported by this server");
 | 
			
		||||
 | 
			
		||||
            std::make_shared<SslSessionType<HandlerType>>(
 | 
			
		||||
                stream_.release_socket(),
 | 
			
		||||
                ip,
 | 
			
		||||
                adminVerification_,
 | 
			
		||||
                *ctx_,
 | 
			
		||||
                tagFactory_,
 | 
			
		||||
                dosGuard_,
 | 
			
		||||
                handler_,
 | 
			
		||||
                std::move(buffer_),
 | 
			
		||||
                maxWsSendingQueueSize_
 | 
			
		||||
            )
 | 
			
		||||
                ->run();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        std::make_shared<PlainSessionType<HandlerType>>(
 | 
			
		||||
            stream_.release_socket(),
 | 
			
		||||
            ip,
 | 
			
		||||
            adminVerification_,
 | 
			
		||||
            tagFactory_,
 | 
			
		||||
            dosGuard_,
 | 
			
		||||
            handler_,
 | 
			
		||||
            std::move(buffer_),
 | 
			
		||||
            maxWsSendingQueueSize_
 | 
			
		||||
        )
 | 
			
		||||
            ->run();
 | 
			
		||||
    }
 | 
			
		||||
struct ServerTag {
 | 
			
		||||
    virtual ~ServerTag() = default;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The WebServer class. It creates server socket and start listening on it.
 | 
			
		||||
 *
 | 
			
		||||
 * Once there is client connection, it will accept it and pass the socket to Detector to detect ssl or plain.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam PlainSessionType The plain session to handle non-ssl connection.
 | 
			
		||||
 * @tparam SslSessionType The SSL session to handle SSL connection.
 | 
			
		||||
 * @tparam HandlerType The handler to process the request and return response.
 | 
			
		||||
 */
 | 
			
		||||
template <
 | 
			
		||||
    template <typename> class PlainSessionType,
 | 
			
		||||
    template <typename> class SslSessionType,
 | 
			
		||||
    SomeServerHandler HandlerType>
 | 
			
		||||
class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>> {
 | 
			
		||||
    using std::enable_shared_from_this<Server<PlainSessionType, SslSessionType, HandlerType>>::shared_from_this;
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept SomeServer = std::derived_from<T, ServerTag>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Web server class.
 | 
			
		||||
 */
 | 
			
		||||
class Server : public ServerTag {
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Check to perform for each new client connection. The check takes client ip as input and returns a Response
 | 
			
		||||
     * if the check failed. Response will be sent to the client and the connection will be closed.
 | 
			
		||||
     */
 | 
			
		||||
    using OnConnectCheck = std::function<std::expected<void, Response>(Connection const&)>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Hook called when any connection disconnects
 | 
			
		||||
     */
 | 
			
		||||
    using OnDisconnectHook = impl::ConnectionHandler::OnDisconnectHook;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    util::Logger log_{"WebServer"};
 | 
			
		||||
    std::reference_wrapper<boost::asio::io_context> ioc_;
 | 
			
		||||
    std::optional<boost::asio::ssl::context> ctx_;
 | 
			
		||||
    util::TagDecoratorFactory tagFactory_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
 | 
			
		||||
    std::shared_ptr<HandlerType> handler_;
 | 
			
		||||
    tcp::acceptor acceptor_;
 | 
			
		||||
    std::shared_ptr<AdminVerificationStrategy> adminVerification_;
 | 
			
		||||
    std::uint32_t maxWsSendingQueueSize_;
 | 
			
		||||
    util::Logger perfLog_{"Performance"};
 | 
			
		||||
 | 
			
		||||
    std::reference_wrapper<boost::asio::io_context> ctx_;
 | 
			
		||||
    std::optional<boost::asio::ssl::context> sslContext_;
 | 
			
		||||
 | 
			
		||||
    util::TagDecoratorFactory tagDecoratorFactory_;
 | 
			
		||||
 | 
			
		||||
    impl::ConnectionHandler connectionHandler_;
 | 
			
		||||
    boost::asio::ip::tcp::endpoint endpoint_;
 | 
			
		||||
 | 
			
		||||
    OnConnectCheck onConnectCheck_;
 | 
			
		||||
 | 
			
		||||
    bool running_{false};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new instance of the web server.
 | 
			
		||||
     * @brief Construct a new Server object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ioc The io_context to run the server on
 | 
			
		||||
     * @param ctx The SSL context if any
 | 
			
		||||
     * @param endpoint The endpoint to listen on
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param adminVerification The admin verification strategy to use
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     * @param ctx The boost::asio::io_context to use.
 | 
			
		||||
     * @param endpoint The endpoint to listen on.
 | 
			
		||||
     * @param sslContext The SSL context to use (optional).
 | 
			
		||||
     * @param processingPolicy The requests processing policy (parallel or sequential).
 | 
			
		||||
     * @param parallelRequestLimit The limit of requests for one connection that can be processed in parallel. Only used
 | 
			
		||||
     * if processingPolicy is parallel.
 | 
			
		||||
     * @param tagDecoratorFactory The tag decorator factory.
 | 
			
		||||
     * @param maxSubscriptionSendQueueSize The maximum size of the subscription send queue.
 | 
			
		||||
     * @param onConnectCheck The check to perform on each connection.
 | 
			
		||||
     * @param onDisconnectHook The hook to call on each disconnection.
 | 
			
		||||
     */
 | 
			
		||||
    Server(
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::optional<boost::asio::ssl::context> ctx,
 | 
			
		||||
        tcp::endpoint endpoint,
 | 
			
		||||
        util::TagDecoratorFactory tagFactory,
 | 
			
		||||
        dosguard::DOSGuardInterface& dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> handler,
 | 
			
		||||
        std::shared_ptr<AdminVerificationStrategy> adminVerification,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : ioc_(std::ref(ioc))
 | 
			
		||||
        , ctx_(std::move(ctx))
 | 
			
		||||
        , tagFactory_(tagFactory)
 | 
			
		||||
        , dosGuard_(std::ref(dosGuard))
 | 
			
		||||
        , handler_(std::move(handler))
 | 
			
		||||
        , acceptor_(boost::asio::make_strand(ioc))
 | 
			
		||||
        , adminVerification_(std::move(adminVerification))
 | 
			
		||||
        , maxWsSendingQueueSize_(maxWsSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::error_code ec;
 | 
			
		||||
        boost::asio::io_context& ctx,
 | 
			
		||||
        boost::asio::ip::tcp::endpoint endpoint,
 | 
			
		||||
        std::optional<boost::asio::ssl::context> sslContext,
 | 
			
		||||
        ProcessingPolicy processingPolicy,
 | 
			
		||||
        std::optional<size_t> parallelRequestLimit,
 | 
			
		||||
        util::TagDecoratorFactory tagDecoratorFactory,
 | 
			
		||||
        std::optional<size_t> maxSubscriptionSendQueueSize,
 | 
			
		||||
        OnConnectCheck onConnectCheck,
 | 
			
		||||
        OnDisconnectHook onDisconnectHook
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        acceptor_.open(endpoint.protocol(), ec);
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return;
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Copy constructor is deleted. The Server couldn't be copied.
 | 
			
		||||
     */
 | 
			
		||||
    Server(Server const&) = delete;
 | 
			
		||||
 | 
			
		||||
        acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec);
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return;
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Move constructor is deleted because connectionHandler_ contains references to some fields of the Server.
 | 
			
		||||
     */
 | 
			
		||||
    Server(Server&&) = delete;
 | 
			
		||||
 | 
			
		||||
        acceptor_.bind(endpoint, ec);
 | 
			
		||||
        if (ec) {
 | 
			
		||||
            LOG(log_.error()) << "Failed to bind to endpoint: " << endpoint << ". message: " << ec.message();
 | 
			
		||||
            throw std::runtime_error(
 | 
			
		||||
                fmt::format("Failed to bind to endpoint: {}:{}", endpoint.address().to_string(), endpoint.port())
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec);
 | 
			
		||||
        if (ec) {
 | 
			
		||||
            LOG(log_.error()) << "Failed to listen at endpoint: " << endpoint << ". message: " << ec.message();
 | 
			
		||||
            throw std::runtime_error(
 | 
			
		||||
                fmt::format("Failed to listen at endpoint: {}:{}", endpoint.address().to_string(), endpoint.port())
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Start accepting incoming connections. */
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set handler for GET requests.
 | 
			
		||||
     * @note This method can't be called after run() is called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param target The target of the request.
 | 
			
		||||
     * @param handler The handler to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        doAccept();
 | 
			
		||||
    }
 | 
			
		||||
    onGet(std::string const& target, MessageHandler handler);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set handler for POST requests.
 | 
			
		||||
     * @note This method can't be called after run() is called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param target The target of the request.
 | 
			
		||||
     * @param handler The handler to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onPost(std::string const& target, MessageHandler handler);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set handler for WebSocket requests.
 | 
			
		||||
     * @note This method can't be called after run() is called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param handler The handler to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onWs(MessageHandler handler);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Run the server.
 | 
			
		||||
     *
 | 
			
		||||
     * @return std::nullopt if the server started successfully, otherwise an error message.
 | 
			
		||||
     */
 | 
			
		||||
    std::optional<std::string>
 | 
			
		||||
    run();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Stop the server. This method will asynchronously sleep unless all the users are disconnected.
 | 
			
		||||
     * @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield The coroutine context.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    stop(boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    doAccept()
 | 
			
		||||
    {
 | 
			
		||||
        acceptor_.async_accept(
 | 
			
		||||
            boost::asio::make_strand(ioc_.get()),
 | 
			
		||||
            boost::beast::bind_front_handler(&Server::onAccept, shared_from_this())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onAccept(boost::beast::error_code ec, tcp::socket socket)
 | 
			
		||||
    {
 | 
			
		||||
        if (!ec) {
 | 
			
		||||
            auto ctxRef =
 | 
			
		||||
                ctx_ ? std::optional<std::reference_wrapper<boost::asio::ssl::context>>{ctx_.value()} : std::nullopt;
 | 
			
		||||
 | 
			
		||||
            std::make_shared<Detector<PlainSessionType, SslSessionType, HandlerType>>(
 | 
			
		||||
                std::move(socket),
 | 
			
		||||
                ctxRef,
 | 
			
		||||
                std::cref(tagFactory_),
 | 
			
		||||
                dosGuard_,
 | 
			
		||||
                handler_,
 | 
			
		||||
                adminVerification_,
 | 
			
		||||
                maxWsSendingQueueSize_
 | 
			
		||||
            )
 | 
			
		||||
                ->run();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        doAccept();
 | 
			
		||||
    }
 | 
			
		||||
    handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @brief The final type of the HttpServer used by Clio. */
 | 
			
		||||
template <typename HandlerType>
 | 
			
		||||
using HttpServer = Server<HttpSession, SslHttpSession, HandlerType>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A factory function that spawns a ready to use HTTP server.
 | 
			
		||||
 * @brief Create a new Server.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam HandlerType The type of handler to process the request
 | 
			
		||||
 * @param config The config to create server
 | 
			
		||||
 * @param ioc The server will run under this io_context
 | 
			
		||||
 * @param dosGuard The dos guard to protect the server
 | 
			
		||||
 * @param handler The handler to process the request
 | 
			
		||||
 * @return The server instance
 | 
			
		||||
 * @param config The configuration.
 | 
			
		||||
 * @param onConnectCheck The check to perform on each client connection.
 | 
			
		||||
 * @param onDisconnectHook The hook to call when client disconnects.
 | 
			
		||||
 * @param context The boost::asio::io_context to use.
 | 
			
		||||
 *
 | 
			
		||||
 * @return The Server or an error message.
 | 
			
		||||
 */
 | 
			
		||||
template <typename HandlerType>
 | 
			
		||||
static std::shared_ptr<HttpServer<HandlerType>>
 | 
			
		||||
makeHttpServer(
 | 
			
		||||
std::expected<Server, std::string>
 | 
			
		||||
makeServer(
 | 
			
		||||
    util::config::ClioConfigDefinition const& config,
 | 
			
		||||
    boost::asio::io_context& ioc,
 | 
			
		||||
    dosguard::DOSGuardInterface& dosGuard,
 | 
			
		||||
    std::shared_ptr<HandlerType> const& handler
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    static util::Logger const log{"WebServer"};  // NOLINT(readability-identifier-naming)
 | 
			
		||||
 | 
			
		||||
    auto expectedSslContext = ng::impl::makeServerSslContext(config);
 | 
			
		||||
    if (not expectedSslContext) {
 | 
			
		||||
        LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error();
 | 
			
		||||
        return nullptr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto const serverConfig = config.getObject("server");
 | 
			
		||||
    auto const address = boost::asio::ip::make_address(serverConfig.get<std::string>("ip"));
 | 
			
		||||
    auto const port = serverConfig.get<unsigned short>("port");
 | 
			
		||||
 | 
			
		||||
    auto expectedAdminVerification = makeAdminVerificationStrategy(config);
 | 
			
		||||
    if (not expectedAdminVerification.has_value()) {
 | 
			
		||||
        LOG(log.error()) << expectedAdminVerification.error();
 | 
			
		||||
        throw std::logic_error{expectedAdminVerification.error()};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the transactions number is 200 per ledger, A client which subscribes everything will send 400+ feeds for
 | 
			
		||||
    // each ledger. we allow user delay 3 ledgers by default
 | 
			
		||||
    auto const maxWsSendingQueueSize = serverConfig.get<uint32_t>("ws_max_sending_queue_size");
 | 
			
		||||
 | 
			
		||||
    auto server = std::make_shared<HttpServer<HandlerType>>(
 | 
			
		||||
        ioc,
 | 
			
		||||
        std::move(expectedSslContext).value(),
 | 
			
		||||
        boost::asio::ip::tcp::endpoint{address, port},
 | 
			
		||||
        util::TagDecoratorFactory(config),
 | 
			
		||||
        dosGuard,
 | 
			
		||||
        handler,
 | 
			
		||||
        std::move(expectedAdminVerification).value(),
 | 
			
		||||
        maxWsSendingQueueSize
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    server->run();
 | 
			
		||||
    return server;
 | 
			
		||||
}
 | 
			
		||||
    Server::OnConnectCheck onConnectCheck,
 | 
			
		||||
    Server::OnDisconnectHook onDisconnectHook,
 | 
			
		||||
    boost::asio::io_context& context
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
}  // namespace web
 | 
			
		||||
 
 | 
			
		||||
@@ -1,188 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/SslWsSession.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/impl/HttpBase.hpp"
 | 
			
		||||
#include "web/interface/Concepts.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
#include <boost/asio/ssl/stream_base.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/stream_traits.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/ssl/ssl_stream.hpp>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
using tcp = boost::asio::ip::tcp;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Represents a HTTPS connection established by a client.
 | 
			
		||||
 *
 | 
			
		||||
 * It will handle the upgrade to secure websocket, pass the ownership of the socket to the upgrade session.
 | 
			
		||||
 * Otherwise, it will pass control to the base class.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam HandlerType The type of the server handler to use
 | 
			
		||||
 */
 | 
			
		||||
template <SomeServerHandler HandlerType>
 | 
			
		||||
class SslHttpSession : public impl::HttpBase<SslHttpSession, HandlerType>,
 | 
			
		||||
                       public std::enable_shared_from_this<SslHttpSession<HandlerType>> {
 | 
			
		||||
    boost::beast::ssl_stream<boost::beast::tcp_stream> stream_;
 | 
			
		||||
    std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
 | 
			
		||||
    std::uint32_t maxWsSendingQueueSize_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new SSL session.
 | 
			
		||||
     *
 | 
			
		||||
     * @param socket The socket. Ownership is transferred to HttpSession
 | 
			
		||||
     * @param ip Client's IP address
 | 
			
		||||
     * @param adminVerification The admin verification strategy to use
 | 
			
		||||
     * @param ctx The SSL context
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param buffer Buffer with initial data received from the peer
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    explicit SslHttpSession(
 | 
			
		||||
        tcp::socket&& socket,
 | 
			
		||||
        std::string const& ip,
 | 
			
		||||
        std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
 | 
			
		||||
        boost::asio::ssl::context& ctx,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> const& handler,
 | 
			
		||||
        boost::beast::flat_buffer buffer,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : impl::HttpBase<SslHttpSession, HandlerType>(
 | 
			
		||||
              ip,
 | 
			
		||||
              tagFactory,
 | 
			
		||||
              adminVerification,
 | 
			
		||||
              dosGuard,
 | 
			
		||||
              handler,
 | 
			
		||||
              std::move(buffer)
 | 
			
		||||
          )
 | 
			
		||||
        , stream_(std::move(socket), ctx)
 | 
			
		||||
        , tagFactory_(tagFactory)
 | 
			
		||||
        , maxWsSendingQueueSize_(maxWsSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~SslHttpSession() override = default;
 | 
			
		||||
 | 
			
		||||
    /** @return The SSL stream. */
 | 
			
		||||
    boost::beast::ssl_stream<boost::beast::tcp_stream>&
 | 
			
		||||
    stream()
 | 
			
		||||
    {
 | 
			
		||||
        return stream_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Initiates the handshake. */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        auto self = this->shared_from_this();
 | 
			
		||||
        boost::asio::dispatch(stream_.get_executor(), [self]() {
 | 
			
		||||
            // Set the timeout.
 | 
			
		||||
            boost::beast::get_lowest_layer(self->stream()).expires_after(std::chrono::seconds(30));
 | 
			
		||||
 | 
			
		||||
            // Perform the SSL handshake
 | 
			
		||||
            // Note, this is the buffered version of the handshake.
 | 
			
		||||
            self->stream_.async_handshake(
 | 
			
		||||
                boost::asio::ssl::stream_base::server,
 | 
			
		||||
                self->buffer_.data(),
 | 
			
		||||
                boost::beast::bind_front_handler(&SslHttpSession<HandlerType>::onHandshake, self)
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Handles the handshake.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ec Error code if any
 | 
			
		||||
     * @param bytesUsed The total amount of data read from the stream
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onHandshake(boost::beast::error_code ec, std::size_t bytesUsed)
 | 
			
		||||
    {
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return this->httpFail(ec, "handshake");
 | 
			
		||||
 | 
			
		||||
        this->buffer_.consume(bytesUsed);
 | 
			
		||||
        this->doRead();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Closes the underlying connection. */
 | 
			
		||||
    void
 | 
			
		||||
    doClose()
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
 | 
			
		||||
        stream_.async_shutdown(boost::beast::bind_front_handler(&SslHttpSession::onShutdown, this->shared_from_this()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Handles a connection shutdown.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ec Error code if any
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onShutdown(boost::beast::error_code ec)
 | 
			
		||||
    {
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return this->httpFail(ec, "shutdown");
 | 
			
		||||
        // At this point the connection is closed gracefully
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @brief Upgrades connection to secure websocket. */
 | 
			
		||||
    void
 | 
			
		||||
    upgrade()
 | 
			
		||||
    {
 | 
			
		||||
        std::make_shared<SslWsUpgrader<HandlerType>>(
 | 
			
		||||
            std::move(stream_),
 | 
			
		||||
            this->clientIp,
 | 
			
		||||
            tagFactory_,
 | 
			
		||||
            this->dosGuard_,
 | 
			
		||||
            this->handler_,
 | 
			
		||||
            std::move(this->buffer_),
 | 
			
		||||
            std::move(this->req_),
 | 
			
		||||
            ConnectionBase::isAdmin(),
 | 
			
		||||
            maxWsSendingQueueSize_
 | 
			
		||||
        )
 | 
			
		||||
            ->run();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -1,208 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/impl/WsBase.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/stream_traits.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/parser.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/ssl.hpp>
 | 
			
		||||
#include <boost/beast/ssl/ssl_stream.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream.hpp>
 | 
			
		||||
#include <boost/optional/optional.hpp>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Represents a secure websocket session.
 | 
			
		||||
 *
 | 
			
		||||
 * Majority of the operations are handled by the base class.
 | 
			
		||||
 */
 | 
			
		||||
template <SomeServerHandler HandlerType>
 | 
			
		||||
class SslWsSession : public impl::WsBase<SslWsSession, HandlerType> {
 | 
			
		||||
    using StreamType = boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>;
 | 
			
		||||
    StreamType ws_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new non-secure websocket session.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stream The SSL stream. Ownership is transferred
 | 
			
		||||
     * @param ip Client's IP address
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param buffer Buffer with initial data received from the peer
 | 
			
		||||
     * @param isAdmin Whether the connection has admin privileges
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    explicit SslWsSession(
 | 
			
		||||
        boost::beast::ssl_stream<boost::beast::tcp_stream>&& stream,
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> const& handler,
 | 
			
		||||
        boost::beast::flat_buffer&& buffer,
 | 
			
		||||
        bool isAdmin,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : impl::WsBase<SslWsSession, HandlerType>(
 | 
			
		||||
              ip,
 | 
			
		||||
              tagFactory,
 | 
			
		||||
              dosGuard,
 | 
			
		||||
              handler,
 | 
			
		||||
              std::move(buffer),
 | 
			
		||||
              maxWsSendingQueueSize
 | 
			
		||||
          )
 | 
			
		||||
        , ws_(std::move(stream))
 | 
			
		||||
    {
 | 
			
		||||
        ConnectionBase::isAdmin_ = isAdmin;  // NOLINT(cppcoreguidelines-prefer-member-initializer)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return The secure websocket stream. */
 | 
			
		||||
    StreamType&
 | 
			
		||||
    ws()
 | 
			
		||||
    {
 | 
			
		||||
        return ws_;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The HTTPS upgrader class, upgrade from an HTTPS session to a secure websocket session.
 | 
			
		||||
 *
 | 
			
		||||
 * Pass the stream to the session class after upgrade.
 | 
			
		||||
 */
 | 
			
		||||
template <SomeServerHandler HandlerType>
 | 
			
		||||
class SslWsUpgrader : public std::enable_shared_from_this<SslWsUpgrader<HandlerType>> {
 | 
			
		||||
    using std::enable_shared_from_this<SslWsUpgrader<HandlerType>>::shared_from_this;
 | 
			
		||||
 | 
			
		||||
    boost::beast::ssl_stream<boost::beast::tcp_stream> https_;
 | 
			
		||||
    boost::optional<http::request_parser<http::string_body>> parser_;
 | 
			
		||||
    boost::beast::flat_buffer buffer_;
 | 
			
		||||
    std::string ip_;
 | 
			
		||||
    std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
 | 
			
		||||
    std::shared_ptr<HandlerType> const handler_;
 | 
			
		||||
    http::request<http::string_body> req_;
 | 
			
		||||
    bool isAdmin_;
 | 
			
		||||
    std::uint32_t maxWsSendingQueueSize_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new upgrader to secure websocket.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stream The SSL stream. Ownership is transferred
 | 
			
		||||
     * @param ip Client's IP address
 | 
			
		||||
     * @param tagFactory A factory that is used to generate tags to track requests and sessions
 | 
			
		||||
     * @param dosGuard The denial of service guard to use
 | 
			
		||||
     * @param handler The server handler to use
 | 
			
		||||
     * @param buffer Buffer with initial data received from the peer. Ownership is transferred
 | 
			
		||||
     * @param request The request. Ownership is transferred
 | 
			
		||||
     * @param isAdmin Whether the connection has admin privileges
 | 
			
		||||
     * @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
 | 
			
		||||
     */
 | 
			
		||||
    SslWsUpgrader(
 | 
			
		||||
        boost::beast::ssl_stream<boost::beast::tcp_stream> stream,
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> handler,
 | 
			
		||||
        boost::beast::flat_buffer&& buffer,
 | 
			
		||||
        http::request<http::string_body> request,
 | 
			
		||||
        bool isAdmin,
 | 
			
		||||
        std::uint32_t maxWsSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : https_(std::move(stream))
 | 
			
		||||
        , buffer_(std::move(buffer))
 | 
			
		||||
        , ip_(std::move(ip))
 | 
			
		||||
        , tagFactory_(tagFactory)
 | 
			
		||||
        , dosGuard_(dosGuard)
 | 
			
		||||
        , handler_(std::move(handler))
 | 
			
		||||
        , req_(std::move(request))
 | 
			
		||||
        , isAdmin_(isAdmin)
 | 
			
		||||
        , maxWsSendingQueueSize_(maxWsSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~SslWsUpgrader() = default;
 | 
			
		||||
 | 
			
		||||
    /** @brief Initiate the upgrade. */
 | 
			
		||||
    void
 | 
			
		||||
    run()
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::get_lowest_layer(https_).expires_after(std::chrono::seconds(30));
 | 
			
		||||
 | 
			
		||||
        boost::asio::dispatch(
 | 
			
		||||
            https_.get_executor(),
 | 
			
		||||
            boost::beast::bind_front_handler(&SslWsUpgrader<HandlerType>::doUpgrade, shared_from_this())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    doUpgrade()
 | 
			
		||||
    {
 | 
			
		||||
        parser_.emplace();
 | 
			
		||||
 | 
			
		||||
        // Apply a reasonable limit to the allowed size of the body in bytes to prevent abuse.
 | 
			
		||||
        static constexpr auto kMAX_BODY_SIZE = 10000;
 | 
			
		||||
        parser_->body_limit(kMAX_BODY_SIZE);
 | 
			
		||||
 | 
			
		||||
        boost::beast::get_lowest_layer(https_).expires_after(std::chrono::seconds(30));
 | 
			
		||||
        onUpgrade();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onUpgrade()
 | 
			
		||||
    {
 | 
			
		||||
        if (!boost::beast::websocket::is_upgrade(req_))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // Disable the timeout. The websocket::stream uses its own timeout settings.
 | 
			
		||||
        boost::beast::get_lowest_layer(https_).expires_never();
 | 
			
		||||
 | 
			
		||||
        std::make_shared<SslWsSession<HandlerType>>(
 | 
			
		||||
            std::move(https_),
 | 
			
		||||
            ip_,
 | 
			
		||||
            tagFactory_,
 | 
			
		||||
            dosGuard_,
 | 
			
		||||
            handler_,
 | 
			
		||||
            std::move(buffer_),
 | 
			
		||||
            isAdmin_,
 | 
			
		||||
            maxWsSendingQueueSize_
 | 
			
		||||
        )
 | 
			
		||||
            ->run(std::move(req_));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
}  // namespace web
 | 
			
		||||
@@ -21,10 +21,14 @@
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
@@ -32,22 +36,39 @@ namespace web {
 | 
			
		||||
 | 
			
		||||
SubscriptionContext::SubscriptionContext(
 | 
			
		||||
    util::TagDecoratorFactory const& factory,
 | 
			
		||||
    std::shared_ptr<ConnectionBase> connection
 | 
			
		||||
    impl::WsConnectionBase& connection,
 | 
			
		||||
    std::optional<size_t> maxSendQueueSize,
 | 
			
		||||
    boost::asio::yield_context yield,
 | 
			
		||||
    ErrorHandler errorHandler
 | 
			
		||||
)
 | 
			
		||||
    : SubscriptionContextInterface{factory}, connection_{connection}
 | 
			
		||||
    : web::SubscriptionContextInterface(factory)
 | 
			
		||||
    , connection_(connection)
 | 
			
		||||
    , maxSendQueueSize_(maxSendQueueSize)
 | 
			
		||||
    , tasksGroup_(yield)
 | 
			
		||||
    , yield_(yield)
 | 
			
		||||
    , errorHandler_(std::move(errorHandler))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SubscriptionContext::~SubscriptionContext()
 | 
			
		||||
{
 | 
			
		||||
    onDisconnect_(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::send(std::shared_ptr<std::string> message)
 | 
			
		||||
{
 | 
			
		||||
    if (auto connection = connection_.lock(); connection != nullptr)
 | 
			
		||||
        connection->send(std::move(message));
 | 
			
		||||
    if (disconnected_)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    if (maxSendQueueSize_.has_value() and tasksGroup_.size() >= *maxSendQueueSize_) {
 | 
			
		||||
        tasksGroup_.spawn(yield_, [this](boost::asio::yield_context innerYield) {
 | 
			
		||||
            connection_.get().close(innerYield);
 | 
			
		||||
        });
 | 
			
		||||
        disconnected_ = true;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tasksGroup_.spawn(yield_, [this, message = std::move(message)](boost::asio::yield_context innerYield) {
 | 
			
		||||
        auto const maybeError = connection_.get().sendBuffer(boost::asio::buffer(*message), innerYield);
 | 
			
		||||
        if (maybeError.has_value() and errorHandler_(*maybeError, connection_))
 | 
			
		||||
            connection_.get().close(innerYield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
@@ -59,13 +80,21 @@ SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::setApiSubversion(uint32_t value)
 | 
			
		||||
{
 | 
			
		||||
    apiSubVersion_ = value;
 | 
			
		||||
    apiSubversion_ = value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t
 | 
			
		||||
SubscriptionContext::apiSubversion() const
 | 
			
		||||
{
 | 
			
		||||
    return apiSubVersion_;
 | 
			
		||||
    return apiSubversion_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::disconnect(boost::asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    onDisconnect_(this);
 | 
			
		||||
    disconnected_ = true;
 | 
			
		||||
    tasksGroup_.asyncWait(yield);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web
 | 
			
		||||
 
 | 
			
		||||
@@ -19,32 +19,55 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/CoroutineGroup.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/interface/Concepts.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
#include "web/impl/WsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/any_io_executor.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/signals2/variadic_signal.hpp>
 | 
			
		||||
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace web {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A context of a WsBase connection for subscriptions.
 | 
			
		||||
 * @brief Implementation of SubscriptionContextInterface.
 | 
			
		||||
 * @note This class is designed to be used with SubscriptionManager. The class is safe to use from multiple threads.
 | 
			
		||||
 * The method disconnect() must be called before the object is destroyed.
 | 
			
		||||
 */
 | 
			
		||||
class SubscriptionContext : public SubscriptionContextInterface {
 | 
			
		||||
    std::weak_ptr<ConnectionBase> connection_;
 | 
			
		||||
class SubscriptionContext : public web::SubscriptionContextInterface {
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Error handler definition. Error handler returns true if connection should be closed false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    using ErrorHandler = std::function<bool(Error const&, Connection const&)>;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    std::reference_wrapper<impl::WsConnectionBase> connection_;
 | 
			
		||||
    std::optional<size_t> maxSendQueueSize_;
 | 
			
		||||
    util::CoroutineGroup tasksGroup_;
 | 
			
		||||
    boost::asio::yield_context yield_;
 | 
			
		||||
    ErrorHandler errorHandler_;
 | 
			
		||||
 | 
			
		||||
    boost::signals2::signal<void(SubscriptionContextInterface*)> onDisconnect_;
 | 
			
		||||
    std::atomic_bool disconnected_{false};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief The API version of the web stream client.
 | 
			
		||||
     * This is used to track the api version of this connection, which mainly is used by subscription. It is different
 | 
			
		||||
     * from the api version in Context, which is only used for the current request.
 | 
			
		||||
     */
 | 
			
		||||
    std::atomic_uint32_t apiSubVersion_ = 0;
 | 
			
		||||
    std::atomic_uint32_t apiSubversion_ = 0u;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
@@ -52,17 +75,21 @@ public:
 | 
			
		||||
     *
 | 
			
		||||
     * @param factory The tag decorator factory to use to init taggable.
 | 
			
		||||
     * @param connection The connection for which the context is created.
 | 
			
		||||
     * @param maxSendQueueSize The maximum size of the send queue. If the queue is full, the connection will be closed.
 | 
			
		||||
     * @param yield The yield context to spawn sending coroutines.
 | 
			
		||||
     * @param errorHandler The error handler.
 | 
			
		||||
     */
 | 
			
		||||
    SubscriptionContext(util::TagDecoratorFactory const& factory, std::shared_ptr<ConnectionBase> connection);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Destroy the Subscription Context object
 | 
			
		||||
     */
 | 
			
		||||
    ~SubscriptionContext() override;
 | 
			
		||||
    SubscriptionContext(
 | 
			
		||||
        util::TagDecoratorFactory const& factory,
 | 
			
		||||
        impl::WsConnectionBase& connection,
 | 
			
		||||
        std::optional<size_t> maxSendQueueSize,
 | 
			
		||||
        boost::asio::yield_context yield,
 | 
			
		||||
        ErrorHandler errorHandler
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Send message to the client
 | 
			
		||||
     * @note This method will not do anything if the related connection got disconnected.
 | 
			
		||||
     * @note This method does nothing after disconnected() was called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     */
 | 
			
		||||
@@ -91,6 +118,15 @@ public:
 | 
			
		||||
     */
 | 
			
		||||
    uint32_t
 | 
			
		||||
    apiSubversion() const override;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Notify the context that related connection is disconnected and wait for all the task to complete.
 | 
			
		||||
     * @note This method must be called before the object is destroyed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield The yield context to wait for all the tasks to complete.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    disconnect(boost::asio::yield_context yield);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
 | 
			
		||||
#include <type_traits>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
 | 
			
		||||
@@ -32,4 +32,4 @@ concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept IsSslTcpStream = std::is_same_v<std::decay_t<T>, boost::asio::ssl::stream<boost::beast::tcp_stream>>;
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -17,20 +17,20 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/impl/ConnectionHandler.hpp"
 | 
			
		||||
#include "web/impl/ConnectionHandler.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/CoroutineGroup.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/MessageHandler.hpp"
 | 
			
		||||
#include "web/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContext.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/MessageHandler.hpp"
 | 
			
		||||
#include "web/ng/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/SubscriptionContext.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/error.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
@@ -47,7 +47,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -387,4 +387,4 @@ ConnectionHandler::handleRequest(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -26,13 +26,13 @@
 | 
			
		||||
#include "util/prometheus/Gauge.hpp"
 | 
			
		||||
#include "util/prometheus/Label.hpp"
 | 
			
		||||
#include "util/prometheus/Prometheus.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/MessageHandler.hpp"
 | 
			
		||||
#include "web/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/MessageHandler.hpp"
 | 
			
		||||
#include "web/ng/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/signals2/signal.hpp>
 | 
			
		||||
@@ -47,7 +47,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
class ConnectionHandler {
 | 
			
		||||
public:
 | 
			
		||||
@@ -161,4 +161,4 @@ private:
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -17,13 +17,13 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/impl/ErrorHandling.hpp"
 | 
			
		||||
#include "web/impl/ErrorHandling.hpp"
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
@@ -37,7 +37,7 @@
 | 
			
		||||
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -161,4 +161,4 @@ ErrorHelper::composeError(rpc::RippledError error) const
 | 
			
		||||
    return composeErrorImpl(error, rawRequest_, request_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
@@ -20,9 +20,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
@@ -31,11 +30,8 @@
 | 
			
		||||
#include <xrpl/protocol/ErrorCodes.h>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
@@ -43,137 +39,76 @@ namespace web::impl {
 | 
			
		||||
 * @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
 | 
			
		||||
 */
 | 
			
		||||
class ErrorHelper {
 | 
			
		||||
    std::shared_ptr<web::ConnectionBase> connection_;
 | 
			
		||||
    std::reference_wrapper<Request const> rawRequest_;
 | 
			
		||||
    std::optional<boost::json::object> request_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    ErrorHelper(
 | 
			
		||||
        std::shared_ptr<web::ConnectionBase> const& connection,
 | 
			
		||||
        std::optional<boost::json::object> request = std::nullopt
 | 
			
		||||
    )
 | 
			
		||||
        : connection_{connection}, request_{std::move(request)}
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Error Helper object
 | 
			
		||||
     *
 | 
			
		||||
     * @param rawRequest The request that caused the error.
 | 
			
		||||
     * @param request The parsed request that caused the error.
 | 
			
		||||
     */
 | 
			
		||||
    ErrorHelper(Request const& rawRequest, std::optional<boost::json::object> request = std::nullopt);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendError(rpc::Status const& err) const
 | 
			
		||||
    {
 | 
			
		||||
        if (connection_->upgraded) {
 | 
			
		||||
            connection_->send(boost::json::serialize(composeError(err)));
 | 
			
		||||
        } else {
 | 
			
		||||
            // Note: a collection of crutches to match rippled output follows
 | 
			
		||||
            if (auto const clioCode = std::get_if<rpc::ClioError>(&err.code)) {
 | 
			
		||||
                switch (*clioCode) {
 | 
			
		||||
                    case rpc::ClioError::RpcInvalidApiVersion:
 | 
			
		||||
                        connection_->send(
 | 
			
		||||
                            std::string{rpc::getErrorInfo(*clioCode).error}, boost::beast::http::status::bad_request
 | 
			
		||||
                        );
 | 
			
		||||
                        break;
 | 
			
		||||
                    case rpc::ClioError::RpcCommandIsMissing:
 | 
			
		||||
                        connection_->send("Null method", boost::beast::http::status::bad_request);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case rpc::ClioError::RpcCommandIsEmpty:
 | 
			
		||||
                        connection_->send("method is empty", boost::beast::http::status::bad_request);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case rpc::ClioError::RpcCommandNotString:
 | 
			
		||||
                        connection_->send("method is not string", boost::beast::http::status::bad_request);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case rpc::ClioError::RpcParamsUnparsable:
 | 
			
		||||
                        connection_->send("params unparsable", boost::beast::http::status::bad_request);
 | 
			
		||||
                        break;
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make an error response from a status.
 | 
			
		||||
     *
 | 
			
		||||
     * @param err The status to make an error response from.
 | 
			
		||||
     * @return
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeError(rpc::Status const& err) const;
 | 
			
		||||
 | 
			
		||||
                    // others are not applicable but we want a compilation error next time we add one
 | 
			
		||||
                    case rpc::ClioError::RpcUnknownOption:
 | 
			
		||||
                    case rpc::ClioError::RpcMalformedCurrency:
 | 
			
		||||
                    case rpc::ClioError::RpcMalformedRequest:
 | 
			
		||||
                    case rpc::ClioError::RpcMalformedOwner:
 | 
			
		||||
                    case rpc::ClioError::RpcMalformedAddress:
 | 
			
		||||
                    case rpc::ClioError::RpcFieldNotFoundTransaction:
 | 
			
		||||
                    case rpc::ClioError::RpcMalformedOracleDocumentId:
 | 
			
		||||
                    case rpc::ClioError::RpcMalformedAuthorizedCredentials:
 | 
			
		||||
                    case rpc::ClioError::EtlConnectionError:
 | 
			
		||||
                    case rpc::ClioError::EtlRequestError:
 | 
			
		||||
                    case rpc::ClioError::EtlRequestTimeout:
 | 
			
		||||
                    case rpc::ClioError::EtlInvalidResponse:
 | 
			
		||||
                        ASSERT(
 | 
			
		||||
                            false, "Unknown rpc error code {}", static_cast<int>(*clioCode)
 | 
			
		||||
                        );  // this should never happen
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                connection_->send(boost::json::serialize(composeError(err)), boost::beast::http::status::bad_request);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make an internal error response.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with an internal error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeInternalError() const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendInternalError() const
 | 
			
		||||
    {
 | 
			
		||||
        connection_->send(
 | 
			
		||||
            boost::json::serialize(composeError(rpc::RippledError::rpcINTERNAL)),
 | 
			
		||||
            boost::beast::http::status::internal_server_error
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make a response for when the server is not ready.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with a not ready error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeNotReadyError() const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendNotReadyError() const
 | 
			
		||||
    {
 | 
			
		||||
        connection_->send(
 | 
			
		||||
            boost::json::serialize(composeError(rpc::RippledError::rpcNOT_READY)), boost::beast::http::status::ok
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make a response for when the server is too busy.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with a too busy error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeTooBusyError() const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendTooBusyError() const
 | 
			
		||||
    {
 | 
			
		||||
        if (connection_->upgraded) {
 | 
			
		||||
            connection_->send(
 | 
			
		||||
                boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)), boost::beast::http::status::ok
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            connection_->send(
 | 
			
		||||
                boost::json::serialize(rpc::makeError(rpc::RippledError::rpcTOO_BUSY)),
 | 
			
		||||
                boost::beast::http::status::service_unavailable
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make a response when json parsing fails.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with a json parsing error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeJsonParsingError() const;
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendJsonParsingError() const
 | 
			
		||||
    {
 | 
			
		||||
        if (connection_->upgraded) {
 | 
			
		||||
            connection_->send(boost::json::serialize(rpc::makeError(rpc::RippledError::rpcBAD_SYNTAX)));
 | 
			
		||||
        } else {
 | 
			
		||||
            connection_->send(
 | 
			
		||||
                fmt::format("Unable to parse JSON from the request"), boost::beast::http::status::bad_request
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Compose an error into json object from a status.
 | 
			
		||||
     *
 | 
			
		||||
     * @param error The status to compose into a json object.
 | 
			
		||||
     * @return The composed json object.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] boost::json::object
 | 
			
		||||
    composeError(rpc::Status const& error) const;
 | 
			
		||||
 | 
			
		||||
    boost::json::object
 | 
			
		||||
    composeError(auto const& error) const
 | 
			
		||||
    {
 | 
			
		||||
        auto e = rpc::makeError(error);
 | 
			
		||||
 | 
			
		||||
        if (request_) {
 | 
			
		||||
            auto const appendFieldIfExist = [&](auto const& field) {
 | 
			
		||||
                if (request_->contains(field) and not request_->at(field).is_null())
 | 
			
		||||
                    e[field] = request_->at(field);
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            appendFieldIfExist(JS(id));
 | 
			
		||||
 | 
			
		||||
            if (connection_->upgraded)
 | 
			
		||||
                appendFieldIfExist(JS(api_version));
 | 
			
		||||
 | 
			
		||||
            e[JS(request)] = request_.value();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (connection_->upgraded) {
 | 
			
		||||
            return e;
 | 
			
		||||
        }
 | 
			
		||||
        return {{JS(result), e}};
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Compose an error into json object from a rippled error.
 | 
			
		||||
     *
 | 
			
		||||
     * @param error The rippled error to compose into a json object.
 | 
			
		||||
     * @return The composed json object.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] boost::json::object
 | 
			
		||||
    composeError(rpc::RippledError error) const;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
 
 | 
			
		||||
@@ -1,327 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/build/Build.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "util/prometheus/Http.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/interface/Concepts.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/error.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/ssl/error.hpp>
 | 
			
		||||
#include <boost/beast/core.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/http.hpp>
 | 
			
		||||
#include <boost/beast/http/error.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/http/verb.hpp>
 | 
			
		||||
#include <boost/beast/ssl.hpp>
 | 
			
		||||
#include <boost/core/ignore_unused.hpp>
 | 
			
		||||
#include <boost/json.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
#include <xrpl/protocol/ErrorCodes.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
static auto constexpr kHEALTH_CHECK_HTML = R"html(
 | 
			
		||||
    <!DOCTYPE html>
 | 
			
		||||
    <html>
 | 
			
		||||
        <head><title>Test page for Clio</title></head>
 | 
			
		||||
        <body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
 | 
			
		||||
    </html>
 | 
			
		||||
)html";
 | 
			
		||||
 | 
			
		||||
using tcp = boost::asio::ip::tcp;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief This is the implementation class for http sessions
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam Derived The derived class
 | 
			
		||||
 * @tparam HandlerType The handler class, will be called when a request is received.
 | 
			
		||||
 */
 | 
			
		||||
template <template <typename> typename Derived, SomeServerHandler HandlerType>
 | 
			
		||||
class HttpBase : public ConnectionBase {
 | 
			
		||||
    Derived<HandlerType>&
 | 
			
		||||
    derived()
 | 
			
		||||
    {
 | 
			
		||||
        return static_cast<Derived<HandlerType>&>(*this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: this should be rewritten using http::message_generator instead
 | 
			
		||||
    struct SendLambda {
 | 
			
		||||
        HttpBase& self;
 | 
			
		||||
 | 
			
		||||
        explicit SendLambda(HttpBase& self) : self(self)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        template <bool IsRequest, typename Body, typename Fields>
 | 
			
		||||
        void
 | 
			
		||||
        operator()(http::message<IsRequest, Body, Fields>&& msg) const
 | 
			
		||||
        {
 | 
			
		||||
            if (self.dead())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // The lifetime of the message has to extend for the duration of the async operation so we use a shared_ptr
 | 
			
		||||
            // to manage it.
 | 
			
		||||
            auto sp = std::make_shared<http::message<IsRequest, Body, Fields>>(std::move(msg));
 | 
			
		||||
 | 
			
		||||
            // Store a type-erased version of the shared pointer in the class to keep it alive.
 | 
			
		||||
            self.res_ = sp;
 | 
			
		||||
 | 
			
		||||
            // Write the response
 | 
			
		||||
            http::async_write(
 | 
			
		||||
                self.derived().stream(),
 | 
			
		||||
                *sp,
 | 
			
		||||
                boost::beast::bind_front_handler(&HttpBase::onWrite, self.derived().shared_from_this(), sp->need_eof())
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<void> res_;
 | 
			
		||||
    SendLambda sender_;
 | 
			
		||||
    std::shared_ptr<AdminVerificationStrategy> adminVerification_;
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    boost::beast::flat_buffer buffer_;
 | 
			
		||||
    http::request<http::string_body> req_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
 | 
			
		||||
    std::shared_ptr<HandlerType> const handler_;
 | 
			
		||||
    util::Logger log_{"WebServer"};
 | 
			
		||||
    util::Logger perfLog_{"Performance"};
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    httpFail(boost::beast::error_code ec, char const* what)
 | 
			
		||||
    {
 | 
			
		||||
        // ssl::error::stream_truncated, also known as an SSL "short read",
 | 
			
		||||
        // indicates the peer closed the connection without performing the
 | 
			
		||||
        // required closing handshake (for example, Google does this to
 | 
			
		||||
        // improve performance). Generally this can be a security issue,
 | 
			
		||||
        // but if your communication protocol is self-terminated (as
 | 
			
		||||
        // it is with both HTTP and WebSocket) then you may simply
 | 
			
		||||
        // ignore the lack of close_notify.
 | 
			
		||||
        //
 | 
			
		||||
        // https://github.com/boostorg/beast/issues/38
 | 
			
		||||
        //
 | 
			
		||||
        // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
 | 
			
		||||
        //
 | 
			
		||||
        // When a short read would cut off the end of an HTTP message,
 | 
			
		||||
        // Beast returns the error boost::beast::http::error::partial_message.
 | 
			
		||||
        // Therefore, if we see a short read here, it has occurred
 | 
			
		||||
        // after the message has been completed, so it is safe to ignore it.
 | 
			
		||||
 | 
			
		||||
        if (ec == boost::asio::ssl::error::stream_truncated)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!ec_ && ec != boost::asio::error::operation_aborted) {
 | 
			
		||||
            ec_ = ec;
 | 
			
		||||
            LOG(perfLog_.info()) << tag() << ": " << what << ": " << ec.message();
 | 
			
		||||
            boost::beast::get_lowest_layer(derived().stream()).socket().close(ec);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    HttpBase(
 | 
			
		||||
        std::string const& ip,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::shared_ptr<AdminVerificationStrategy> adminVerification,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> handler,
 | 
			
		||||
        boost::beast::flat_buffer buffer
 | 
			
		||||
    )
 | 
			
		||||
        : ConnectionBase(tagFactory, ip)
 | 
			
		||||
        , sender_(*this)
 | 
			
		||||
        , adminVerification_(std::move(adminVerification))
 | 
			
		||||
        , buffer_(std::move(buffer))
 | 
			
		||||
        , dosGuard_(dosGuard)
 | 
			
		||||
        , handler_(std::move(handler))
 | 
			
		||||
    {
 | 
			
		||||
        LOG(perfLog_.debug()) << tag() << "http session created";
 | 
			
		||||
        dosGuard_.get().increment(ip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~HttpBase() override
 | 
			
		||||
    {
 | 
			
		||||
        LOG(perfLog_.debug()) << tag() << "http session closed";
 | 
			
		||||
        if (not upgraded)
 | 
			
		||||
            dosGuard_.get().decrement(this->clientIp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    doRead()
 | 
			
		||||
    {
 | 
			
		||||
        if (dead())
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // Make the request empty before reading, otherwise the operation behavior is undefined.
 | 
			
		||||
        req_ = {};
 | 
			
		||||
 | 
			
		||||
        // Set the timeout.
 | 
			
		||||
        boost::beast::get_lowest_layer(derived().stream()).expires_after(std::chrono::seconds(30));
 | 
			
		||||
 | 
			
		||||
        http::async_read(
 | 
			
		||||
            derived().stream(),
 | 
			
		||||
            buffer_,
 | 
			
		||||
            req_,
 | 
			
		||||
            boost::beast::bind_front_handler(&HttpBase::onRead, derived().shared_from_this())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onRead(boost::beast::error_code ec, [[maybe_unused]] std::size_t bytesTransferred)
 | 
			
		||||
    {
 | 
			
		||||
        if (ec == http::error::end_of_stream)
 | 
			
		||||
            return derived().doClose();
 | 
			
		||||
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return httpFail(ec, "read");
 | 
			
		||||
 | 
			
		||||
        if (req_.method() == http::verb::get and req_.target() == "/health")
 | 
			
		||||
            return sender_(httpResponse(http::status::ok, "text/html", kHEALTH_CHECK_HTML));
 | 
			
		||||
 | 
			
		||||
        // Update isAdmin property of the connection
 | 
			
		||||
        ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);
 | 
			
		||||
 | 
			
		||||
        if (boost::beast::websocket::is_upgrade(req_)) {
 | 
			
		||||
            if (dosGuard_.get().isOk(this->clientIp)) {
 | 
			
		||||
                // Disable the timeout. The websocket::stream uses its own timeout settings.
 | 
			
		||||
                boost::beast::get_lowest_layer(derived().stream()).expires_never();
 | 
			
		||||
 | 
			
		||||
                upgraded = true;
 | 
			
		||||
                return derived().upgrade();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return sender_(httpResponse(http::status::too_many_requests, "text/html", "Too many requests"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (auto response = util::prometheus::handlePrometheusRequest(req_, isAdmin()); response.has_value())
 | 
			
		||||
            return sender_(std::move(response.value()));
 | 
			
		||||
 | 
			
		||||
        if (req_.method() != http::verb::post) {
 | 
			
		||||
            return sender_(httpResponse(http::status::bad_request, "text/html", "Expected a POST request"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LOG(log_.info()) << tag() << "Received request from ip = " << clientIp;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            (*handler_)(req_.body(), derived().shared_from_this());
 | 
			
		||||
        } catch (std::exception const&) {
 | 
			
		||||
            return sender_(httpResponse(
 | 
			
		||||
                http::status::internal_server_error,
 | 
			
		||||
                "application/json",
 | 
			
		||||
                boost::json::serialize(rpc::makeError(rpc::RippledError::rpcINTERNAL))
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendSlowDown(std::string const&) override
 | 
			
		||||
    {
 | 
			
		||||
        sender_(httpResponse(
 | 
			
		||||
            http::status::service_unavailable,
 | 
			
		||||
            "text/plain",
 | 
			
		||||
            boost::json::serialize(rpc::makeError(rpc::RippledError::rpcSLOW_DOWN))
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Send a response to the client
 | 
			
		||||
     * The message length will be added to the DOSGuard, if the limit is reached, a warning will be added to the
 | 
			
		||||
     * response
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    send(std::string&& msg, http::status status = http::status::ok) override
 | 
			
		||||
    {
 | 
			
		||||
        if (!dosGuard_.get().add(clientIp, msg.size())) {
 | 
			
		||||
            auto jsonResponse = boost::json::parse(msg).as_object();
 | 
			
		||||
            jsonResponse["warning"] = "load";
 | 
			
		||||
            if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
 | 
			
		||||
                jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
 | 
			
		||||
            } else {
 | 
			
		||||
                jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Reserialize when we need to include this warning
 | 
			
		||||
            msg = boost::json::serialize(jsonResponse);
 | 
			
		||||
        }
 | 
			
		||||
        sender_(httpResponse(status, "application/json", std::move(msg)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    SubscriptionContextPtr
 | 
			
		||||
    makeSubscriptionContext(util::TagDecoratorFactory const&) override
 | 
			
		||||
    {
 | 
			
		||||
        ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
 | 
			
		||||
        std::unreachable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onWrite(bool close, boost::beast::error_code ec, std::size_t bytesTransferred)
 | 
			
		||||
    {
 | 
			
		||||
        boost::ignore_unused(bytesTransferred);
 | 
			
		||||
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return httpFail(ec, "write");
 | 
			
		||||
 | 
			
		||||
        // This means we should close the connection, usually because
 | 
			
		||||
        // the response indicated the "Connection: close" semantic.
 | 
			
		||||
        if (close)
 | 
			
		||||
            return derived().doClose();
 | 
			
		||||
 | 
			
		||||
        res_ = nullptr;
 | 
			
		||||
        doRead();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    http::response<http::string_body>
 | 
			
		||||
    httpResponse(http::status status, std::string contentType, std::string message) const
 | 
			
		||||
    {
 | 
			
		||||
        http::response<http::string_body> res{status, req_.version()};
 | 
			
		||||
        res.set(http::field::server, "clio-server-" + util::build::getClioVersionString());
 | 
			
		||||
        res.set(http::field::content_type, contentType);
 | 
			
		||||
        res.keep_alive(req_.keep_alive());
 | 
			
		||||
        res.body() = std::move(message);
 | 
			
		||||
        res.prepare_payload();
 | 
			
		||||
        return res;
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -21,12 +21,12 @@
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/Concepts.hpp"
 | 
			
		||||
#include "web/ng/impl/WsConnection.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/Concepts.hpp"
 | 
			
		||||
#include "web/impl/WsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
@@ -49,7 +49,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
class UpgradableConnection : public Connection {
 | 
			
		||||
public:
 | 
			
		||||
@@ -229,4 +229,4 @@ using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
 | 
			
		||||
 | 
			
		||||
using SslHttpConnection = HttpConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/impl/ServerSslContext.hpp"
 | 
			
		||||
#include "web/impl/ServerSslContext.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
@@ -94,4 +94,4 @@ makeServerSslContext(std::string const& certData, std::string const& keyData)
 | 
			
		||||
    return ctx;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -27,7 +27,7 @@
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
std::expected<std::optional<boost::asio::ssl::context>, std::string>
 | 
			
		||||
makeServerSslContext(util::config::ClioConfigDefinition const& config);
 | 
			
		||||
@@ -35,4 +35,4 @@ makeServerSslContext(util::config::ClioConfigDefinition const& config);
 | 
			
		||||
std::expected<boost::asio::ssl::context, std::string>
 | 
			
		||||
makeServerSslContext(std::string const& certData, std::string const& keyData);
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -1,318 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/SubscriptionContext.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/interface/Concepts.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBase.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/error.hpp>
 | 
			
		||||
#include <boost/beast/core.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/role.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/version.hpp>
 | 
			
		||||
#include <boost/beast/websocket/error.hpp>
 | 
			
		||||
#include <boost/beast/websocket/rfc6455.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream_base.hpp>
 | 
			
		||||
#include <boost/core/ignore_unused.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
#include <xrpl/protocol/ErrorCodes.h>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <queue>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Web socket implementation. This class is the base class of the web socket session, it will handle the read and
 | 
			
		||||
 * write operations.
 | 
			
		||||
 *
 | 
			
		||||
 * The write operation is via a queue, each write operation of this session will be sent in order.
 | 
			
		||||
 * The write operation also supports shared_ptr of string, so the caller can keep the string alive until it is sent.
 | 
			
		||||
 * It is useful when we have multiple sessions sending the same content.
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam Derived The derived class
 | 
			
		||||
 * @tparam HandlerType The handler type, will be called when a request is received.
 | 
			
		||||
 */
 | 
			
		||||
template <template <typename> typename Derived, SomeServerHandler HandlerType>
 | 
			
		||||
class WsBase : public ConnectionBase, public std::enable_shared_from_this<WsBase<Derived, HandlerType>> {
 | 
			
		||||
    using std::enable_shared_from_this<WsBase<Derived, HandlerType>>::shared_from_this;
 | 
			
		||||
 | 
			
		||||
    boost::beast::flat_buffer buffer_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
 | 
			
		||||
    bool sending_ = false;
 | 
			
		||||
    std::queue<std::shared_ptr<std::string>> messages_;
 | 
			
		||||
    std::shared_ptr<HandlerType> const handler_;
 | 
			
		||||
 | 
			
		||||
    SubscriptionContextPtr subscriptionContext_;
 | 
			
		||||
    std::uint32_t maxSendingQueueSize_;
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    util::Logger log_{"WebServer"};
 | 
			
		||||
    util::Logger perfLog_{"Performance"};
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    wsFail(boost::beast::error_code ec, char const* what)
 | 
			
		||||
    {
 | 
			
		||||
        // Don't log if the WebSocket stream was gracefully closed at both endpoints
 | 
			
		||||
        if (ec != boost::beast::websocket::error::closed)
 | 
			
		||||
            LOG(log_.error()) << tag() << ": " << what << ": " << ec.message() << ": " << ec.value();
 | 
			
		||||
 | 
			
		||||
        if (!ec_ && ec != boost::asio::error::operation_aborted) {
 | 
			
		||||
            ec_ = ec;
 | 
			
		||||
            boost::beast::get_lowest_layer(derived().ws()).socket().close(ec);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    explicit WsBase(
 | 
			
		||||
        std::string ip,
 | 
			
		||||
        std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
 | 
			
		||||
        std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
 | 
			
		||||
        std::shared_ptr<HandlerType> const& handler,
 | 
			
		||||
        boost::beast::flat_buffer&& buffer,
 | 
			
		||||
        std::uint32_t maxSendingQueueSize
 | 
			
		||||
    )
 | 
			
		||||
        : ConnectionBase(tagFactory, ip)
 | 
			
		||||
        , buffer_(std::move(buffer))
 | 
			
		||||
        , dosGuard_(dosGuard)
 | 
			
		||||
        , handler_(handler)
 | 
			
		||||
        , maxSendingQueueSize_(maxSendingQueueSize)
 | 
			
		||||
    {
 | 
			
		||||
        upgraded = true;  // NOLINT (cppcoreguidelines-pro-type-member-init)
 | 
			
		||||
 | 
			
		||||
        LOG(perfLog_.debug()) << tag() << "session created";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~WsBase() override
 | 
			
		||||
    {
 | 
			
		||||
        LOG(perfLog_.debug()) << tag() << "session closed";
 | 
			
		||||
        dosGuard_.get().decrement(clientIp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Derived<HandlerType>&
 | 
			
		||||
    derived()
 | 
			
		||||
    {
 | 
			
		||||
        return static_cast<Derived<HandlerType>&>(*this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    doWrite()
 | 
			
		||||
    {
 | 
			
		||||
        sending_ = true;
 | 
			
		||||
        derived().ws().async_write(
 | 
			
		||||
            boost::asio::buffer(messages_.front()->data(), messages_.front()->size()),
 | 
			
		||||
            boost::beast::bind_front_handler(&WsBase::onWrite, derived().shared_from_this())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onWrite(boost::system::error_code ec, std::size_t)
 | 
			
		||||
    {
 | 
			
		||||
        messages_.pop();
 | 
			
		||||
        sending_ = false;
 | 
			
		||||
        if (ec) {
 | 
			
		||||
            wsFail(ec, "Failed to write");
 | 
			
		||||
        } else {
 | 
			
		||||
            maybeSendNext();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    maybeSendNext()
 | 
			
		||||
    {
 | 
			
		||||
        if (ec_ || sending_ || messages_.empty())
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        doWrite();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    sendSlowDown(std::string const& request) override
 | 
			
		||||
    {
 | 
			
		||||
        sendError(rpc::RippledError::rpcSLOW_DOWN, request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Send a message to the client
 | 
			
		||||
     * @param msg The message to send, it will keep the string alive until it is sent. It is useful when we have
 | 
			
		||||
     * multiple session sending the same content.
 | 
			
		||||
     * Be aware that the message length will not be added to the DOSGuard from this function.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    send(std::shared_ptr<std::string> msg) override
 | 
			
		||||
    {
 | 
			
		||||
        // Note: post used instead of dispatch to guarantee async behavior of wsFail and maybeSendNext
 | 
			
		||||
        boost::asio::post(
 | 
			
		||||
            derived().ws().get_executor(),
 | 
			
		||||
            [this, self = derived().shared_from_this(), msg = std::move(msg)]() {
 | 
			
		||||
                if (messages_.size() > maxSendingQueueSize_) {
 | 
			
		||||
                    wsFail(boost::asio::error::timed_out, "Client is too slow");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                messages_.push(msg);
 | 
			
		||||
                maybeSendNext();
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get the subscription context for this connection.
 | 
			
		||||
     *
 | 
			
		||||
     * @param factory Tag TagDecoratorFactory to use to create the context.
 | 
			
		||||
     * @return The subscription context for this connection.
 | 
			
		||||
     */
 | 
			
		||||
    SubscriptionContextPtr
 | 
			
		||||
    makeSubscriptionContext(util::TagDecoratorFactory const& factory) override
 | 
			
		||||
    {
 | 
			
		||||
        if (subscriptionContext_ == nullptr) {
 | 
			
		||||
            subscriptionContext_ = std::make_shared<SubscriptionContext>(factory, shared_from_this());
 | 
			
		||||
        }
 | 
			
		||||
        return subscriptionContext_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Send a message to the client
 | 
			
		||||
     * @param msg The message to send
 | 
			
		||||
     * Send this message to the client. The message length will be added to the DOSGuard
 | 
			
		||||
     * If the DOSGuard is triggered, the message will be modified to include a warning
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    send(std::string&& msg, http::status) override
 | 
			
		||||
    {
 | 
			
		||||
        if (!dosGuard_.get().add(clientIp, msg.size())) {
 | 
			
		||||
            auto jsonResponse = boost::json::parse(msg).as_object();
 | 
			
		||||
            jsonResponse["warning"] = "load";
 | 
			
		||||
 | 
			
		||||
            if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
 | 
			
		||||
                jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
 | 
			
		||||
            } else {
 | 
			
		||||
                jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Reserialize when we need to include this warning
 | 
			
		||||
            msg = boost::json::serialize(jsonResponse);
 | 
			
		||||
        }
 | 
			
		||||
        auto sharedMsg = std::make_shared<std::string>(std::move(msg));
 | 
			
		||||
        send(std::move(sharedMsg));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Accept the session asynchronously
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    run(http::request<http::string_body> req)
 | 
			
		||||
    {
 | 
			
		||||
        using namespace boost::beast;
 | 
			
		||||
 | 
			
		||||
        derived().ws().set_option(websocket::stream_base::timeout::suggested(role_type::server));
 | 
			
		||||
 | 
			
		||||
        // Set a decorator to change the Server of the handshake
 | 
			
		||||
        derived().ws().set_option(websocket::stream_base::decorator([](websocket::response_type& res) {
 | 
			
		||||
            res.set(http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-server-async");
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        derived().ws().async_accept(req, bind_front_handler(&WsBase::onAccept, this->shared_from_this()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onAccept(boost::beast::error_code ec)
 | 
			
		||||
    {
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return wsFail(ec, "accept");
 | 
			
		||||
 | 
			
		||||
        LOG(perfLog_.info()) << tag() << "accepting new connection";
 | 
			
		||||
 | 
			
		||||
        doRead();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    doRead()
 | 
			
		||||
    {
 | 
			
		||||
        if (dead())
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        // Note: use entirely new buffer so previously used, potentially large, capacity is deallocated
 | 
			
		||||
        buffer_ = boost::beast::flat_buffer{};
 | 
			
		||||
 | 
			
		||||
        derived().ws().async_read(buffer_, boost::beast::bind_front_handler(&WsBase::onRead, this->shared_from_this()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    onRead(boost::beast::error_code ec, std::size_t bytesTransferred)
 | 
			
		||||
    {
 | 
			
		||||
        boost::ignore_unused(bytesTransferred);
 | 
			
		||||
 | 
			
		||||
        if (ec)
 | 
			
		||||
            return wsFail(ec, "read");
 | 
			
		||||
 | 
			
		||||
        LOG(perfLog_.info()) << tag() << "Received request from ip = " << this->clientIp;
 | 
			
		||||
 | 
			
		||||
        std::string requestStr{static_cast<char const*>(buffer_.data().data()), buffer_.size()};
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            (*handler_)(requestStr, shared_from_this());
 | 
			
		||||
        } catch (std::exception const&) {
 | 
			
		||||
            sendError(rpc::RippledError::rpcINTERNAL, std::move(requestStr));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        doRead();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    sendError(rpc::RippledError error, std::string requestStr)
 | 
			
		||||
    {
 | 
			
		||||
        auto e = rpc::makeError(error);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            auto request = boost::json::parse(requestStr);
 | 
			
		||||
            if (request.is_object() && request.as_object().contains("id"))
 | 
			
		||||
                e["id"] = request.as_object().at("id");
 | 
			
		||||
            e["request"] = std::move(request);
 | 
			
		||||
        } catch (std::exception const&) {
 | 
			
		||||
            e["request"] = requestStr;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this->send(std::make_shared<std::string>(boost::json::serialize(e)));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -21,11 +21,11 @@
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/build/Build.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/Concepts.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/Concepts.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
@@ -50,7 +50,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
namespace web::impl {
 | 
			
		||||
 | 
			
		||||
class WsConnectionBase : public Connection {
 | 
			
		||||
public:
 | 
			
		||||
@@ -192,4 +192,4 @@ makeWsConnection(
 | 
			
		||||
    return connection;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
}  // namespace web::impl
 | 
			
		||||
@@ -1,393 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2023, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "data/BackendInterface.hpp"
 | 
			
		||||
#include "etlng/ETLServiceInterface.hpp"
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/Factories.hpp"
 | 
			
		||||
#include "rpc/JS.hpp"
 | 
			
		||||
#include "rpc/RPCHelpers.hpp"
 | 
			
		||||
#include "rpc/common/impl/APIVersionParser.hpp"
 | 
			
		||||
#include "util/Assert.hpp"
 | 
			
		||||
#include "util/CoroutineGroup.hpp"
 | 
			
		||||
#include "util/JsonUtils.hpp"
 | 
			
		||||
#include "util/Profiler.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/ErrorHandling.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/steady_timer.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
#include <boost/system/system_error.hpp>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <ratio>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief The server handler for RPC requests called by web server.
 | 
			
		||||
 *
 | 
			
		||||
 * Note: see @ref web::SomeServerHandler concept
 | 
			
		||||
 */
 | 
			
		||||
template <typename RPCEngineType>
 | 
			
		||||
class RPCServerHandler {
 | 
			
		||||
    std::shared_ptr<BackendInterface const> const backend_;
 | 
			
		||||
    std::shared_ptr<RPCEngineType> const rpcEngine_;
 | 
			
		||||
    std::shared_ptr<etlng::ETLServiceInterface const> const etl_;
 | 
			
		||||
    std::reference_wrapper<dosguard::DOSGuardInterface> dosguard_;
 | 
			
		||||
    util::TagDecoratorFactory const tagFactory_;
 | 
			
		||||
    rpc::impl::ProductionAPIVersionParser apiVersionParser_;  // can be injected if needed
 | 
			
		||||
 | 
			
		||||
    util::Logger log_{"RPC"};
 | 
			
		||||
    util::Logger perfLog_{"Performance"};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create a new server handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @param config Clio config to use
 | 
			
		||||
     * @param backend The backend to use
 | 
			
		||||
     * @param rpcEngine The RPC engine to use
 | 
			
		||||
     * @param etl The ETL to use
 | 
			
		||||
     * @param dosguard The DOS guard service to use for request rate limiting
 | 
			
		||||
     */
 | 
			
		||||
    RPCServerHandler(
 | 
			
		||||
        util::config::ClioConfigDefinition const& config,
 | 
			
		||||
        std::shared_ptr<BackendInterface const> const& backend,
 | 
			
		||||
        std::shared_ptr<RPCEngineType> const& rpcEngine,
 | 
			
		||||
        std::shared_ptr<etlng::ETLServiceInterface const> const& etl,
 | 
			
		||||
        dosguard::DOSGuardInterface& dosguard
 | 
			
		||||
    )
 | 
			
		||||
        : backend_(backend)
 | 
			
		||||
        , rpcEngine_(rpcEngine)
 | 
			
		||||
        , etl_(etl)
 | 
			
		||||
        , dosguard_(dosguard)
 | 
			
		||||
        , tagFactory_(config)
 | 
			
		||||
        , apiVersionParser_(config.getObject("api_version"))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief The callback when server receives a request.
 | 
			
		||||
     *
 | 
			
		||||
     * @param request The request
 | 
			
		||||
     * @param connectionMetadata The connection metadata
 | 
			
		||||
     * @param subscriptionContext The subscription context
 | 
			
		||||
     * @param yield The yield context
 | 
			
		||||
     * @return The response
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    operator()(
 | 
			
		||||
        Request const& request,
 | 
			
		||||
        ConnectionMetadata const& connectionMetadata,
 | 
			
		||||
        SubscriptionContextPtr subscriptionContext,
 | 
			
		||||
        boost::asio::yield_context yield
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (not dosguard_.get().isOk(connectionMetadata.ip())) {
 | 
			
		||||
            return makeSlowDownResponse(request, std::nullopt);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        std::optional<Response> response;
 | 
			
		||||
        util::CoroutineGroup coroutineGroup{yield, 1};
 | 
			
		||||
        auto const onTaskComplete = coroutineGroup.registerForeign(yield);
 | 
			
		||||
        ASSERT(onTaskComplete.has_value(), "Coroutine group can't be full");
 | 
			
		||||
 | 
			
		||||
        bool const postSuccessful = rpcEngine_->post(
 | 
			
		||||
            [this,
 | 
			
		||||
             &request,
 | 
			
		||||
             &response,
 | 
			
		||||
             &onTaskComplete = onTaskComplete.value(),
 | 
			
		||||
             &connectionMetadata,
 | 
			
		||||
             subscriptionContext = std::move(subscriptionContext)](boost::asio::yield_context innerYield) mutable {
 | 
			
		||||
                try {
 | 
			
		||||
                    boost::system::error_code ec;
 | 
			
		||||
                    auto parsedRequest = boost::json::parse(request.message(), ec);
 | 
			
		||||
                    if (ec.failed() or not parsedRequest.is_object()) {
 | 
			
		||||
                        rpcEngine_->notifyBadSyntax();
 | 
			
		||||
                        response = impl::ErrorHelper{request}.makeJsonParsingError();
 | 
			
		||||
                        if (ec.failed()) {
 | 
			
		||||
                            LOG(log_.warn())
 | 
			
		||||
                                << "Error parsing JSON: " << ec.message() << ". For request: " << request.message();
 | 
			
		||||
                        } else {
 | 
			
		||||
                            LOG(log_.warn()) << "Received not a JSON object. For request: " << request.message();
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        auto parsedObject = std::move(parsedRequest).as_object();
 | 
			
		||||
 | 
			
		||||
                        if (not dosguard_.get().request(connectionMetadata.ip(), parsedObject)) {
 | 
			
		||||
                            response = makeSlowDownResponse(request, parsedObject);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            LOG(perfLog_.debug()) << connectionMetadata.tag() << "Adding to work queue";
 | 
			
		||||
 | 
			
		||||
                            if (not connectionMetadata.wasUpgraded() and shouldReplaceParams(parsedObject))
 | 
			
		||||
                                parsedObject[JS(params)] = boost::json::array({boost::json::object{}});
 | 
			
		||||
 | 
			
		||||
                            response = handleRequest(
 | 
			
		||||
                                innerYield,
 | 
			
		||||
                                request,
 | 
			
		||||
                                std::move(parsedObject),
 | 
			
		||||
                                connectionMetadata,
 | 
			
		||||
                                std::move(subscriptionContext)
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (std::exception const& ex) {
 | 
			
		||||
                    LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
 | 
			
		||||
                    rpcEngine_->notifyInternalError();
 | 
			
		||||
                    response = impl::ErrorHelper{request}.makeInternalError();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // notify the coroutine group that the foreign task is done
 | 
			
		||||
                onTaskComplete();
 | 
			
		||||
            },
 | 
			
		||||
            connectionMetadata.ip()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (not postSuccessful) {
 | 
			
		||||
            // onTaskComplete must be called to notify coroutineGroup that the foreign task is done
 | 
			
		||||
            onTaskComplete->operator()();
 | 
			
		||||
            rpcEngine_->notifyTooBusy();
 | 
			
		||||
            return impl::ErrorHelper{request}.makeTooBusyError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Put the coroutine to sleep until the foreign task is done
 | 
			
		||||
        coroutineGroup.asyncWait(yield);
 | 
			
		||||
        ASSERT(response.has_value(), "Woke up coroutine without setting response");
 | 
			
		||||
 | 
			
		||||
        if (not dosguard_.get().add(connectionMetadata.ip(), response->message().size())) {
 | 
			
		||||
            response->setMessage(makeLoadWarning(*response));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return std::move(response).value();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    Response
 | 
			
		||||
    handleRequest(
 | 
			
		||||
        boost::asio::yield_context yield,
 | 
			
		||||
        Request const& rawRequest,
 | 
			
		||||
        boost::json::object&& request,
 | 
			
		||||
        ConnectionMetadata const& connectionMetadata,
 | 
			
		||||
        SubscriptionContextPtr subscriptionContext
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        LOG(log_.info()) << connectionMetadata.tag() << (connectionMetadata.wasUpgraded() ? "ws" : "http")
 | 
			
		||||
                         << " received request from work queue: " << util::removeSecret(request)
 | 
			
		||||
                         << " ip = " << connectionMetadata.ip();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            auto const range = backend_->fetchLedgerRange();
 | 
			
		||||
            if (!range) {
 | 
			
		||||
                // for error that happened before the handler, we don't attach any warnings
 | 
			
		||||
                rpcEngine_->notifyNotReady();
 | 
			
		||||
                return impl::ErrorHelper{rawRequest, std::move(request)}.makeNotReadyError();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto const context = [&] {
 | 
			
		||||
                if (connectionMetadata.wasUpgraded()) {
 | 
			
		||||
                    ASSERT(subscriptionContext != nullptr, "Subscription context must exist for a WS connection");
 | 
			
		||||
                    return rpc::makeWsContext(
 | 
			
		||||
                        yield,
 | 
			
		||||
                        request,
 | 
			
		||||
                        std::move(subscriptionContext),
 | 
			
		||||
                        tagFactory_.with(connectionMetadata.tag()),
 | 
			
		||||
                        *range,
 | 
			
		||||
                        connectionMetadata.ip(),
 | 
			
		||||
                        std::cref(apiVersionParser_),
 | 
			
		||||
                        connectionMetadata.isAdmin()
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                return rpc::makeHttpContext(
 | 
			
		||||
                    yield,
 | 
			
		||||
                    request,
 | 
			
		||||
                    tagFactory_.with(connectionMetadata.tag()),
 | 
			
		||||
                    *range,
 | 
			
		||||
                    connectionMetadata.ip(),
 | 
			
		||||
                    std::cref(apiVersionParser_),
 | 
			
		||||
                    connectionMetadata.isAdmin()
 | 
			
		||||
                );
 | 
			
		||||
            }();
 | 
			
		||||
 | 
			
		||||
            if (!context) {
 | 
			
		||||
                auto const err = context.error();
 | 
			
		||||
                LOG(perfLog_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
 | 
			
		||||
                LOG(log_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
 | 
			
		||||
 | 
			
		||||
                // we count all those as BadSyntax - as the WS path would.
 | 
			
		||||
                // Although over HTTP these will yield a 400 status with a plain text response (for most).
 | 
			
		||||
                rpcEngine_->notifyBadSyntax();
 | 
			
		||||
                return impl::ErrorHelper(rawRequest, std::move(request)).makeError(err);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
 | 
			
		||||
 | 
			
		||||
            auto us = std::chrono::duration<int, std::milli>(timeDiff);
 | 
			
		||||
            rpc::logDuration(request, context->tag(), us);
 | 
			
		||||
 | 
			
		||||
            boost::json::object response;
 | 
			
		||||
 | 
			
		||||
            if (!result.response.has_value()) {
 | 
			
		||||
                // note: error statuses are counted/notified in buildResponse itself
 | 
			
		||||
                response = impl::ErrorHelper(rawRequest, request).composeError(result.response.error());
 | 
			
		||||
                auto const responseStr = boost::json::serialize(response);
 | 
			
		||||
 | 
			
		||||
                LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
 | 
			
		||||
                LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr;
 | 
			
		||||
            } else {
 | 
			
		||||
                // This can still technically be an error. Clio counts forwarded requests as successful.
 | 
			
		||||
                rpcEngine_->notifyComplete(context->method, us);
 | 
			
		||||
 | 
			
		||||
                auto& json = result.response.value();
 | 
			
		||||
                auto const isForwarded =
 | 
			
		||||
                    json.contains("forwarded") && json.at("forwarded").is_bool() && json.at("forwarded").as_bool();
 | 
			
		||||
 | 
			
		||||
                if (isForwarded)
 | 
			
		||||
                    json.erase("forwarded");
 | 
			
		||||
 | 
			
		||||
                // if the result is forwarded - just use it as is
 | 
			
		||||
                // if forwarded request has error, for http, error should be in "result"; for ws, error should
 | 
			
		||||
                // be at top
 | 
			
		||||
                if (isForwarded && (json.contains(JS(result)) || connectionMetadata.wasUpgraded())) {
 | 
			
		||||
                    for (auto const& [k, v] : json)
 | 
			
		||||
                        response.insert_or_assign(k, v);
 | 
			
		||||
                } else {
 | 
			
		||||
                    response[JS(result)] = json;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (isForwarded)
 | 
			
		||||
                    response["forwarded"] = true;
 | 
			
		||||
 | 
			
		||||
                // for ws there is an additional field "status" in the response,
 | 
			
		||||
                // otherwise the "status" is in the "result" field
 | 
			
		||||
                if (connectionMetadata.wasUpgraded()) {
 | 
			
		||||
                    auto const appendFieldIfExist = [&](auto const& field) {
 | 
			
		||||
                        if (request.contains(field) and not request.at(field).is_null())
 | 
			
		||||
                            response[field] = request.at(field);
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    appendFieldIfExist(JS(id));
 | 
			
		||||
                    appendFieldIfExist(JS(api_version));
 | 
			
		||||
 | 
			
		||||
                    if (!response.contains(JS(error)))
 | 
			
		||||
                        response[JS(status)] = JS(success);
 | 
			
		||||
 | 
			
		||||
                    response[JS(type)] = JS(response);
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (response.contains(JS(result)) && !response[JS(result)].as_object().contains(JS(error)))
 | 
			
		||||
                        response[JS(result)].as_object()[JS(status)] = JS(success);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            boost::json::array warnings = std::move(result.warnings);
 | 
			
		||||
            warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcClio));
 | 
			
		||||
 | 
			
		||||
            if (etl_->lastCloseAgeSeconds() >= 60)
 | 
			
		||||
                warnings.emplace_back(rpc::makeWarning(rpc::WarnRpcOutdated));
 | 
			
		||||
 | 
			
		||||
            response["warnings"] = warnings;
 | 
			
		||||
            return Response{boost::beast::http::status::ok, response, rawRequest};
 | 
			
		||||
        } catch (std::exception const& ex) {
 | 
			
		||||
            // note: while we are catching this in buildResponse too, this is here to make sure
 | 
			
		||||
            // that any other code that may throw is outside of buildResponse is also worked around.
 | 
			
		||||
            LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
 | 
			
		||||
            LOG(log_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
 | 
			
		||||
 | 
			
		||||
            rpcEngine_->notifyInternalError();
 | 
			
		||||
            return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static Response
 | 
			
		||||
    makeSlowDownResponse(Request const& request, std::optional<boost::json::value> requestJson)
 | 
			
		||||
    {
 | 
			
		||||
        auto error = rpc::makeError(rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
 | 
			
		||||
        if (not request.isHttp()) {
 | 
			
		||||
            try {
 | 
			
		||||
                if (not requestJson.has_value()) {
 | 
			
		||||
                    requestJson = boost::json::parse(request.message());
 | 
			
		||||
                }
 | 
			
		||||
                if (requestJson->is_object() && requestJson->as_object().contains("id"))
 | 
			
		||||
                    error["id"] = requestJson->as_object().at("id");
 | 
			
		||||
                error["request"] = request.message();
 | 
			
		||||
            } catch (std::exception const&) {
 | 
			
		||||
                error["request"] = request.message();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return web::ng::Response{boost::beast::http::status::service_unavailable, error, request};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static boost::json::object
 | 
			
		||||
    makeLoadWarning(Response const& response)
 | 
			
		||||
    {
 | 
			
		||||
        auto jsonResponse = boost::json::parse(response.message()).as_object();
 | 
			
		||||
        jsonResponse["warning"] = "load";
 | 
			
		||||
        if (jsonResponse.contains("warnings") && jsonResponse["warnings"].is_array()) {
 | 
			
		||||
            jsonResponse["warnings"].as_array().push_back(rpc::makeWarning(rpc::WarnRpcRateLimit));
 | 
			
		||||
        } else {
 | 
			
		||||
            jsonResponse["warnings"] = boost::json::array{rpc::makeWarning(rpc::WarnRpcRateLimit)};
 | 
			
		||||
        }
 | 
			
		||||
        return jsonResponse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bool
 | 
			
		||||
    shouldReplaceParams(boost::json::object const& req) const
 | 
			
		||||
    {
 | 
			
		||||
        auto const hasParams = req.contains(JS(params));
 | 
			
		||||
        auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
 | 
			
		||||
        auto const paramsIsEmptyString =
 | 
			
		||||
            hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
 | 
			
		||||
        auto const paramsIsEmptyObject =
 | 
			
		||||
            hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
 | 
			
		||||
        auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
 | 
			
		||||
        auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
 | 
			
		||||
        auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
 | 
			
		||||
        auto const firstArgIsNull = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
 | 
			
		||||
        auto const firstArgIsEmptyString = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_string() and
 | 
			
		||||
            req.at(JS(params)).as_array().at(0).as_string().empty();
 | 
			
		||||
 | 
			
		||||
        // Note: all this compatibility dance is to match `rippled` as close as possible
 | 
			
		||||
        return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or arrayIsEmpty or
 | 
			
		||||
            firstArgIsEmptyString or firstArgIsNull;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
@@ -1,192 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/MessageHandler.hpp"
 | 
			
		||||
#include "web/ng/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/ConnectionHandler.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
 | 
			
		||||
#include <concepts>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A tag class for server to help identify Server in templated code.
 | 
			
		||||
 */
 | 
			
		||||
struct ServerTag {
 | 
			
		||||
    virtual ~ServerTag() = default;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept SomeServer = std::derived_from<T, ServerTag>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Web server class.
 | 
			
		||||
 */
 | 
			
		||||
class Server : public ServerTag {
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Check to perform for each new client connection. The check takes client ip as input and returns a Response
 | 
			
		||||
     * if the check failed. Response will be sent to the client and the connection will be closed.
 | 
			
		||||
     */
 | 
			
		||||
    using OnConnectCheck = std::function<std::expected<void, Response>(Connection const&)>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Hook called when any connection disconnects
 | 
			
		||||
     */
 | 
			
		||||
    using OnDisconnectHook = impl::ConnectionHandler::OnDisconnectHook;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    util::Logger log_{"WebServer"};
 | 
			
		||||
    util::Logger perfLog_{"Performance"};
 | 
			
		||||
 | 
			
		||||
    std::reference_wrapper<boost::asio::io_context> ctx_;
 | 
			
		||||
    std::optional<boost::asio::ssl::context> sslContext_;
 | 
			
		||||
 | 
			
		||||
    util::TagDecoratorFactory tagDecoratorFactory_;
 | 
			
		||||
 | 
			
		||||
    impl::ConnectionHandler connectionHandler_;
 | 
			
		||||
    boost::asio::ip::tcp::endpoint endpoint_;
 | 
			
		||||
 | 
			
		||||
    OnConnectCheck onConnectCheck_;
 | 
			
		||||
 | 
			
		||||
    bool running_{false};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Server object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ctx The boost::asio::io_context to use.
 | 
			
		||||
     * @param endpoint The endpoint to listen on.
 | 
			
		||||
     * @param sslContext The SSL context to use (optional).
 | 
			
		||||
     * @param processingPolicy The requests processing policy (parallel or sequential).
 | 
			
		||||
     * @param parallelRequestLimit The limit of requests for one connection that can be processed in parallel. Only used
 | 
			
		||||
     * if processingPolicy is parallel.
 | 
			
		||||
     * @param tagDecoratorFactory The tag decorator factory.
 | 
			
		||||
     * @param maxSubscriptionSendQueueSize The maximum size of the subscription send queue.
 | 
			
		||||
     * @param onConnectCheck The check to perform on each connection.
 | 
			
		||||
     * @param onDisconnectHook The hook to call on each disconnection.
 | 
			
		||||
     */
 | 
			
		||||
    Server(
 | 
			
		||||
        boost::asio::io_context& ctx,
 | 
			
		||||
        boost::asio::ip::tcp::endpoint endpoint,
 | 
			
		||||
        std::optional<boost::asio::ssl::context> sslContext,
 | 
			
		||||
        ProcessingPolicy processingPolicy,
 | 
			
		||||
        std::optional<size_t> parallelRequestLimit,
 | 
			
		||||
        util::TagDecoratorFactory tagDecoratorFactory,
 | 
			
		||||
        std::optional<size_t> maxSubscriptionSendQueueSize,
 | 
			
		||||
        OnConnectCheck onConnectCheck,
 | 
			
		||||
        OnDisconnectHook onDisconnectHook
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Copy constructor is deleted. The Server couldn't be copied.
 | 
			
		||||
     */
 | 
			
		||||
    Server(Server const&) = delete;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Move constructor is deleted because connectionHandler_ contains references to some fields of the Server.
 | 
			
		||||
     */
 | 
			
		||||
    Server(Server&&) = delete;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set handler for GET requests.
 | 
			
		||||
     * @note This method can't be called after run() is called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param target The target of the request.
 | 
			
		||||
     * @param handler The handler to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onGet(std::string const& target, MessageHandler handler);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set handler for POST requests.
 | 
			
		||||
     * @note This method can't be called after run() is called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param target The target of the request.
 | 
			
		||||
     * @param handler The handler to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onPost(std::string const& target, MessageHandler handler);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set handler for WebSocket requests.
 | 
			
		||||
     * @note This method can't be called after run() is called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param handler The handler to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onWs(MessageHandler handler);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Run the server.
 | 
			
		||||
     *
 | 
			
		||||
     * @return std::nullopt if the server started successfully, otherwise an error message.
 | 
			
		||||
     */
 | 
			
		||||
    std::optional<std::string>
 | 
			
		||||
    run();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Stop the server. This method will asynchronously sleep unless all the users are disconnected.
 | 
			
		||||
     * @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield The coroutine context.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    stop(boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    void
 | 
			
		||||
    handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Create a new Server.
 | 
			
		||||
 *
 | 
			
		||||
 * @param config The configuration.
 | 
			
		||||
 * @param onConnectCheck The check to perform on each client connection.
 | 
			
		||||
 * @param onDisconnectHook The hook to call when client disconnects.
 | 
			
		||||
 * @param context The boost::asio::io_context to use.
 | 
			
		||||
 *
 | 
			
		||||
 * @return The Server or an error message.
 | 
			
		||||
 */
 | 
			
		||||
std::expected<Server, std::string>
 | 
			
		||||
makeServer(
 | 
			
		||||
    util::config::ClioConfigDefinition const& config,
 | 
			
		||||
    Server::OnConnectCheck onConnectCheck,
 | 
			
		||||
    Server::OnDisconnectHook onDisconnectHook,
 | 
			
		||||
    boost::asio::io_context& context
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
@@ -1,100 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "web/ng/SubscriptionContext.hpp"
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
 | 
			
		||||
SubscriptionContext::SubscriptionContext(
 | 
			
		||||
    util::TagDecoratorFactory const& factory,
 | 
			
		||||
    impl::WsConnectionBase& connection,
 | 
			
		||||
    std::optional<size_t> maxSendQueueSize,
 | 
			
		||||
    boost::asio::yield_context yield,
 | 
			
		||||
    ErrorHandler errorHandler
 | 
			
		||||
)
 | 
			
		||||
    : web::SubscriptionContextInterface(factory)
 | 
			
		||||
    , connection_(connection)
 | 
			
		||||
    , maxSendQueueSize_(maxSendQueueSize)
 | 
			
		||||
    , tasksGroup_(yield)
 | 
			
		||||
    , yield_(yield)
 | 
			
		||||
    , errorHandler_(std::move(errorHandler))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::send(std::shared_ptr<std::string> message)
 | 
			
		||||
{
 | 
			
		||||
    if (disconnected_)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    if (maxSendQueueSize_.has_value() and tasksGroup_.size() >= *maxSendQueueSize_) {
 | 
			
		||||
        tasksGroup_.spawn(yield_, [this](boost::asio::yield_context innerYield) {
 | 
			
		||||
            connection_.get().close(innerYield);
 | 
			
		||||
        });
 | 
			
		||||
        disconnected_ = true;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tasksGroup_.spawn(yield_, [this, message = std::move(message)](boost::asio::yield_context innerYield) {
 | 
			
		||||
        auto const maybeError = connection_.get().sendBuffer(boost::asio::buffer(*message), innerYield);
 | 
			
		||||
        if (maybeError.has_value() and errorHandler_(*maybeError, connection_))
 | 
			
		||||
            connection_.get().close(innerYield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
 | 
			
		||||
{
 | 
			
		||||
    onDisconnect_.connect(slot);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::setApiSubversion(uint32_t value)
 | 
			
		||||
{
 | 
			
		||||
    apiSubversion_ = value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t
 | 
			
		||||
SubscriptionContext::apiSubversion() const
 | 
			
		||||
{
 | 
			
		||||
    return apiSubversion_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
SubscriptionContext::disconnect(boost::asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    onDisconnect_(this);
 | 
			
		||||
    disconnected_ = true;
 | 
			
		||||
    tasksGroup_.asyncWait(yield);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
@@ -1,132 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/CoroutineGroup.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/impl/WsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/any_io_executor.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/signals2/variadic_signal.hpp>
 | 
			
		||||
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace web::ng {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Implementation of SubscriptionContextInterface.
 | 
			
		||||
 * @note This class is designed to be used with SubscriptionManager. The class is safe to use from multiple threads.
 | 
			
		||||
 * The method disconnect() must be called before the object is destroyed.
 | 
			
		||||
 */
 | 
			
		||||
class SubscriptionContext : public web::SubscriptionContextInterface {
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Error handler definition. Error handler returns true if connection should be closed false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    using ErrorHandler = std::function<bool(Error const&, Connection const&)>;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    std::reference_wrapper<impl::WsConnectionBase> connection_;
 | 
			
		||||
    std::optional<size_t> maxSendQueueSize_;
 | 
			
		||||
    util::CoroutineGroup tasksGroup_;
 | 
			
		||||
    boost::asio::yield_context yield_;
 | 
			
		||||
    ErrorHandler errorHandler_;
 | 
			
		||||
 | 
			
		||||
    boost::signals2::signal<void(SubscriptionContextInterface*)> onDisconnect_;
 | 
			
		||||
    std::atomic_bool disconnected_{false};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief The API version of the web stream client.
 | 
			
		||||
     * This is used to track the api version of this connection, which mainly is used by subscription. It is different
 | 
			
		||||
     * from the api version in Context, which is only used for the current request.
 | 
			
		||||
     */
 | 
			
		||||
    std::atomic_uint32_t apiSubversion_ = 0u;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Subscription Context object
 | 
			
		||||
     *
 | 
			
		||||
     * @param factory The tag decorator factory to use to init taggable.
 | 
			
		||||
     * @param connection The connection for which the context is created.
 | 
			
		||||
     * @param maxSendQueueSize The maximum size of the send queue. If the queue is full, the connection will be closed.
 | 
			
		||||
     * @param yield The yield context to spawn sending coroutines.
 | 
			
		||||
     * @param errorHandler The error handler.
 | 
			
		||||
     */
 | 
			
		||||
    SubscriptionContext(
 | 
			
		||||
        util::TagDecoratorFactory const& factory,
 | 
			
		||||
        impl::WsConnectionBase& connection,
 | 
			
		||||
        std::optional<size_t> maxSendQueueSize,
 | 
			
		||||
        boost::asio::yield_context yield,
 | 
			
		||||
        ErrorHandler errorHandler
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Send message to the client
 | 
			
		||||
     * @note This method does nothing after disconnected() was called.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    send(std::shared_ptr<std::string> message) override;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Connect a slot to onDisconnect connection signal.
 | 
			
		||||
     *
 | 
			
		||||
     * @param slot The slot to connect.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    onDisconnect(OnDisconnectSlot const& slot) override;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the API subversion.
 | 
			
		||||
     * @param value The value to set.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    setApiSubversion(uint32_t value) override;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Get the API subversion.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The API subversion.
 | 
			
		||||
     */
 | 
			
		||||
    uint32_t
 | 
			
		||||
    apiSubversion() const override;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Notify the context that related connection is disconnected and wait for all the task to complete.
 | 
			
		||||
     * @note This method must be called before the object is destroyed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield The yield context to wait for all the tasks to complete.
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    disconnect(boost::asio::yield_context yield);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng
 | 
			
		||||
@@ -1,114 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
#include <fmt/core.h>
 | 
			
		||||
#include <xrpl/protocol/ErrorCodes.h>
 | 
			
		||||
#include <xrpl/protocol/jss.h>
 | 
			
		||||
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
namespace web::ng::impl {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
 | 
			
		||||
 */
 | 
			
		||||
class ErrorHelper {
 | 
			
		||||
    std::reference_wrapper<Request const> rawRequest_;
 | 
			
		||||
    std::optional<boost::json::object> request_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Error Helper object
 | 
			
		||||
     *
 | 
			
		||||
     * @param rawRequest The request that caused the error.
 | 
			
		||||
     * @param request The parsed request that caused the error.
 | 
			
		||||
     */
 | 
			
		||||
    ErrorHelper(Request const& rawRequest, std::optional<boost::json::object> request = std::nullopt);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make an error response from a status.
 | 
			
		||||
     *
 | 
			
		||||
     * @param err The status to make an error response from.
 | 
			
		||||
     * @return
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeError(rpc::Status const& err) const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make an internal error response.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with an internal error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeInternalError() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make a response for when the server is not ready.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with a not ready error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeNotReadyError() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make a response for when the server is too busy.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with a too busy error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeTooBusyError() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Make a response when json parsing fails.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A response with a json parsing error.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Response
 | 
			
		||||
    makeJsonParsingError() const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Compose an error into json object from a status.
 | 
			
		||||
     *
 | 
			
		||||
     * @param error The status to compose into a json object.
 | 
			
		||||
     * @return The composed json object.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] boost::json::object
 | 
			
		||||
    composeError(rpc::Status const& error) const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Compose an error into json object from a rippled error.
 | 
			
		||||
     *
 | 
			
		||||
     * @param error The rippled error to compose into a json object.
 | 
			
		||||
     * @return The composed json object.
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] boost::json::object
 | 
			
		||||
    composeError(rpc::RippledError error) const;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace web::ng::impl
 | 
			
		||||
@@ -19,11 +19,11 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/HttpConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
@@ -33,25 +33,25 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
struct MockConnectionMetadataImpl : web::ng::ConnectionMetadata {
 | 
			
		||||
    using web::ng::ConnectionMetadata::ConnectionMetadata;
 | 
			
		||||
struct MockConnectionMetadataImpl : web::ConnectionMetadata {
 | 
			
		||||
    using web::ConnectionMetadata::ConnectionMetadata;
 | 
			
		||||
    MOCK_METHOD(bool, wasUpgraded, (), (const, override));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using MockConnectionMetadata = testing::NiceMock<MockConnectionMetadataImpl>;
 | 
			
		||||
using StrictMockConnectionMetadata = testing::StrictMock<MockConnectionMetadataImpl>;
 | 
			
		||||
 | 
			
		||||
struct MockConnectionImpl : web::ng::Connection {
 | 
			
		||||
    using web::ng::Connection::Connection;
 | 
			
		||||
struct MockConnectionImpl : web::Connection {
 | 
			
		||||
    using web::Connection::Connection;
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(bool, wasUpgraded, (), (const, override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
 | 
			
		||||
 | 
			
		||||
    using SendReturnType = std::optional<web::ng::Error>;
 | 
			
		||||
    MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
 | 
			
		||||
    using SendReturnType = std::optional<web::Error>;
 | 
			
		||||
    MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
 | 
			
		||||
    using ReceiveReturnType = std::expected<web::Request, web::Error>;
 | 
			
		||||
    MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
 | 
			
		||||
@@ -21,11 +21,11 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/HttpConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
@@ -37,15 +37,15 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
 | 
			
		||||
struct MockHttpConnectionImpl : web::impl::UpgradableConnection {
 | 
			
		||||
    using UpgradableConnection::UpgradableConnection;
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(bool, wasUpgraded, (), (const, override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
 | 
			
		||||
 | 
			
		||||
    using SendReturnType = std::optional<web::ng::Error>;
 | 
			
		||||
    MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
 | 
			
		||||
    using SendReturnType = std::optional<web::Error>;
 | 
			
		||||
    MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        SendReturnType,
 | 
			
		||||
@@ -54,15 +54,15 @@ struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
 | 
			
		||||
        (override)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
 | 
			
		||||
    using ReceiveReturnType = std::expected<web::Request, web::Error>;
 | 
			
		||||
    MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    using IsUpgradeRequestedReturnType = std::expected<bool, web::ng::Error>;
 | 
			
		||||
    using IsUpgradeRequestedReturnType = std::expected<bool, web::Error>;
 | 
			
		||||
    MOCK_METHOD(IsUpgradeRequestedReturnType, isUpgradeRequested, (boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    using UpgradeReturnType = std::expected<web::ng::ConnectionPtr, web::ng::Error>;
 | 
			
		||||
    using UpgradeReturnType = std::expected<web::ConnectionPtr, web::Error>;
 | 
			
		||||
    using OptionalSslContext = std::optional<boost::asio::ssl::context>;
 | 
			
		||||
    MOCK_METHOD(
 | 
			
		||||
        UpgradeReturnType,
 | 
			
		||||
@@ -19,11 +19,11 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/WsConnection.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/WsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
@@ -34,22 +34,22 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
 | 
			
		||||
struct MockWsConnectionImpl : web::ng::impl::WsConnectionBase {
 | 
			
		||||
struct MockWsConnectionImpl : web::impl::WsConnectionBase {
 | 
			
		||||
    using WsConnectionBase::WsConnectionBase;
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(bool, wasUpgraded, (), (const, override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, setTimeout, (std::chrono::steady_clock::duration), (override));
 | 
			
		||||
 | 
			
		||||
    using SendReturnType = std::optional<web::ng::Error>;
 | 
			
		||||
    MOCK_METHOD(SendReturnType, send, (web::ng::Response, boost::asio::yield_context), (override));
 | 
			
		||||
    using SendReturnType = std::optional<web::Error>;
 | 
			
		||||
    MOCK_METHOD(SendReturnType, send, (web::Response, boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
 | 
			
		||||
    using ReceiveReturnType = std::expected<web::Request, web::Error>;
 | 
			
		||||
    MOCK_METHOD(ReceiveReturnType, receive, (boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    MOCK_METHOD(void, close, (boost::asio::yield_context), (override));
 | 
			
		||||
 | 
			
		||||
    using SendBufferReturnType = std::optional<web::ng::Error>;
 | 
			
		||||
    using SendBufferReturnType = std::optional<web::Error>;
 | 
			
		||||
    MOCK_METHOD(SendBufferReturnType, sendBuffer, (boost::asio::const_buffer, boost::asio::yield_context), (override));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -182,17 +182,16 @@ target_sources(
 | 
			
		||||
          web/dosguard/IntervalSweepHandlerTests.cpp
 | 
			
		||||
          web/dosguard/WeightsTests.cpp
 | 
			
		||||
          web/dosguard/WhitelistHandlerTests.cpp
 | 
			
		||||
          web/ResponseTests.cpp
 | 
			
		||||
          web/RequestTests.cpp
 | 
			
		||||
          web/RPCServerHandlerTests.cpp
 | 
			
		||||
          web/ServerTests.cpp
 | 
			
		||||
          web/SubscriptionContextTests.cpp
 | 
			
		||||
          web/impl/ConnectionHandlerTests.cpp
 | 
			
		||||
          web/impl/ErrorHandlingTests.cpp
 | 
			
		||||
          web/ng/ResponseTests.cpp
 | 
			
		||||
          web/ng/RequestTests.cpp
 | 
			
		||||
          web/ng/RPCServerHandlerTests.cpp
 | 
			
		||||
          web/ng/ServerTests.cpp
 | 
			
		||||
          web/ng/SubscriptionContextTests.cpp
 | 
			
		||||
          web/ng/impl/ConnectionHandlerTests.cpp
 | 
			
		||||
          web/ng/impl/ErrorHandlingTests.cpp
 | 
			
		||||
          web/ng/impl/HttpConnectionTests.cpp
 | 
			
		||||
          web/ng/impl/ServerSslContextTests.cpp
 | 
			
		||||
          web/ng/impl/WsConnectionTests.cpp
 | 
			
		||||
          web/impl/HttpConnectionTests.cpp
 | 
			
		||||
          web/impl/ServerSslContextTests.cpp
 | 
			
		||||
          web/impl/WsConnectionTests.cpp
 | 
			
		||||
          web/RPCServerHandlerTests.cpp
 | 
			
		||||
          web/ServerTests.cpp
 | 
			
		||||
          web/SubscriptionContextTests.cpp
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,6 @@ TEST_F(CliArgsTests, Parse_NoArgs)
 | 
			
		||||
    int const returnCode = 123;
 | 
			
		||||
    EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
 | 
			
		||||
        EXPECT_EQ(run.configPath, CliArgs::kDEFAULT_CONFIG_PATH);
 | 
			
		||||
        EXPECT_FALSE(run.useNgWebServer);
 | 
			
		||||
        return returnCode;
 | 
			
		||||
    });
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
@@ -64,29 +63,6 @@ TEST_F(CliArgsTests, Parse_NoArgs)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(CliArgsTests, Parse_NgWebServer)
 | 
			
		||||
{
 | 
			
		||||
    for (auto& argv : {std::array{"clio_server", "-w"}, std::array{"clio_server", "--ng-web-server"}}) {
 | 
			
		||||
        auto const action = CliArgs::parse(argv.size(), const_cast<char const**>(argv.data()));
 | 
			
		||||
 | 
			
		||||
        int const returnCode = 123;
 | 
			
		||||
        EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
 | 
			
		||||
            EXPECT_EQ(run.configPath, CliArgs::kDEFAULT_CONFIG_PATH);
 | 
			
		||||
            EXPECT_TRUE(run.useNgWebServer);
 | 
			
		||||
            return returnCode;
 | 
			
		||||
        });
 | 
			
		||||
        EXPECT_EQ(
 | 
			
		||||
            action.apply(
 | 
			
		||||
                onRunMock.AsStdFunction(),
 | 
			
		||||
                onExitMock.AsStdFunction(),
 | 
			
		||||
                onMigrateMock.AsStdFunction(),
 | 
			
		||||
                onVerifyMock.AsStdFunction()
 | 
			
		||||
            ),
 | 
			
		||||
            returnCode
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(CliArgsTests, Parse_VersionHelp)
 | 
			
		||||
{
 | 
			
		||||
    for (auto& argv :
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
#include "util/MockPrometheus.hpp"
 | 
			
		||||
#include "util/MockSubscriptionManager.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "web/ng/Server.hpp"
 | 
			
		||||
#include "web/Server.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/executor_work_guard.hpp>
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
@@ -62,7 +62,7 @@ TEST_F(StopperTest, stopCalledMultipleTimes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
 | 
			
		||||
    struct ServerMock : web::ng::ServerTag {
 | 
			
		||||
    struct ServerMock : web::ServerTag {
 | 
			
		||||
        MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,12 +27,12 @@
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/AdminVerificationStrategy.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/MockConnection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardMock.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/MockConnection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
@@ -108,7 +108,7 @@ struct MetricsHandlerTests : util::prometheus::WithPrometheus, SyncAsioContextTe
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MetricsHandler metricsHandler{adminVerifier};
 | 
			
		||||
    web::ng::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
 | 
			
		||||
    web::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(MetricsHandlerTests, Call)
 | 
			
		||||
@@ -122,7 +122,7 @@ TEST_F(MetricsHandlerTests, Call)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct HealthCheckHandlerTests : SyncAsioContextTest, WebHandlersTest {
 | 
			
		||||
    web::ng::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
 | 
			
		||||
    web::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
 | 
			
		||||
    HealthCheckHandler healthCheckHandler;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -142,19 +142,19 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
 | 
			
		||||
 | 
			
		||||
    struct RpcHandlerMock {
 | 
			
		||||
        MOCK_METHOD(
 | 
			
		||||
            web::ng::Response,
 | 
			
		||||
            web::Response,
 | 
			
		||||
            call,
 | 
			
		||||
            (web::ng::Request const&,
 | 
			
		||||
             web::ng::ConnectionMetadata const&,
 | 
			
		||||
            (web::Request const&,
 | 
			
		||||
             web::ConnectionMetadata const&,
 | 
			
		||||
             web::SubscriptionContextPtr,
 | 
			
		||||
             boost::asio::yield_context),
 | 
			
		||||
            ()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        web::ng::Response
 | 
			
		||||
        web::Response
 | 
			
		||||
        operator()(
 | 
			
		||||
            web::ng::Request const& request,
 | 
			
		||||
            web::ng::ConnectionMetadata const& connectionMetadata,
 | 
			
		||||
            web::Request const& request,
 | 
			
		||||
            web::ConnectionMetadata const& connectionMetadata,
 | 
			
		||||
            web::SubscriptionContextPtr subscriptionContext,
 | 
			
		||||
            boost::asio::yield_context yield
 | 
			
		||||
        )
 | 
			
		||||
@@ -170,7 +170,7 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestHandlerTest, RpcHandlerThrows)
 | 
			
		||||
{
 | 
			
		||||
    web::ng::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
 | 
			
		||||
    web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*adminVerifier, isAdmin).WillOnce(testing::Return(true));
 | 
			
		||||
    EXPECT_CALL(rpcHandler, call).WillOnce(testing::Throw(std::runtime_error{"some error"}));
 | 
			
		||||
@@ -191,9 +191,9 @@ TEST_F(RequestHandlerTest, RpcHandlerThrows)
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestHandlerTest, NoErrors)
 | 
			
		||||
{
 | 
			
		||||
    web::ng::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
 | 
			
		||||
    web::ng::Response const response{http::status::ok, "some response", request};
 | 
			
		||||
    auto const httpResponse = web::ng::Response{response}.intoHttpResponse();
 | 
			
		||||
    web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
 | 
			
		||||
    web::Response const response{http::status::ok, "some response", request};
 | 
			
		||||
    auto const httpResponse = web::Response{response}.intoHttpResponse();
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*adminVerifier, isAdmin).WillOnce(testing::Return(true));
 | 
			
		||||
    EXPECT_CALL(rpcHandler, call).WillOnce(testing::Return(response));
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -18,7 +18,7 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/NameGenerator.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace web;
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
struct RequestTest : public ::testing::Test {
 | 
			
		||||
@@ -23,9 +23,9 @@
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/ng/MockConnection.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/MockConnection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
@@ -42,7 +42,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace web;
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -17,62 +17,156 @@
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/LoggerFixtures.hpp"
 | 
			
		||||
#include "util/AsioContextTestFixture.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/SubscriptionContext.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBaseMock.hpp"
 | 
			
		||||
#include "web/impl/MockWsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/post.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/system/errc.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
using namespace web;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
struct SubscriptionContextTests : NoLoggerFixture {
 | 
			
		||||
struct SubscriptionContextTests : SyncAsioContextTest {
 | 
			
		||||
    SubscriptionContext
 | 
			
		||||
    makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
 | 
			
		||||
    {
 | 
			
		||||
        return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
 | 
			
		||||
    }};
 | 
			
		||||
    ConnectionBaseStrictMockPtr connection_ =
 | 
			
		||||
        std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, "some ip");
 | 
			
		||||
 | 
			
		||||
    SubscriptionContext subscriptionContext_{tagFactory_, connection_};
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<void(SubscriptionContextInterface*)>> callbackMock_;
 | 
			
		||||
    MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<bool(web::Error const&, Connection const&)>> errorHandler_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, send)
 | 
			
		||||
TEST_F(SubscriptionContextTests, Send)
 | 
			
		||||
{
 | 
			
		||||
    auto message = std::make_shared<std::string>("message");
 | 
			
		||||
    EXPECT_CALL(*connection_, send(message));
 | 
			
		||||
    subscriptionContext_.send(message);
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("some message");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
            EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
 | 
			
		||||
            return std::nullopt;
 | 
			
		||||
        });
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, sendConnectionExpired)
 | 
			
		||||
TEST_F(SubscriptionContextTests, SendOrder)
 | 
			
		||||
{
 | 
			
		||||
    auto message = std::make_shared<std::string>("message");
 | 
			
		||||
    connection_.reset();
 | 
			
		||||
    subscriptionContext_.send(message);
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message1 = std::make_shared<std::string>("message1");
 | 
			
		||||
        auto const message2 = std::make_shared<std::string>("message2");
 | 
			
		||||
 | 
			
		||||
        testing::Sequence const sequence;
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer)
 | 
			
		||||
            .InSequence(sequence)
 | 
			
		||||
            .WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
                EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            });
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer)
 | 
			
		||||
            .InSequence(sequence)
 | 
			
		||||
            .WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
                EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        subscriptionContext.send(message1);
 | 
			
		||||
        subscriptionContext.send(message2);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, onDisconnect)
 | 
			
		||||
TEST_F(SubscriptionContextTests, SendFailed)
 | 
			
		||||
{
 | 
			
		||||
    auto localContext = std::make_unique<SubscriptionContext>(tagFactory_, connection_);
 | 
			
		||||
    localContext->onDisconnect(callbackMock_.AsStdFunction());
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("some message");
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(callbackMock_, Call(localContext.get()));
 | 
			
		||||
    localContext.reset();
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
            EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
 | 
			
		||||
            return boost::system::errc::make_error_code(boost::system::errc::not_supported);
 | 
			
		||||
        });
 | 
			
		||||
        EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
 | 
			
		||||
        EXPECT_CALL(connection_, close);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, setApiSubversion)
 | 
			
		||||
TEST_F(SubscriptionContextTests, SendTooManySubscriptions)
 | 
			
		||||
{
 | 
			
		||||
    EXPECT_EQ(subscriptionContext_.apiSubversion(), 0);
 | 
			
		||||
    subscriptionContext_.setApiSubversion(42);
 | 
			
		||||
    EXPECT_EQ(subscriptionContext_.apiSubversion(), 42);
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield, 1);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("message1");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer)
 | 
			
		||||
            .WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
 | 
			
		||||
                boost::asio::post(innerYield);  // simulate send is slow by switching to another coroutine
 | 
			
		||||
                EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            });
 | 
			
		||||
        EXPECT_CALL(connection_, close);
 | 
			
		||||
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, SendAfterDisconnect)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("some message");
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, OnDisconnect)
 | 
			
		||||
{
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
 | 
			
		||||
        EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(SubscriptionContextTests, SetApiSubversion)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        subscriptionContext.setApiSubversion(42);
 | 
			
		||||
        EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,15 +24,15 @@
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/Connection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/ConnectionHandler.hpp"
 | 
			
		||||
#include "web/ng/impl/MockHttpConnection.hpp"
 | 
			
		||||
#include "web/ng/impl/MockWsConnection.hpp"
 | 
			
		||||
#include "web/impl/ConnectionHandler.hpp"
 | 
			
		||||
#include "web/impl/MockHttpConnection.hpp"
 | 
			
		||||
#include "web/impl/MockWsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/error.hpp>
 | 
			
		||||
@@ -57,8 +57,8 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng::impl;
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace web::impl;
 | 
			
		||||
using namespace web;
 | 
			
		||||
using namespace util;
 | 
			
		||||
using testing::Return;
 | 
			
		||||
namespace beast = boost::beast;
 | 
			
		||||
@@ -20,101 +20,213 @@
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "util/LoggerFixtures.hpp"
 | 
			
		||||
#include "util/NameGenerator.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/impl/ErrorHandling.hpp"
 | 
			
		||||
#include "web/interface/ConnectionBaseMock.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/http/verb.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
using namespace web::impl;
 | 
			
		||||
using namespace web;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingTests : NoLoggerFixture {
 | 
			
		||||
protected:
 | 
			
		||||
    util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
 | 
			
		||||
    }};
 | 
			
		||||
    std::string const clientIp_ = "some ip";
 | 
			
		||||
    ConnectionBaseStrictMockPtr connection_ =
 | 
			
		||||
        std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, clientIp_);
 | 
			
		||||
    static Request
 | 
			
		||||
    makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
 | 
			
		||||
    {
 | 
			
		||||
        if (isHttp)
 | 
			
		||||
            return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
 | 
			
		||||
        static Request::HttpHeaders const kHEADERS;
 | 
			
		||||
        return Request{body.value_or(""), kHEADERS};
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingComposeErrorTestBundle {
 | 
			
		||||
struct ErrorHandlingMakeErrorTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    bool connectionUpgraded;
 | 
			
		||||
    std::optional<boost::json::object> request;
 | 
			
		||||
    bool isHttp;
 | 
			
		||||
    rpc::Status status;
 | 
			
		||||
    std::string expectedMessage;
 | 
			
		||||
    boost::beast::http::status expectedStatus;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingMakeErrorTest : ErrorHandlingTests,
 | 
			
		||||
                                    testing::WithParamInterface<ErrorHandlingMakeErrorTestBundle> {};
 | 
			
		||||
 | 
			
		||||
TEST_P(ErrorHandlingMakeErrorTest, MakeError)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(GetParam().isHttp);
 | 
			
		||||
    ErrorHelper const errorHelper{request};
 | 
			
		||||
 | 
			
		||||
    auto response = errorHelper.makeError(GetParam().status);
 | 
			
		||||
    EXPECT_EQ(response.message(), GetParam().expectedMessage);
 | 
			
		||||
    if (GetParam().isHttp) {
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
 | 
			
		||||
 | 
			
		||||
        std::string expectedContentType = "text/html";
 | 
			
		||||
        if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
 | 
			
		||||
            expectedContentType = "application/json";
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    ErrorHandlingMakeErrorTestGroup,
 | 
			
		||||
    ErrorHandlingMakeErrorTest,
 | 
			
		||||
    testing::ValuesIn({
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "WsRequest",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::RippledError::rpcTOO_BUSY},
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
 | 
			
		||||
            boost::beast::http::status::ok
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_InvalidApiVersion",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
 | 
			
		||||
            "invalid_API_version",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_CommandIsMissing",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandIsMissing},
 | 
			
		||||
            "Null method",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_CommandIsEmpty",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
 | 
			
		||||
            "method is empty",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_CommandNotString",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandNotString},
 | 
			
		||||
            "method is not string",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_ParamsUnparsable",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcParamsUnparsable},
 | 
			
		||||
            "params unparsable",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_RippledError",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::RippledError::rpcTOO_BUSY},
 | 
			
		||||
            R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
    }),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingMakeInternalErrorTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    bool isHttp;
 | 
			
		||||
    std::optional<std::string> request;
 | 
			
		||||
    boost::json::object expectedResult;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
 | 
			
		||||
                                       testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
 | 
			
		||||
struct ErrorHandlingMakeInternalErrorTest : ErrorHandlingTests,
 | 
			
		||||
                                            testing::WithParamInterface<ErrorHandlingMakeInternalErrorTestBundle> {};
 | 
			
		||||
 | 
			
		||||
TEST_P(ErrorHandlingComposeErrorTest, composeError)
 | 
			
		||||
TEST_P(ErrorHandlingMakeInternalErrorTest, ComposeError)
 | 
			
		||||
{
 | 
			
		||||
    connection_->upgraded = GetParam().connectionUpgraded;
 | 
			
		||||
    ErrorHelper const errorHelper{connection_, GetParam().request};
 | 
			
		||||
    auto const result = errorHelper.composeError(rpc::RippledError::rpcNOT_READY);
 | 
			
		||||
    EXPECT_EQ(boost::json::serialize(result), boost::json::serialize(GetParam().expectedResult));
 | 
			
		||||
    auto const request = makeRequest(GetParam().isHttp, GetParam().request);
 | 
			
		||||
    std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
 | 
			
		||||
        ? std::make_optional(boost::json::parse(*GetParam().request).as_object())
 | 
			
		||||
        : std::nullopt;
 | 
			
		||||
    ErrorHelper const errorHelper{request, requestJson};
 | 
			
		||||
 | 
			
		||||
    auto response = errorHelper.makeInternalError();
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
 | 
			
		||||
    if (GetParam().isHttp) {
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
 | 
			
		||||
        EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    ErrorHandlingComposeErrorTestGroup,
 | 
			
		||||
    ErrorHandlingComposeErrorTest,
 | 
			
		||||
    ErrorHandlingMakeInternalErrorTest,
 | 
			
		||||
    testing::ValuesIn(
 | 
			
		||||
        {ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "NoRequest_UpgradedConnection",
 | 
			
		||||
             true,
 | 
			
		||||
        {ErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "NoRequest_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             {{"error", "notReady"},
 | 
			
		||||
              {"error_code", 13},
 | 
			
		||||
              {"error_message", "Not ready to handle this request."},
 | 
			
		||||
             {{"error", "internal"},
 | 
			
		||||
              {"error_code", 73},
 | 
			
		||||
              {"error_message", "Internal error."},
 | 
			
		||||
              {"status", "error"},
 | 
			
		||||
              {"type", "response"}}
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "NoRequest_NotUpgradedConnection",
 | 
			
		||||
             false,
 | 
			
		||||
         ErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "NoRequest_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             {{"result",
 | 
			
		||||
               {{"error", "notReady"},
 | 
			
		||||
                {"error_code", 13},
 | 
			
		||||
                {"error_message", "Not ready to handle this request."},
 | 
			
		||||
               {{"error", "internal"},
 | 
			
		||||
                {"error_code", 73},
 | 
			
		||||
                {"error_message", "Internal error."},
 | 
			
		||||
                {"status", "error"},
 | 
			
		||||
                {"type", "response"}}}}
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_UpgradedConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             boost::json::object{{"id", 1}, {"api_version", 2}},
 | 
			
		||||
             {{"error", "notReady"},
 | 
			
		||||
              {"error_code", 13},
 | 
			
		||||
              {"error_message", "Not ready to handle this request."},
 | 
			
		||||
         ErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
 | 
			
		||||
             {{"error", "internal"},
 | 
			
		||||
              {"error_code", 73},
 | 
			
		||||
              {"error_message", "Internal error."},
 | 
			
		||||
              {"status", "error"},
 | 
			
		||||
              {"type", "response"},
 | 
			
		||||
              {"id", 1},
 | 
			
		||||
              {"api_version", 2},
 | 
			
		||||
              {"request", {{"id", 1}, {"api_version", 2}}}}
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_NotUpgradedConnection",
 | 
			
		||||
         ErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection_NoId",
 | 
			
		||||
             false,
 | 
			
		||||
             boost::json::object{{"id", 1}, {"api_version", 2}},
 | 
			
		||||
             std::string{R"JSON({"api_version": 2})JSON"},
 | 
			
		||||
             {{"error", "internal"},
 | 
			
		||||
              {"error_code", 73},
 | 
			
		||||
              {"error_message", "Internal error."},
 | 
			
		||||
              {"status", "error"},
 | 
			
		||||
              {"type", "response"},
 | 
			
		||||
              {"api_version", 2},
 | 
			
		||||
              {"request", {{"api_version", 2}}}}
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "Request_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
 | 
			
		||||
             {{"result",
 | 
			
		||||
               {{"error", "notReady"},
 | 
			
		||||
                {"error_code", 13},
 | 
			
		||||
                {"error_message", "Not ready to handle this request."},
 | 
			
		||||
               {{"error", "internal"},
 | 
			
		||||
                {"error_code", 73},
 | 
			
		||||
                {"error_message", "Internal error."},
 | 
			
		||||
                {"status", "error"},
 | 
			
		||||
                {"type", "response"},
 | 
			
		||||
                {"id", 1},
 | 
			
		||||
@@ -124,169 +236,122 @@ INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingSendErrorTestBundle {
 | 
			
		||||
TEST_F(ErrorHandlingTests, MakeNotReadyError)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(true);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeNotReadyError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
    EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
    EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(false);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeTooBusyError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendTooBusyError_HttpConnection)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(true);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeTooBusyError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
    EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
 | 
			
		||||
    EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(false);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeJsonParsingError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, makeJsonParsingError_HttpConnection)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(true);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeJsonParsingError();
 | 
			
		||||
    EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
 | 
			
		||||
    auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
    EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
 | 
			
		||||
    EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingComposeErrorTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    bool connectionUpgraded;
 | 
			
		||||
    rpc::Status status;
 | 
			
		||||
    bool isHttp;
 | 
			
		||||
    std::optional<boost::json::object> request;
 | 
			
		||||
    std::string expectedMessage;
 | 
			
		||||
    boost::beast::http::status expectedStatus;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ErrorHandlingSendErrorTest : ErrorHandlingTests,
 | 
			
		||||
                                    testing::WithParamInterface<ErrorHandlingSendErrorTestBundle> {};
 | 
			
		||||
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
 | 
			
		||||
                                       testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
 | 
			
		||||
 | 
			
		||||
TEST_P(ErrorHandlingSendErrorTest, sendError)
 | 
			
		||||
TEST_P(ErrorHandlingComposeErrorTest, ComposeError)
 | 
			
		||||
{
 | 
			
		||||
    connection_->upgraded = GetParam().connectionUpgraded;
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus));
 | 
			
		||||
    errorHelper.sendError(GetParam().status);
 | 
			
		||||
    auto const request = makeRequest(GetParam().isHttp);
 | 
			
		||||
    ErrorHelper const errorHelper{request, GetParam().request};
 | 
			
		||||
    auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
 | 
			
		||||
    EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    ErrorHandlingSendErrorTestGroup,
 | 
			
		||||
    ErrorHandlingSendErrorTest,
 | 
			
		||||
    testing::ValuesIn({
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "UpgradedConnection",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::RippledError::rpcTOO_BUSY},
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
 | 
			
		||||
            boost::beast::http::status::ok
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "NotUpgradedConnection_InvalidApiVersion",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
 | 
			
		||||
            "invalid_API_version",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "NotUpgradedConnection_CommandIsMissing",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandIsMissing},
 | 
			
		||||
            "Null method",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "NotUpgradedConnection_CommandIsEmpty",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
 | 
			
		||||
            "method is empty",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "NotUpgradedConnection_CommandNotString",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandNotString},
 | 
			
		||||
            "method is not string",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "NotUpgradedConnection_ParamsUnparsable",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcParamsUnparsable},
 | 
			
		||||
            "params unparsable",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        ErrorHandlingSendErrorTestBundle{
 | 
			
		||||
            "NotUpgradedConnection_RippledError",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::RippledError::rpcTOO_BUSY},
 | 
			
		||||
            R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
    }),
 | 
			
		||||
    ErrorHandlingComposeErrorTestGroup,
 | 
			
		||||
    ErrorHandlingComposeErrorTest,
 | 
			
		||||
    testing::ValuesIn(
 | 
			
		||||
        {ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "NoRequest_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "NoRequest_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             boost::json::object{{"id", 1}, {"api_version", 2}},
 | 
			
		||||
             R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})JSON",
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection_NoId",
 | 
			
		||||
             false,
 | 
			
		||||
             boost::json::object{{"api_version", 2}},
 | 
			
		||||
             R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})JSON",
 | 
			
		||||
         },
 | 
			
		||||
         ErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             boost::json::object{{"id", 1}, {"api_version", 2}},
 | 
			
		||||
             R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})JSON"
 | 
			
		||||
         }}
 | 
			
		||||
    ),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendInternalError)
 | 
			
		||||
{
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(
 | 
			
		||||
        *connection_,
 | 
			
		||||
        send(
 | 
			
		||||
            std::string{
 | 
			
		||||
                R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
 | 
			
		||||
            },
 | 
			
		||||
            boost::beast::http::status::internal_server_error
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    errorHelper.sendInternalError();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendNotReadyError)
 | 
			
		||||
{
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
    EXPECT_CALL(
 | 
			
		||||
        *connection_,
 | 
			
		||||
        send(
 | 
			
		||||
            std::string{
 | 
			
		||||
                R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
 | 
			
		||||
            },
 | 
			
		||||
            boost::beast::http::status::ok
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    errorHelper.sendNotReadyError();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendTooBusyError_UpgradedConnection)
 | 
			
		||||
{
 | 
			
		||||
    connection_->upgraded = true;
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
    EXPECT_CALL(
 | 
			
		||||
        *connection_,
 | 
			
		||||
        send(
 | 
			
		||||
            std::string{
 | 
			
		||||
                R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
 | 
			
		||||
            },
 | 
			
		||||
            boost::beast::http::status::ok
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    errorHelper.sendTooBusyError();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendTooBusyError_NotUpgradedConnection)
 | 
			
		||||
{
 | 
			
		||||
    connection_->upgraded = false;
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
    EXPECT_CALL(
 | 
			
		||||
        *connection_,
 | 
			
		||||
        send(
 | 
			
		||||
            std::string{
 | 
			
		||||
                R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
 | 
			
		||||
            },
 | 
			
		||||
            boost::beast::http::status::service_unavailable
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    errorHelper.sendTooBusyError();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendJsonParsingError_UpgradedConnection)
 | 
			
		||||
{
 | 
			
		||||
    connection_->upgraded = true;
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
    EXPECT_CALL(
 | 
			
		||||
        *connection_,
 | 
			
		||||
        send(
 | 
			
		||||
            std::string{
 | 
			
		||||
                R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
 | 
			
		||||
            },
 | 
			
		||||
            boost::beast::http::status::ok
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    errorHelper.sendJsonParsingError();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ErrorHandlingTests, sendJsonParsingError_NotUpgradedConnection)
 | 
			
		||||
{
 | 
			
		||||
    connection_->upgraded = false;
 | 
			
		||||
    ErrorHelper const errorHelper{connection_};
 | 
			
		||||
    EXPECT_CALL(
 | 
			
		||||
        *connection_,
 | 
			
		||||
        send(std::string{"Unable to parse JSON from the request"}, boost::beast::http::status::bad_request)
 | 
			
		||||
    );
 | 
			
		||||
    errorHelper.sendJsonParsingError();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,9 +25,9 @@
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/HttpConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
@@ -46,8 +46,8 @@
 | 
			
		||||
#include <ranges>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng::impl;
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace web::impl;
 | 
			
		||||
using namespace web;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
#include "util/config/ConfigFileJson.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/ng/impl/ServerSslContext.hpp"
 | 
			
		||||
#include "web/impl/ServerSslContext.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng::impl;
 | 
			
		||||
using namespace web::impl;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
struct MakeServerSslContextFromConfigTestBundle {
 | 
			
		||||
@@ -25,11 +25,11 @@
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/ng/impl/WsConnection.hpp"
 | 
			
		||||
#include "web/Error.hpp"
 | 
			
		||||
#include "web/Request.hpp"
 | 
			
		||||
#include "web/Response.hpp"
 | 
			
		||||
#include "web/impl/HttpConnection.hpp"
 | 
			
		||||
#include "web/impl/WsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/error.hpp>
 | 
			
		||||
#include <boost/asio/executor_work_guard.hpp>
 | 
			
		||||
@@ -51,8 +51,8 @@
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng::impl;
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace web::impl;
 | 
			
		||||
using namespace web;
 | 
			
		||||
using namespace util;
 | 
			
		||||
 | 
			
		||||
struct WebWsConnectionTests : SyncAsioContextTest {
 | 
			
		||||
@@ -1,611 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "rpc/common/Types.hpp"
 | 
			
		||||
#include "util/AsioContextTestFixture.hpp"
 | 
			
		||||
#include "util/MockBackendTestFixture.hpp"
 | 
			
		||||
#include "util/MockETLService.hpp"
 | 
			
		||||
#include "util/MockPrometheus.hpp"
 | 
			
		||||
#include "util/MockRPCEngine.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/dosguard/DOSGuardMock.hpp"
 | 
			
		||||
#include "web/ng/MockConnection.hpp"
 | 
			
		||||
#include "web/ng/RPCServerHandler.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/http/verb.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <stdexcept>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <unordered_set>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using testing::Return;
 | 
			
		||||
using testing::StrictMock;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
struct NgRpcServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest {
 | 
			
		||||
    ClioConfigDefinition config{ClioConfigDefinition{
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
 | 
			
		||||
        {"api_version.min", ConfigValue{ConfigType::Integer}.defaultValue(1)},
 | 
			
		||||
        {"api_version.max", ConfigValue{ConfigType::Integer}.defaultValue(2)},
 | 
			
		||||
        {"api_version.default", ConfigValue{ConfigType::Integer}.defaultValue(1)}
 | 
			
		||||
    }};
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    std::shared_ptr<testing::StrictMock<MockRPCEngine>> rpcEngine_ =
 | 
			
		||||
        std::make_shared<testing::StrictMock<MockRPCEngine>>();
 | 
			
		||||
    std::shared_ptr<StrictMock<MockETLService>> etl_ = std::make_shared<StrictMock<MockETLService>>();
 | 
			
		||||
    DOSGuardStrictMock dosguard_;
 | 
			
		||||
    RPCServerHandler<MockRPCEngine> rpcServerHandler_{config, backend_, rpcEngine_, etl_, dosguard_};
 | 
			
		||||
 | 
			
		||||
    util::TagDecoratorFactory tagFactory_{config};
 | 
			
		||||
    std::string const ip_ = "some ip";
 | 
			
		||||
    StrictMockConnectionMetadata connectionMetadata_{ip_, tagFactory_};
 | 
			
		||||
    Request::HttpHeaders const httpHeaders_;
 | 
			
		||||
 | 
			
		||||
    static Request
 | 
			
		||||
    makeHttpRequest(std::string_view body)
 | 
			
		||||
    {
 | 
			
		||||
        return Request{http::request<http::string_body>{http::verb::post, "/", 11, body}};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Request
 | 
			
		||||
    makeWsRequest(std::string body)
 | 
			
		||||
    {
 | 
			
		||||
        return Request{std::move(body), httpHeaders_};
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedHttpRequest)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const request = makeHttpRequest("some message");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const responseHttp = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
 | 
			
		||||
 | 
			
		||||
        auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsRequest)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const requestStr = "some message";
 | 
			
		||||
        auto const request = makeWsRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse());
 | 
			
		||||
 | 
			
		||||
        auto const responseJson = boost::json::parse(responseWs).as_object();
 | 
			
		||||
        EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
        EXPECT_EQ(responseJson.at("request").as_string(), requestStr);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsJsonRequest)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const requestStr = R"JSON({"request": "some message", "id": "some id"})JSON";
 | 
			
		||||
        auto const request = makeWsRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse());
 | 
			
		||||
 | 
			
		||||
        auto const responseJson = boost::json::parse(responseWs).as_object();
 | 
			
		||||
        EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
        EXPECT_EQ(responseJson.at("request").as_string(), requestStr);
 | 
			
		||||
        EXPECT_EQ(responseJson.at("id").as_string(), "some id");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, PostToRpcEngineFailed)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const request = makeHttpRequest("some message");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce(Return(false));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, notifyTooBusy());
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::service_unavailable);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, CoroutineSleepsUntilRpcEngineFinishes)
 | 
			
		||||
{
 | 
			
		||||
    StrictMock<testing::MockFunction<void()>> rpcServerHandlerDone;
 | 
			
		||||
    StrictMock<testing::MockFunction<void()>> rpcEngineDone;
 | 
			
		||||
    testing::Expectation const expectedRpcEngineDone = EXPECT_CALL(rpcEngineDone, Call);
 | 
			
		||||
    EXPECT_CALL(rpcServerHandlerDone, Call).After(expectedRpcEngineDone);
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const request = makeHttpRequest("some message");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            boost::asio::spawn(
 | 
			
		||||
                ctx_,
 | 
			
		||||
                [this, &rpcEngineDone, fn = std::forward<decltype(fn)>(fn)](boost::asio::yield_context yield) {
 | 
			
		||||
                    EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
 | 
			
		||||
                    fn(yield);
 | 
			
		||||
                    rpcEngineDone.Call();
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
        rpcServerHandlerDone.Call();
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, JsonParseFailed)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const request = makeHttpRequest("not a json");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
        EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, DosguardRejectedParsedRequest)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = "{}";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(false));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
        auto const responseHttp = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
 | 
			
		||||
 | 
			
		||||
        auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, DosguardAddsLoadWarning)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = "{}";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(false));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(false));
 | 
			
		||||
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
        auto const responseHttp = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
 | 
			
		||||
 | 
			
		||||
        auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(responseJson.at("warning").as_string(), "load");
 | 
			
		||||
        EXPECT_EQ(responseJson.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcRateLimit);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, GotNotJsonObject)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto const request = makeHttpRequest("[]");
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
        EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_NoRangeFromBackend)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = "{}";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillOnce(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyNotReady);
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "notReady");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_ContextCreationFailed)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = "{}";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::bad_request);
 | 
			
		||||
        EXPECT_EQ(httpResponse.body(), "Null method");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseFailed)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method"})JSON";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{rpc::Status{rpc::ClioError::RpcUnknownOption}}));
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "unknownOption");
 | 
			
		||||
 | 
			
		||||
        ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1);
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseThrewAnException)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method"})JSON";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse).WillOnce([](auto&&) -> rpc::Result {
 | 
			
		||||
                throw std::runtime_error("some error");
 | 
			
		||||
            });
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyInternalError);
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::internal_server_error);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method"})JSON";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyComplete);
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
 | 
			
		||||
 | 
			
		||||
        ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_OutdatedWarning)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method"})JSON";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyComplete);
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(61));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
 | 
			
		||||
 | 
			
		||||
        std::unordered_set<int64_t> warningCodes;
 | 
			
		||||
        std::ranges::transform(
 | 
			
		||||
            jsonResponse.at("warnings").as_array(),
 | 
			
		||||
            std::inserter(warningCodes, warningCodes.end()),
 | 
			
		||||
            [](auto const& w) { return w.as_object().at("id").as_int64(); }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(warningCodes.size(), 2);
 | 
			
		||||
        EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcClio));
 | 
			
		||||
        EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcOutdated));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_Forwarded)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method"})JSON";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{
 | 
			
		||||
                    {"result", boost::json::object{{"some key", "some value"}}}, {"forwarded", true}
 | 
			
		||||
                }}}));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyComplete);
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("forwarded").as_bool(), true);
 | 
			
		||||
 | 
			
		||||
        ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_HasError)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method"})JSON";
 | 
			
		||||
        auto const request = makeHttpRequest(requestStr);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{
 | 
			
		||||
                    rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
 | 
			
		||||
                }));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyComplete);
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
 | 
			
		||||
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
 | 
			
		||||
 | 
			
		||||
        ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct NgRpcServerHandlerWsTest : NgRpcServerHandlerTest {
 | 
			
		||||
    struct MockSubscriptionContext : web::SubscriptionContextInterface {
 | 
			
		||||
        using web::SubscriptionContextInterface::SubscriptionContextInterface;
 | 
			
		||||
 | 
			
		||||
        MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
 | 
			
		||||
        MOCK_METHOD(void, onDisconnect, (web::SubscriptionContextInterface::OnDisconnectSlot const&), (override));
 | 
			
		||||
        MOCK_METHOD(void, setApiSubversion, (uint32_t), (override));
 | 
			
		||||
        MOCK_METHOD(uint32_t, apiSubversion, (), (const, override));
 | 
			
		||||
    };
 | 
			
		||||
    using StrictMockSubscriptionContext = testing::StrictMock<MockSubscriptionContext>;
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    std::shared_ptr<StrictMockSubscriptionContext> subscriptionContext_ =
 | 
			
		||||
        std::make_shared<StrictMockSubscriptionContext>(tagFactory_);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        Request::HttpHeaders const headers;
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method", "id": 1234, "api_version": 1})JSON";
 | 
			
		||||
        auto const request = Request(requestStr, headers);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyComplete);
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(response.message()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("status").as_string(), "success");
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
 | 
			
		||||
 | 
			
		||||
        ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest_HasError)
 | 
			
		||||
{
 | 
			
		||||
    backend_->setRange(0, 1);
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        Request::HttpHeaders const headers;
 | 
			
		||||
        std::string const requestStr = R"JSON({"method":"some_method", "id": 1234, "api_version": 1})JSON";
 | 
			
		||||
        auto const request = Request(requestStr, headers);
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
 | 
			
		||||
        EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
 | 
			
		||||
            EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, buildResponse)
 | 
			
		||||
                .WillOnce(Return(rpc::Result{
 | 
			
		||||
                    rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
 | 
			
		||||
                }));
 | 
			
		||||
            EXPECT_CALL(*rpcEngine_, notifyComplete);
 | 
			
		||||
            EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
 | 
			
		||||
            fn(yield);
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
 | 
			
		||||
 | 
			
		||||
        auto const jsonResponse = boost::json::parse(response.message()).as_object();
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
 | 
			
		||||
 | 
			
		||||
        ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
 | 
			
		||||
        EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,583 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/AsioContextTestFixture.hpp"
 | 
			
		||||
#include "util/AssignRandomPort.hpp"
 | 
			
		||||
#include "util/LoggerFixtures.hpp"
 | 
			
		||||
#include "util/MockPrometheus.hpp"
 | 
			
		||||
#include "util/NameGenerator.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/TestHttpClient.hpp"
 | 
			
		||||
#include "util/TestWebSocketClient.hpp"
 | 
			
		||||
#include "util/config/ConfigConstraints.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigFileJson.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/ProcessingPolicy.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/Response.hpp"
 | 
			
		||||
#include "web/ng/Server.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/ip/address_v4.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/steady_timer.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/http/verb.hpp>
 | 
			
		||||
#include <boost/beast/websocket/error.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <ranges>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
struct MakeServerTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    std::string configJson;
 | 
			
		||||
    bool expectSuccess;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct MakeServerTest : NoLoggerFixture, testing::WithParamInterface<MakeServerTestBundle> {
 | 
			
		||||
protected:
 | 
			
		||||
    boost::asio::io_context ioContext_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_P(MakeServerTest, Make)
 | 
			
		||||
{
 | 
			
		||||
    ConfigFileJson const json{boost::json::parse(GetParam().configJson).as_object()};
 | 
			
		||||
 | 
			
		||||
    util::config::ClioConfigDefinition config{
 | 
			
		||||
        {"server.ip", ConfigValue{ConfigType::String}.optional()},
 | 
			
		||||
        {"server.port", ConfigValue{ConfigType::Integer}.optional()},
 | 
			
		||||
        {"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
 | 
			
		||||
        {"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
 | 
			
		||||
        {"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
 | 
			
		||||
        {"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
 | 
			
		||||
        {"ssl_key_file", ConfigValue{ConfigType::String}.optional()}
 | 
			
		||||
 | 
			
		||||
    };
 | 
			
		||||
    auto const errors = config.parse(json);
 | 
			
		||||
    ASSERT_TRUE(!errors.has_value());
 | 
			
		||||
 | 
			
		||||
    auto const expectedServer =
 | 
			
		||||
        makeServer(config, [](auto&&) -> std::expected<void, Response> { return {}; }, [](auto&&) {}, ioContext_);
 | 
			
		||||
    EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    MakeServerTests,
 | 
			
		||||
    MakeServerTest,
 | 
			
		||||
    testing::Values(
 | 
			
		||||
        MakeServerTestBundle{
 | 
			
		||||
            "BadEndpoint",
 | 
			
		||||
            R"JSON(
 | 
			
		||||
                {
 | 
			
		||||
                    "server": {"ip": "wrong", "port": 12345}
 | 
			
		||||
                }
 | 
			
		||||
            )JSON",
 | 
			
		||||
            false
 | 
			
		||||
        },
 | 
			
		||||
        MakeServerTestBundle{
 | 
			
		||||
            "BadSslConfig",
 | 
			
		||||
            R"JSON(
 | 
			
		||||
        {
 | 
			
		||||
            "server": {"ip": "127.0.0.1", "port": 12345},
 | 
			
		||||
            "ssl_cert_file": "some_file"
 | 
			
		||||
        }
 | 
			
		||||
            )JSON",
 | 
			
		||||
            false
 | 
			
		||||
        },
 | 
			
		||||
        MakeServerTestBundle{
 | 
			
		||||
            "BadProcessingPolicy",
 | 
			
		||||
            R"JSON(
 | 
			
		||||
        {
 | 
			
		||||
            "server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "wrong"}
 | 
			
		||||
        }
 | 
			
		||||
            )JSON",
 | 
			
		||||
            false
 | 
			
		||||
        },
 | 
			
		||||
        MakeServerTestBundle{
 | 
			
		||||
            "CorrectConfig_ParallelPolicy",
 | 
			
		||||
            R"JSON(
 | 
			
		||||
        {
 | 
			
		||||
            "server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "parallel"}
 | 
			
		||||
        }
 | 
			
		||||
            )JSON",
 | 
			
		||||
            true
 | 
			
		||||
        },
 | 
			
		||||
        MakeServerTestBundle{
 | 
			
		||||
            "CorrectConfig_SequentPolicy",
 | 
			
		||||
            R"JSON(
 | 
			
		||||
        {
 | 
			
		||||
            "server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "sequent"}
 | 
			
		||||
        }
 | 
			
		||||
            )JSON",
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
struct ServerTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
 | 
			
		||||
    ServerTest()
 | 
			
		||||
    {
 | 
			
		||||
        [&]() { ASSERT_TRUE(server_.has_value()); }();
 | 
			
		||||
        server_->onGet("/", getHandler_.AsStdFunction());
 | 
			
		||||
        server_->onPost("/", postHandler_.AsStdFunction());
 | 
			
		||||
        server_->onWs(wsHandler_.AsStdFunction());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    uint32_t const serverPort_ = tests::util::generateFreePort();
 | 
			
		||||
 | 
			
		||||
    ClioConfigDefinition const config_{
 | 
			
		||||
        {"server.ip", ConfigValue{ConfigType::String}.defaultValue("127.0.0.1").withConstraint(gValidateIp)},
 | 
			
		||||
        {"server.port", ConfigValue{ConfigType::Integer}.defaultValue(serverPort_).withConstraint(gValidatePort)},
 | 
			
		||||
        {"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
 | 
			
		||||
        {"server.admin_password", ConfigValue{ConfigType::String}.optional()},
 | 
			
		||||
        {"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
 | 
			
		||||
        {"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
 | 
			
		||||
        {"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
 | 
			
		||||
        {"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
 | 
			
		||||
        {"ssl_cert_file", ConfigValue{ConfigType::String}.optional()}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Server::OnConnectCheck emptyOnConnectCheck_ = [](auto&&) -> std::expected<void, Response> { return {}; };
 | 
			
		||||
    std::expected<Server, std::string> server_ = makeServer(config_, emptyOnConnectCheck_, [](auto&&) {}, ctx_);
 | 
			
		||||
 | 
			
		||||
    std::string requestMessage_ = "some request";
 | 
			
		||||
    std::string const headerName_ = "Some-header";
 | 
			
		||||
    std::string const headerValue_ = "some value";
 | 
			
		||||
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<
 | 
			
		||||
        Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
 | 
			
		||||
        getHandler_;
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<
 | 
			
		||||
        Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
 | 
			
		||||
        postHandler_;
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<
 | 
			
		||||
        Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
 | 
			
		||||
        wsHandler_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerTest, BadEndpoint)
 | 
			
		||||
{
 | 
			
		||||
    boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0};
 | 
			
		||||
    util::TagDecoratorFactory const tagDecoratorFactory{
 | 
			
		||||
        ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
 | 
			
		||||
    };
 | 
			
		||||
    Server server{
 | 
			
		||||
        ctx_,
 | 
			
		||||
        endpoint,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ProcessingPolicy::Sequential,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        tagDecoratorFactory,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        emptyOnConnectCheck_,
 | 
			
		||||
        [](auto&&) {}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auto maybeError = server.run();
 | 
			
		||||
    ASSERT_TRUE(maybeError.has_value());
 | 
			
		||||
    EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ServerHttpTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    http::verb method;
 | 
			
		||||
 | 
			
		||||
    Request::Method
 | 
			
		||||
    expectedMethod() const
 | 
			
		||||
    {
 | 
			
		||||
        switch (method) {
 | 
			
		||||
            case http::verb::get:
 | 
			
		||||
                return Request::Method::Get;
 | 
			
		||||
            case http::verb::post:
 | 
			
		||||
                return Request::Method::Post;
 | 
			
		||||
            default:
 | 
			
		||||
                return Request::Method::Unsupported;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ServerHttpTest : ServerTest, testing::WithParamInterface<ServerHttpTestBundle> {};
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerHttpTest, ClientDisconnects)
 | 
			
		||||
{
 | 
			
		||||
    HttpAsyncClient client{ctx_};
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        client.disconnect();
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server_->run();
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerHttpTest, OnConnectCheck)
 | 
			
		||||
{
 | 
			
		||||
    auto const serverPort = tests::util::generateFreePort();
 | 
			
		||||
    boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
 | 
			
		||||
    util::TagDecoratorFactory const tagDecoratorFactory{
 | 
			
		||||
        ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<std::expected<void, Response>(Connection const&)>> onConnectCheck;
 | 
			
		||||
 | 
			
		||||
    Server server{
 | 
			
		||||
        ctx_,
 | 
			
		||||
        endpoint,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ProcessingPolicy::Sequential,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        tagDecoratorFactory,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        onConnectCheck.AsStdFunction(),
 | 
			
		||||
        [](auto&&) {}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    HttpAsyncClient client{ctx_};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        boost::asio::steady_timer timer{yield.get_executor()};
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(onConnectCheck, Call)
 | 
			
		||||
            .WillOnce([&timer](Connection const& connection) -> std::expected<void, Response> {
 | 
			
		||||
                EXPECT_EQ(connection.ip(), "127.0.0.1");
 | 
			
		||||
                timer.cancel();
 | 
			
		||||
                return {};
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        // Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
 | 
			
		||||
        client.send(
 | 
			
		||||
            http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
 | 
			
		||||
            yield,
 | 
			
		||||
            std::chrono::milliseconds{100}
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Wait for the onConnectCheck to be called
 | 
			
		||||
        timer.expires_after(std::chrono::milliseconds{100});
 | 
			
		||||
        boost::system::error_code error;  // Unused
 | 
			
		||||
        timer.async_wait(yield[error]);
 | 
			
		||||
 | 
			
		||||
        client.gracefulShutdown();
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server.run();
 | 
			
		||||
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerHttpTest, OnConnectCheckFailed)
 | 
			
		||||
{
 | 
			
		||||
    auto const serverPort = tests::util::generateFreePort();
 | 
			
		||||
    boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
 | 
			
		||||
    util::TagDecoratorFactory const tagDecoratorFactory{
 | 
			
		||||
        ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<std::expected<void, Response>(Connection const&)>> onConnectCheck;
 | 
			
		||||
 | 
			
		||||
    Server server{
 | 
			
		||||
        ctx_,
 | 
			
		||||
        endpoint,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ProcessingPolicy::Sequential,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        tagDecoratorFactory,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        onConnectCheck.AsStdFunction(),
 | 
			
		||||
        [](auto&&) {}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    HttpAsyncClient client{ctx_};
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(onConnectCheck, Call).WillOnce([](Connection const& connection) {
 | 
			
		||||
        EXPECT_EQ(connection.ip(), "127.0.0.1");
 | 
			
		||||
        return std::unexpected{
 | 
			
		||||
            Response{http::status::too_many_requests, boost::json::object{{"error", "some error"}}, connection}
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        // Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
 | 
			
		||||
        client.send(
 | 
			
		||||
            http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
 | 
			
		||||
            yield,
 | 
			
		||||
            std::chrono::milliseconds{100}
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        auto const response = client.receive(yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_TRUE(response.has_value()) << response.error().message(); }();
 | 
			
		||||
        EXPECT_EQ(response->result(), http::status::too_many_requests);
 | 
			
		||||
        EXPECT_EQ(response->body(), R"JSON({"error":"some error"})JSON");
 | 
			
		||||
        EXPECT_EQ(response->version(), 11);
 | 
			
		||||
 | 
			
		||||
        client.gracefulShutdown();
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server.run();
 | 
			
		||||
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerHttpTest, OnDisconnectHook)
 | 
			
		||||
{
 | 
			
		||||
    auto const serverPort = tests::util::generateFreePort();
 | 
			
		||||
    boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
 | 
			
		||||
    util::TagDecoratorFactory const tagDecoratorFactory{
 | 
			
		||||
        ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<void(Connection const&)>> onDisconnectHookMock;
 | 
			
		||||
 | 
			
		||||
    Server server{
 | 
			
		||||
        ctx_,
 | 
			
		||||
        endpoint,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        ProcessingPolicy::Sequential,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        tagDecoratorFactory,
 | 
			
		||||
        std::nullopt,
 | 
			
		||||
        emptyOnConnectCheck_,
 | 
			
		||||
        onDisconnectHookMock.AsStdFunction()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    HttpAsyncClient client{ctx_};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        boost::asio::steady_timer timer{ctx_.get_executor(), std::chrono::milliseconds{100}};
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(onDisconnectHookMock, Call).WillOnce([&timer](auto&&) { timer.cancel(); });
 | 
			
		||||
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        client.send(
 | 
			
		||||
            http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
 | 
			
		||||
            yield,
 | 
			
		||||
            std::chrono::milliseconds{100}
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        client.gracefulShutdown();
 | 
			
		||||
 | 
			
		||||
        // Wait for OnDisconnectHook is called
 | 
			
		||||
        boost::system::error_code error;
 | 
			
		||||
        timer.async_wait(yield[error]);
 | 
			
		||||
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server.run();
 | 
			
		||||
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerHttpTest, ClientIsDisconnectedIfServerStopped)
 | 
			
		||||
{
 | 
			
		||||
    HttpAsyncClient client{ctx_};
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        // Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
 | 
			
		||||
        maybeError = client.send(
 | 
			
		||||
            http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
 | 
			
		||||
            yield,
 | 
			
		||||
            std::chrono::milliseconds{100}
 | 
			
		||||
        );
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        auto message = client.receive(yield, std::chrono::milliseconds{100});
 | 
			
		||||
        EXPECT_TRUE(message.has_value()) << message.error().message();
 | 
			
		||||
        EXPECT_EQ(message->result(), http::status::service_unavailable);
 | 
			
		||||
        EXPECT_EQ(message->body(), "This Clio node is shutting down. Please try another node.");
 | 
			
		||||
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server_->run();
 | 
			
		||||
    runSyncOperation([this](auto yield) { server_->stop(yield); });
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_P(ServerHttpTest, RequestResponse)
 | 
			
		||||
{
 | 
			
		||||
    HttpAsyncClient client{ctx_};
 | 
			
		||||
 | 
			
		||||
    http::request<http::string_body> request{GetParam().method, "/", 11, requestMessage_};
 | 
			
		||||
    request.set(headerName_, headerValue_);
 | 
			
		||||
 | 
			
		||||
    Response const response{http::status::ok, "some response", Request{request}};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
 | 
			
		||||
            maybeError = client.send(request, yield, std::chrono::milliseconds{100});
 | 
			
		||||
            EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
 | 
			
		||||
 | 
			
		||||
            auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
 | 
			
		||||
            [&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
 | 
			
		||||
            EXPECT_EQ(expectedResponse->result(), http::status::ok);
 | 
			
		||||
            EXPECT_EQ(expectedResponse->body(), response.message());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client.gracefulShutdown();
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    auto& handler = GetParam().method == http::verb::get ? getHandler_ : postHandler_;
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(handler, Call)
 | 
			
		||||
        .Times(3)
 | 
			
		||||
        .WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
 | 
			
		||||
            EXPECT_TRUE(receivedRequest.isHttp());
 | 
			
		||||
            EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod());
 | 
			
		||||
            EXPECT_EQ(receivedRequest.message(), request.body());
 | 
			
		||||
            EXPECT_EQ(receivedRequest.target(), request.target());
 | 
			
		||||
            EXPECT_EQ(receivedRequest.headerValue(headerName_), request.at(headerName_));
 | 
			
		||||
 | 
			
		||||
            return response;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    server_->run();
 | 
			
		||||
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_SUITE_P(
 | 
			
		||||
    ServerHttpTests,
 | 
			
		||||
    ServerHttpTest,
 | 
			
		||||
    testing::Values(ServerHttpTestBundle{"GET", http::verb::get}, ServerHttpTestBundle{"POST", http::verb::post}),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerTest, WsClientDisconnects)
 | 
			
		||||
{
 | 
			
		||||
    WebSocketAsyncClient client{ctx_};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        client.close();
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server_->run();
 | 
			
		||||
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerTest, WsRequestResponse)
 | 
			
		||||
{
 | 
			
		||||
    WebSocketAsyncClient client{ctx_};
 | 
			
		||||
 | 
			
		||||
    Request::HttpHeaders const headers{};
 | 
			
		||||
    Response const response{http::status::ok, "some response", Request{requestMessage_, headers}};
 | 
			
		||||
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        [&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
 | 
			
		||||
 | 
			
		||||
        for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
 | 
			
		||||
            maybeError = client.send(yield, requestMessage_, std::chrono::milliseconds{100});
 | 
			
		||||
            EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
 | 
			
		||||
 | 
			
		||||
            auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
 | 
			
		||||
            [&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
 | 
			
		||||
            EXPECT_EQ(expectedResponse.value(), response.message());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client.gracefulClose(yield, std::chrono::milliseconds{100});
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    EXPECT_CALL(wsHandler_, Call)
 | 
			
		||||
        .Times(3)
 | 
			
		||||
        .WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
 | 
			
		||||
            EXPECT_FALSE(receivedRequest.isHttp());
 | 
			
		||||
            EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket);
 | 
			
		||||
            EXPECT_EQ(receivedRequest.message(), requestMessage_);
 | 
			
		||||
            EXPECT_EQ(receivedRequest.target(), std::nullopt);
 | 
			
		||||
 | 
			
		||||
            return response;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    server_->run();
 | 
			
		||||
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(ServerTest, WsClientIsDisconnectedIfServerStopped)
 | 
			
		||||
{
 | 
			
		||||
    WebSocketAsyncClient client{ctx_};
 | 
			
		||||
    boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto maybeError =
 | 
			
		||||
            client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
 | 
			
		||||
        EXPECT_TRUE(maybeError.has_value());
 | 
			
		||||
        EXPECT_EQ(maybeError.value().value(), static_cast<int>(boost::beast::websocket::error::upgrade_declined));
 | 
			
		||||
 | 
			
		||||
        ctx_.stop();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    server_->run();
 | 
			
		||||
    runSyncOperation([this](auto yield) { server_->stop(yield); });
 | 
			
		||||
    runContext();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,172 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/AsioContextTestFixture.hpp"
 | 
			
		||||
#include "util/Taggable.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "web/SubscriptionContextInterface.hpp"
 | 
			
		||||
#include "web/ng/Connection.hpp"
 | 
			
		||||
#include "web/ng/Error.hpp"
 | 
			
		||||
#include "web/ng/SubscriptionContext.hpp"
 | 
			
		||||
#include "web/ng/impl/MockWsConnection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/post.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/system/errc.hpp>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
using namespace util::config;
 | 
			
		||||
 | 
			
		||||
struct NgSubscriptionContextTests : SyncAsioContextTest {
 | 
			
		||||
    SubscriptionContext
 | 
			
		||||
    makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
 | 
			
		||||
    {
 | 
			
		||||
        return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
 | 
			
		||||
    }};
 | 
			
		||||
    MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<bool(web::ng::Error const&, Connection const&)>> errorHandler_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, Send)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("some message");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
            EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
 | 
			
		||||
            return std::nullopt;
 | 
			
		||||
        });
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, SendOrder)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message1 = std::make_shared<std::string>("message1");
 | 
			
		||||
        auto const message2 = std::make_shared<std::string>("message2");
 | 
			
		||||
 | 
			
		||||
        testing::Sequence const sequence;
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer)
 | 
			
		||||
            .InSequence(sequence)
 | 
			
		||||
            .WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
                EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            });
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer)
 | 
			
		||||
            .InSequence(sequence)
 | 
			
		||||
            .WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
                EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        subscriptionContext.send(message1);
 | 
			
		||||
        subscriptionContext.send(message2);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, SendFailed)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("some message");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
 | 
			
		||||
            EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
 | 
			
		||||
            return boost::system::errc::make_error_code(boost::system::errc::not_supported);
 | 
			
		||||
        });
 | 
			
		||||
        EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
 | 
			
		||||
        EXPECT_CALL(connection_, close);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, SendTooManySubscriptions)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield, 1);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("message1");
 | 
			
		||||
 | 
			
		||||
        EXPECT_CALL(connection_, sendBuffer)
 | 
			
		||||
            .WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
 | 
			
		||||
                boost::asio::post(innerYield);  // simulate send is slow by switching to another coroutine
 | 
			
		||||
                EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
 | 
			
		||||
                return std::nullopt;
 | 
			
		||||
            });
 | 
			
		||||
        EXPECT_CALL(connection_, close);
 | 
			
		||||
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, SendAfterDisconnect)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        auto const message = std::make_shared<std::string>("some message");
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
        subscriptionContext.send(message);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, OnDisconnect)
 | 
			
		||||
{
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
 | 
			
		||||
        EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
 | 
			
		||||
        subscriptionContext.disconnect(yield);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgSubscriptionContextTests, SetApiSubversion)
 | 
			
		||||
{
 | 
			
		||||
    runSpawn([this](boost::asio::yield_context yield) {
 | 
			
		||||
        auto subscriptionContext = makeSubscriptionContext(yield);
 | 
			
		||||
        subscriptionContext.setApiSubversion(42);
 | 
			
		||||
        EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,358 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "rpc/Errors.hpp"
 | 
			
		||||
#include "util/LoggerFixtures.hpp"
 | 
			
		||||
#include "util/NameGenerator.hpp"
 | 
			
		||||
#include "web/ng/Request.hpp"
 | 
			
		||||
#include "web/ng/impl/ErrorHandling.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/status.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/http/verb.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <boost/json/serialize.hpp>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <variant>
 | 
			
		||||
 | 
			
		||||
using namespace web::ng::impl;
 | 
			
		||||
using namespace web::ng;
 | 
			
		||||
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingTests : NoLoggerFixture {
 | 
			
		||||
    static Request
 | 
			
		||||
    makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
 | 
			
		||||
    {
 | 
			
		||||
        if (isHttp)
 | 
			
		||||
            return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
 | 
			
		||||
        static Request::HttpHeaders const kHEADERS;
 | 
			
		||||
        return Request{body.value_or(""), kHEADERS};
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingMakeErrorTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    bool isHttp;
 | 
			
		||||
    rpc::Status status;
 | 
			
		||||
    std::string expectedMessage;
 | 
			
		||||
    boost::beast::http::status expectedStatus;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingMakeErrorTest : NgErrorHandlingTests,
 | 
			
		||||
                                      testing::WithParamInterface<NgErrorHandlingMakeErrorTestBundle> {};
 | 
			
		||||
 | 
			
		||||
TEST_P(NgErrorHandlingMakeErrorTest, MakeError)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(GetParam().isHttp);
 | 
			
		||||
    ErrorHelper const errorHelper{request};
 | 
			
		||||
 | 
			
		||||
    auto response = errorHelper.makeError(GetParam().status);
 | 
			
		||||
    EXPECT_EQ(response.message(), GetParam().expectedMessage);
 | 
			
		||||
    if (GetParam().isHttp) {
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
 | 
			
		||||
 | 
			
		||||
        std::string expectedContentType = "text/html";
 | 
			
		||||
        if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
 | 
			
		||||
            expectedContentType = "application/json";
 | 
			
		||||
 | 
			
		||||
        EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    ng_ErrorHandlingMakeErrorTestGroup,
 | 
			
		||||
    NgErrorHandlingMakeErrorTest,
 | 
			
		||||
    testing::ValuesIn({
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "WsRequest",
 | 
			
		||||
            false,
 | 
			
		||||
            rpc::Status{rpc::RippledError::rpcTOO_BUSY},
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
 | 
			
		||||
            boost::beast::http::status::ok
 | 
			
		||||
        },
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_InvalidApiVersion",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
 | 
			
		||||
            "invalid_API_version",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_CommandIsMissing",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandIsMissing},
 | 
			
		||||
            "Null method",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_CommandIsEmpty",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
 | 
			
		||||
            "method is empty",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_CommandNotString",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcCommandNotString},
 | 
			
		||||
            "method is not string",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_ParamsUnparsable",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::ClioError::RpcParamsUnparsable},
 | 
			
		||||
            "params unparsable",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
        NgErrorHandlingMakeErrorTestBundle{
 | 
			
		||||
            "HttpRequest_RippledError",
 | 
			
		||||
            true,
 | 
			
		||||
            rpc::Status{rpc::RippledError::rpcTOO_BUSY},
 | 
			
		||||
            R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
 | 
			
		||||
            boost::beast::http::status::bad_request
 | 
			
		||||
        },
 | 
			
		||||
    }),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingMakeInternalErrorTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    bool isHttp;
 | 
			
		||||
    std::optional<std::string> request;
 | 
			
		||||
    boost::json::object expectedResult;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingMakeInternalErrorTest : NgErrorHandlingTests,
 | 
			
		||||
                                              testing::WithParamInterface<NgErrorHandlingMakeInternalErrorTestBundle> {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_P(NgErrorHandlingMakeInternalErrorTest, ComposeError)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(GetParam().isHttp, GetParam().request);
 | 
			
		||||
    std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
 | 
			
		||||
        ? std::make_optional(boost::json::parse(*GetParam().request).as_object())
 | 
			
		||||
        : std::nullopt;
 | 
			
		||||
    ErrorHelper const errorHelper{request, requestJson};
 | 
			
		||||
 | 
			
		||||
    auto response = errorHelper.makeInternalError();
 | 
			
		||||
 | 
			
		||||
    EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
 | 
			
		||||
    if (GetParam().isHttp) {
 | 
			
		||||
        auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
        EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
 | 
			
		||||
        EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    ng_ErrorHandlingComposeErrorTestGroup,
 | 
			
		||||
    NgErrorHandlingMakeInternalErrorTest,
 | 
			
		||||
    testing::ValuesIn(
 | 
			
		||||
        {NgErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "NoRequest_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             {{"error", "internal"},
 | 
			
		||||
              {"error_code", 73},
 | 
			
		||||
              {"error_message", "Internal error."},
 | 
			
		||||
              {"status", "error"},
 | 
			
		||||
              {"type", "response"}}
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "NoRequest_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             {{"result",
 | 
			
		||||
               {{"error", "internal"},
 | 
			
		||||
                {"error_code", 73},
 | 
			
		||||
                {"error_message", "Internal error."},
 | 
			
		||||
                {"status", "error"},
 | 
			
		||||
                {"type", "response"}}}}
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
 | 
			
		||||
             {{"error", "internal"},
 | 
			
		||||
              {"error_code", 73},
 | 
			
		||||
              {"error_message", "Internal error."},
 | 
			
		||||
              {"status", "error"},
 | 
			
		||||
              {"type", "response"},
 | 
			
		||||
              {"id", 1},
 | 
			
		||||
              {"api_version", 2},
 | 
			
		||||
              {"request", {{"id", 1}, {"api_version", 2}}}}
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection_NoId",
 | 
			
		||||
             false,
 | 
			
		||||
             std::string{R"JSON({"api_version": 2})JSON"},
 | 
			
		||||
             {{"error", "internal"},
 | 
			
		||||
              {"error_code", 73},
 | 
			
		||||
              {"error_message", "Internal error."},
 | 
			
		||||
              {"status", "error"},
 | 
			
		||||
              {"type", "response"},
 | 
			
		||||
              {"api_version", 2},
 | 
			
		||||
              {"request", {{"api_version", 2}}}}
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingMakeInternalErrorTestBundle{
 | 
			
		||||
             "Request_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
 | 
			
		||||
             {{"result",
 | 
			
		||||
               {{"error", "internal"},
 | 
			
		||||
                {"error_code", 73},
 | 
			
		||||
                {"error_message", "Internal error."},
 | 
			
		||||
                {"status", "error"},
 | 
			
		||||
                {"type", "response"},
 | 
			
		||||
                {"id", 1},
 | 
			
		||||
                {"request", {{"id", 1}, {"api_version", 2}}}}}}
 | 
			
		||||
         }}
 | 
			
		||||
    ),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TEST_F(NgErrorHandlingTests, MakeNotReadyError)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(true);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeNotReadyError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
    EXPECT_EQ(httpResponse.result(), http::status::ok);
 | 
			
		||||
    EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(false);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeTooBusyError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgErrorHandlingTests, sendTooBusyError_HttpConnection)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(true);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeTooBusyError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
    EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
 | 
			
		||||
    EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(false);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeJsonParsingError();
 | 
			
		||||
    EXPECT_EQ(
 | 
			
		||||
        response.message(),
 | 
			
		||||
        std::string{
 | 
			
		||||
            R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(NgErrorHandlingTests, makeJsonParsingError_HttpConnection)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(true);
 | 
			
		||||
    auto response = ErrorHelper{request}.makeJsonParsingError();
 | 
			
		||||
    EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
 | 
			
		||||
    auto const httpResponse = std::move(response).intoHttpResponse();
 | 
			
		||||
    EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
 | 
			
		||||
    EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingComposeErrorTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    bool isHttp;
 | 
			
		||||
    std::optional<boost::json::object> request;
 | 
			
		||||
    std::string expectedMessage;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct NgErrorHandlingComposeErrorTest : NgErrorHandlingTests,
 | 
			
		||||
                                         testing::WithParamInterface<NgErrorHandlingComposeErrorTestBundle> {};
 | 
			
		||||
 | 
			
		||||
TEST_P(NgErrorHandlingComposeErrorTest, ComposeError)
 | 
			
		||||
{
 | 
			
		||||
    auto const request = makeRequest(GetParam().isHttp);
 | 
			
		||||
    ErrorHelper const errorHelper{request, GetParam().request};
 | 
			
		||||
    auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
 | 
			
		||||
    EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    ng_ErrorHandlingComposeErrorTestGroup,
 | 
			
		||||
    NgErrorHandlingComposeErrorTest,
 | 
			
		||||
    testing::ValuesIn(
 | 
			
		||||
        {NgErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "NoRequest_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "NoRequest_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             std::nullopt,
 | 
			
		||||
             R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection",
 | 
			
		||||
             false,
 | 
			
		||||
             boost::json::object{{"id", 1}, {"api_version", 2}},
 | 
			
		||||
             R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})JSON",
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_WebsocketConnection_NoId",
 | 
			
		||||
             false,
 | 
			
		||||
             boost::json::object{{"api_version", 2}},
 | 
			
		||||
             R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})JSON",
 | 
			
		||||
         },
 | 
			
		||||
         NgErrorHandlingComposeErrorTestBundle{
 | 
			
		||||
             "Request_HttpConnection",
 | 
			
		||||
             true,
 | 
			
		||||
             boost::json::object{{"id", 1}, {"api_version", 2}},
 | 
			
		||||
             R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})JSON"
 | 
			
		||||
         }}
 | 
			
		||||
    ),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
		Reference in New Issue
	
	Block a user