mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	Requests library (#1140)
For #51. First part of improving forwarding - library for easy async requests.
This commit is contained in:
		@@ -163,6 +163,10 @@ target_sources (clio PRIVATE
 | 
			
		||||
  src/util/prometheus/OStream.cpp
 | 
			
		||||
  src/util/prometheus/Prometheus.cpp
 | 
			
		||||
  src/util/Random.cpp
 | 
			
		||||
  src/util/requests/RequestBuilder.cpp
 | 
			
		||||
  src/util/requests/Types.cpp
 | 
			
		||||
  src/util/requests/WsConnection.cpp
 | 
			
		||||
  src/util/requests/impl/SslContext.cpp
 | 
			
		||||
  src/util/Taggable.cpp
 | 
			
		||||
  src/util/TerminationHandler.cpp
 | 
			
		||||
  src/util/TxUtils.cpp
 | 
			
		||||
@@ -192,8 +196,10 @@ if (tests)
 | 
			
		||||
    unittests/util/TestGlobals.cpp
 | 
			
		||||
    unittests/util/AssertTests.cpp
 | 
			
		||||
    unittests/util/BatchingTests.cpp
 | 
			
		||||
    unittests/util/TxUtilTests.cpp
 | 
			
		||||
    unittests/util/TestHttpServer.cpp
 | 
			
		||||
    unittests/util/TestObject.cpp
 | 
			
		||||
    unittests/util/TestWsServer.cpp
 | 
			
		||||
    unittests/util/TxUtilTests.cpp
 | 
			
		||||
    unittests/util/StringUtils.cpp
 | 
			
		||||
    unittests/util/LedgerUtilsTests.cpp
 | 
			
		||||
    unittests/util/prometheus/CounterTests.cpp
 | 
			
		||||
@@ -204,6 +210,9 @@ if (tests)
 | 
			
		||||
    unittests/util/prometheus/MetricBuilderTests.cpp
 | 
			
		||||
    unittests/util/prometheus/MetricsFamilyTests.cpp
 | 
			
		||||
    unittests/util/prometheus/OStreamTests.cpp
 | 
			
		||||
    unittests/util/requests/RequestBuilderTests.cpp
 | 
			
		||||
    unittests/util/requests/SslContextTests.cpp
 | 
			
		||||
    unittests/util/requests/WsConnectionTests.cpp
 | 
			
		||||
    # ETL
 | 
			
		||||
    unittests/etl/ExtractionDataPipeTests.cpp
 | 
			
		||||
    unittests/etl/ExtractorTests.cpp
 | 
			
		||||
 
 | 
			
		||||
@@ -112,26 +112,6 @@ public:
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgers> validatedLedgers
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A factory function for the ETL source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param config The configuration to use
 | 
			
		||||
     * @param ioc The io_context to run on
 | 
			
		||||
     * @param backend BackendInterface implementation
 | 
			
		||||
     * @param subscriptions Subscription manager
 | 
			
		||||
     * @param validatedLedgers The network validated ledgers datastructure
 | 
			
		||||
     * @param balancer The load balancer
 | 
			
		||||
     */
 | 
			
		||||
    static std::unique_ptr<Source>
 | 
			
		||||
    make_Source(
 | 
			
		||||
        util::Config const& config,
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgers> validatedLedgers,
 | 
			
		||||
        LoadBalancer& balancer
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    ~LoadBalancer();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -200,6 +180,26 @@ public:
 | 
			
		||||
    getETLState() noexcept;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A factory function for the ETL source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param config The configuration to use
 | 
			
		||||
     * @param ioc The io_context to run on
 | 
			
		||||
     * @param backend BackendInterface implementation
 | 
			
		||||
     * @param subscriptions Subscription manager
 | 
			
		||||
     * @param validatedLedgers The network validated ledgers datastructure
 | 
			
		||||
     * @param balancer The load balancer
 | 
			
		||||
     */
 | 
			
		||||
    static std::unique_ptr<Source>
 | 
			
		||||
    make_Source(
 | 
			
		||||
        util::Config const& config,
 | 
			
		||||
        boost::asio::io_context& ioc,
 | 
			
		||||
        std::shared_ptr<BackendInterface> backend,
 | 
			
		||||
        std::shared_ptr<feed::SubscriptionManager> subscriptions,
 | 
			
		||||
        std::shared_ptr<NetworkValidatedLedgers> validatedLedgers,
 | 
			
		||||
        LoadBalancer& balancer
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Execute a function on a randomly selected source.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -363,7 +363,7 @@ public:
 | 
			
		||||
        boost::json::object response;
 | 
			
		||||
 | 
			
		||||
        namespace beast = boost::beast;
 | 
			
		||||
        namespace http = boost::beast::http;
 | 
			
		||||
        namespace http = beast::http;
 | 
			
		||||
        namespace websocket = beast::websocket;
 | 
			
		||||
        namespace net = boost::asio;
 | 
			
		||||
        using tcp = boost::asio::ip::tcp;
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
// forward declarations
 | 
			
		||||
@@ -63,7 +64,6 @@ class RPCEngine {
 | 
			
		||||
    util::Logger log_{"RPC"};
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<BackendInterface> backend_;
 | 
			
		||||
    std::shared_ptr<etl::LoadBalancer> balancer_;
 | 
			
		||||
    std::reference_wrapper<web::DOSGuard const> dosGuard_;
 | 
			
		||||
    std::reference_wrapper<WorkQueue> workQueue_;
 | 
			
		||||
    std::reference_wrapper<Counters> counters_;
 | 
			
		||||
@@ -82,7 +82,6 @@ public:
 | 
			
		||||
        std::shared_ptr<HandlerProvider const> const& handlerProvider
 | 
			
		||||
    )
 | 
			
		||||
        : backend_{backend}
 | 
			
		||||
        , balancer_{balancer}
 | 
			
		||||
        , dosGuard_{std::cref(dosGuard)}
 | 
			
		||||
        , workQueue_{std::ref(workQueue)}
 | 
			
		||||
        , counters_{std::ref(counters)}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,8 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/Assert.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/outcome.hpp>
 | 
			
		||||
#include <boost/outcome/policy/base.hpp>
 | 
			
		||||
#include <boost/outcome/result.hpp>
 | 
			
		||||
@@ -161,29 +163,41 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr T const&
 | 
			
		||||
    value() const
 | 
			
		||||
    value() const&
 | 
			
		||||
    {
 | 
			
		||||
        return Base::value();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr T&
 | 
			
		||||
    value()
 | 
			
		||||
    value() &
 | 
			
		||||
    {
 | 
			
		||||
        return Base::value();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr T
 | 
			
		||||
    value() &&
 | 
			
		||||
    {
 | 
			
		||||
        return std::move(*base()).value();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr E const&
 | 
			
		||||
    error() const
 | 
			
		||||
    error() const&
 | 
			
		||||
    {
 | 
			
		||||
        return Base::error();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr E&
 | 
			
		||||
    error()
 | 
			
		||||
    error() &
 | 
			
		||||
    {
 | 
			
		||||
        return Base::error();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr E
 | 
			
		||||
    error() &&
 | 
			
		||||
    {
 | 
			
		||||
        return std::move(*base()).error();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr explicit
 | 
			
		||||
    operator bool() const
 | 
			
		||||
    {
 | 
			
		||||
@@ -216,6 +230,15 @@ public:
 | 
			
		||||
    {
 | 
			
		||||
        return &this->value();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    Base*
 | 
			
		||||
    base()
 | 
			
		||||
    {
 | 
			
		||||
        auto b = dynamic_cast<Base*>(this);
 | 
			
		||||
        ASSERT(b != nullptr, "Base class is not Base");
 | 
			
		||||
        return b;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Specialization of Expected<void, E>.  Allows returning either success
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										196
									
								
								src/util/requests/RequestBuilder.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/util/requests/RequestBuilder.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/requests/RequestBuilder.h"
 | 
			
		||||
 | 
			
		||||
#include "util/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
#include "util/requests/impl/StreamData.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/associated_executor.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
#include <boost/asio/ssl/error.hpp>
 | 
			
		||||
#include <boost/asio/ssl/stream_base.hpp>
 | 
			
		||||
#include <boost/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/http/dynamic_body.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/ssl_stream.hpp>
 | 
			
		||||
#include <boost/beast/version.hpp>
 | 
			
		||||
#include <openssl/err.h>
 | 
			
		||||
#include <openssl/tls1.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace util::requests {
 | 
			
		||||
 | 
			
		||||
namespace beast = boost::beast;
 | 
			
		||||
namespace http = beast::http;
 | 
			
		||||
namespace asio = boost::asio;
 | 
			
		||||
using tcp = asio::ip::tcp;
 | 
			
		||||
 | 
			
		||||
RequestBuilder::RequestBuilder(std::string host, std::string port) : host_(std::move(host)), port_(std::move(port))
 | 
			
		||||
{
 | 
			
		||||
    request_.set(http::field::host, host_);
 | 
			
		||||
    request_.target("/");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestBuilder&
 | 
			
		||||
RequestBuilder::addHeader(HttpHeader const& header)
 | 
			
		||||
{
 | 
			
		||||
    request_.set(header.name, header.value);
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestBuilder&
 | 
			
		||||
RequestBuilder::addHeaders(std::vector<HttpHeader> const& headers)
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& header : headers)
 | 
			
		||||
        addHeader(header);
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestBuilder&
 | 
			
		||||
RequestBuilder::addData(std::string data)
 | 
			
		||||
{
 | 
			
		||||
    request_.body() = data;
 | 
			
		||||
    request_.prepare_payload();
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestBuilder&
 | 
			
		||||
RequestBuilder::setTimeout(std::chrono::milliseconds const timeout)
 | 
			
		||||
{
 | 
			
		||||
    timeout_ = timeout;
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestBuilder&
 | 
			
		||||
RequestBuilder::setTarget(std::string_view target)
 | 
			
		||||
{
 | 
			
		||||
    request_.target(target);
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestBuilder&
 | 
			
		||||
RequestBuilder::setSslEnabled(bool const enabled)
 | 
			
		||||
{
 | 
			
		||||
    sslEnabled_ = enabled;
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Expected<std::string, RequestError>
 | 
			
		||||
RequestBuilder::get(asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    return doRequest(yield, http::verb::get);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Expected<std::string, RequestError>
 | 
			
		||||
RequestBuilder::post(asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    return doRequest(yield, http::verb::post);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Expected<std::string, RequestError>
 | 
			
		||||
RequestBuilder::doRequest(asio::yield_context yield, beast::http::verb method)
 | 
			
		||||
{
 | 
			
		||||
    if (sslEnabled_) {
 | 
			
		||||
        auto streamData = impl::SslTcpStreamData::create(yield);
 | 
			
		||||
        if (not streamData.has_value())
 | 
			
		||||
            return Unexpected{std::move(streamData).error()};
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
 | 
			
		||||
        if (!SSL_set_tlsext_host_name(streamData->stream.native_handle(), host_.c_str())) {
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
            beast::error_code errorCode;
 | 
			
		||||
            errorCode.assign(static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category());
 | 
			
		||||
            return Unexpected{RequestError{"SSL setup failed", errorCode}};
 | 
			
		||||
        }
 | 
			
		||||
        return doRequestImpl(std::move(streamData).value(), yield, method);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto streamData = impl::TcpStreamData{yield};
 | 
			
		||||
    return doRequestImpl(std::move(streamData), yield, method);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template <typename StreamDataType>
 | 
			
		||||
Expected<std::string, RequestError>
 | 
			
		||||
RequestBuilder::doRequestImpl(StreamDataType&& streamData, asio::yield_context yield, http::verb const method)
 | 
			
		||||
{
 | 
			
		||||
    auto executor = asio::get_associated_executor(yield);
 | 
			
		||||
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    tcp::resolver resolver(executor);
 | 
			
		||||
    auto const resolverResult = resolver.async_resolve(host_, port_, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Resolve error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    auto& stream = streamData.stream;
 | 
			
		||||
 | 
			
		||||
    beast::get_lowest_layer(stream).expires_after(timeout_);
 | 
			
		||||
    beast::get_lowest_layer(stream).async_connect(resolverResult, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Connection error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    request_.method(method);
 | 
			
		||||
 | 
			
		||||
    if constexpr (StreamDataType::sslEnabled) {
 | 
			
		||||
        beast::get_lowest_layer(stream).expires_after(timeout_);
 | 
			
		||||
        stream.async_handshake(asio::ssl::stream_base::client, yield[errorCode]);
 | 
			
		||||
        if (errorCode)
 | 
			
		||||
            return Unexpected{RequestError{"Handshake error", errorCode}};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beast::get_lowest_layer(stream).expires_after(timeout_);
 | 
			
		||||
    http::async_write(stream, request_, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Write error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    beast::flat_buffer buffer;
 | 
			
		||||
    http::response<http::string_body> response;
 | 
			
		||||
 | 
			
		||||
    http::async_read(stream, buffer, response, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Read error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    if (response.result() != http::status::ok)
 | 
			
		||||
        return Unexpected{RequestError{"Response status not OK"}};
 | 
			
		||||
 | 
			
		||||
    beast::get_lowest_layer(stream).socket().shutdown(tcp::socket::shutdown_both, errorCode);
 | 
			
		||||
 | 
			
		||||
    if (errorCode && errorCode != beast::errc::not_connected)
 | 
			
		||||
        return Unexpected{RequestError{"Shutdown socket error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    return std::move(response).body();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests
 | 
			
		||||
							
								
								
									
										155
									
								
								src/util/requests/RequestBuilder.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/util/requests/RequestBuilder.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
#include <boost/beast.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <boost/beast/http/verb.hpp>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace util::requests {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Builder for HTTP requests
 | 
			
		||||
 */
 | 
			
		||||
class RequestBuilder {
 | 
			
		||||
    std::string host_;
 | 
			
		||||
    std::string port_;
 | 
			
		||||
    std::chrono::milliseconds timeout_{DEFAULT_TIMEOUT};
 | 
			
		||||
    boost::beast::http::request<boost::beast::http::string_body> request_;
 | 
			
		||||
    bool sslEnabled_{false};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Request Builder object
 | 
			
		||||
     *
 | 
			
		||||
     * @param host host to connect to
 | 
			
		||||
     * @param port port to connect to
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder(std::string host, std::string port);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Add a header to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param header header to add
 | 
			
		||||
     * @return reference to itself
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder&
 | 
			
		||||
    addHeader(HttpHeader const& header);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Add headers to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param headers headers to add
 | 
			
		||||
     * @return reference to itself
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder&
 | 
			
		||||
    addHeaders(std::vector<HttpHeader> const& headers);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Add body or data to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param data data to add
 | 
			
		||||
     * @return reference to itself
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder&
 | 
			
		||||
    addData(std::string data);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the timeout for the request
 | 
			
		||||
     *
 | 
			
		||||
     * @note Default timeout is defined in DEFAULT_TIMEOUT
 | 
			
		||||
     *
 | 
			
		||||
     * @param timeout timeout to set
 | 
			
		||||
     * @return reference to itself
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder&
 | 
			
		||||
    setTimeout(std::chrono::milliseconds timeout);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the target for the request
 | 
			
		||||
     *
 | 
			
		||||
     * @note Default target is "/"
 | 
			
		||||
     *
 | 
			
		||||
     * @param target target to set
 | 
			
		||||
     * @return reference to itself
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder&
 | 
			
		||||
    setTarget(std::string_view target);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set SSL enabled or disabled
 | 
			
		||||
     *
 | 
			
		||||
     * @note Default is false
 | 
			
		||||
     *
 | 
			
		||||
     * @param ssl boolean value to set
 | 
			
		||||
     * @return reference to itself
 | 
			
		||||
     */
 | 
			
		||||
    RequestBuilder&
 | 
			
		||||
    setSslEnabled(bool enabled);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Perform a GET request asynchronously
 | 
			
		||||
     *
 | 
			
		||||
     * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is
 | 
			
		||||
     * fine to call only get() or only post() of the same RequestBuilder from multiple threads.
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield yield context
 | 
			
		||||
     * @return expected response or error
 | 
			
		||||
     */
 | 
			
		||||
    Expected<std::string, RequestError>
 | 
			
		||||
    get(boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Perform a POST request asynchronously
 | 
			
		||||
     *
 | 
			
		||||
     * @note It is not thread-safe to call get() and post() of the same RequestBuilder from multiple threads. But it is
 | 
			
		||||
     * fine to call only get() or only post() of the same RequestBuilder from multiple threads.
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield yield context
 | 
			
		||||
     * @return expected response or error
 | 
			
		||||
     */
 | 
			
		||||
    Expected<std::string, RequestError>
 | 
			
		||||
    post(boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
    static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT{30000};
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    Expected<std::string, RequestError>
 | 
			
		||||
    doRequest(boost::asio::yield_context yield, boost::beast::http::verb method);
 | 
			
		||||
 | 
			
		||||
    template <typename StreamDataType>
 | 
			
		||||
    Expected<std::string, RequestError>
 | 
			
		||||
    doRequestImpl(StreamDataType&& streamData, boost::asio::yield_context yield, boost::beast::http::verb method);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests
 | 
			
		||||
							
								
								
									
										44
									
								
								src/util/requests/Types.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/util/requests/Types.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/requests/Types.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace util::requests {
 | 
			
		||||
 | 
			
		||||
RequestError::RequestError(std::string message) : message(std::move(message))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RequestError::RequestError(std::string msg, boost::beast::error_code const& ec) : message(std::move(msg))
 | 
			
		||||
{
 | 
			
		||||
    message.append(": ");
 | 
			
		||||
    message.append(ec.message());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HttpHeader::HttpHeader(boost::beast::http::field name, std::string value) : name(name), value(std::move(value))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests
 | 
			
		||||
							
								
								
									
										61
									
								
								src/util/requests/Types.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/util/requests/Types.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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 <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace util::requests {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Error type for HTTP requests
 | 
			
		||||
 */
 | 
			
		||||
struct RequestError {
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Request Error object
 | 
			
		||||
     *
 | 
			
		||||
     * @param message error message
 | 
			
		||||
     */
 | 
			
		||||
    explicit RequestError(std::string message);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new Request Error object
 | 
			
		||||
     *
 | 
			
		||||
     * @param message error message
 | 
			
		||||
     * @param ec error code from boost::beast
 | 
			
		||||
     */
 | 
			
		||||
    RequestError(std::string msg, boost::beast::error_code const& ec);
 | 
			
		||||
 | 
			
		||||
    std::string message;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief HTTP header
 | 
			
		||||
 */
 | 
			
		||||
struct HttpHeader {
 | 
			
		||||
    HttpHeader(boost::beast::http::field name, std::string value);
 | 
			
		||||
 | 
			
		||||
    boost::beast::http::field name;
 | 
			
		||||
    std::string value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests
 | 
			
		||||
							
								
								
									
										172
									
								
								src/util/requests/WsConnection.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/util/requests/WsConnection.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,172 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2024, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/requests/WsConnection.h"
 | 
			
		||||
 | 
			
		||||
#include "util/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
#include "util/requests/impl/StreamData.h"
 | 
			
		||||
#include "util/requests/impl/WsConnectionImpl.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio.hpp>
 | 
			
		||||
#include <boost/asio/associated_executor.hpp>
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/error.hpp>
 | 
			
		||||
#include <boost/asio/ssl/stream_base.hpp>
 | 
			
		||||
#include <boost/beast.hpp>
 | 
			
		||||
#include <boost/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/make_printable.hpp>
 | 
			
		||||
#include <boost/beast/core/role.hpp>
 | 
			
		||||
#include <boost/beast/core/stream_traits.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/version.hpp>
 | 
			
		||||
#include <boost/beast/websocket/rfc6455.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream_base.hpp>
 | 
			
		||||
#include <fmt/core.h>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <openssl/err.h>
 | 
			
		||||
#include <openssl/tls1.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace util::requests {
 | 
			
		||||
 | 
			
		||||
namespace beast = boost::beast;
 | 
			
		||||
namespace websocket = beast::websocket;
 | 
			
		||||
namespace asio = boost::asio;
 | 
			
		||||
 | 
			
		||||
WsConnectionBuilder::WsConnectionBuilder(std::string host, std::string port)
 | 
			
		||||
    : host_(std::move(host)), port_(std::move(port))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
WsConnectionBuilder&
 | 
			
		||||
WsConnectionBuilder::addHeader(HttpHeader header)
 | 
			
		||||
{
 | 
			
		||||
    headers_.push_back(std::move(header));
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
WsConnectionBuilder&
 | 
			
		||||
WsConnectionBuilder::addHeaders(std::vector<HttpHeader> headers)
 | 
			
		||||
{
 | 
			
		||||
    headers_.insert(headers_.end(), std::make_move_iterator(headers.begin()), std::make_move_iterator(headers.end()));
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
WsConnectionBuilder&
 | 
			
		||||
WsConnectionBuilder::setTarget(std::string target)
 | 
			
		||||
{
 | 
			
		||||
    target_ = std::move(target);
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
WsConnectionBuilder&
 | 
			
		||||
WsConnectionBuilder::setConnectionTimeout(std::chrono::milliseconds timeout)
 | 
			
		||||
{
 | 
			
		||||
    timeout_ = timeout;
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
WsConnectionBuilder&
 | 
			
		||||
WsConnectionBuilder::setSslEnabled(bool sslEnabled)
 | 
			
		||||
{
 | 
			
		||||
    sslEnabled_ = sslEnabled;
 | 
			
		||||
    return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Expected<WsConnectionPtr, RequestError>
 | 
			
		||||
WsConnectionBuilder::connect(asio::yield_context yield) const
 | 
			
		||||
{
 | 
			
		||||
    if (sslEnabled_) {
 | 
			
		||||
        auto streamData = impl::SslWsStreamData::create(yield);
 | 
			
		||||
        if (not streamData.has_value())
 | 
			
		||||
            return Unexpected{std::move(streamData).error()};
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
 | 
			
		||||
        if (!SSL_set_tlsext_host_name(streamData->stream.next_layer().native_handle(), host_.c_str())) {
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
            beast::error_code errorCode;
 | 
			
		||||
            errorCode.assign(static_cast<int>(::ERR_get_error()), beast::net::error::get_ssl_category());
 | 
			
		||||
            return Unexpected{RequestError{"SSL setup failed", errorCode}};
 | 
			
		||||
        }
 | 
			
		||||
        return connectImpl(std::move(streamData).value(), yield);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return connectImpl(impl::WsStreamData{yield}, yield);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template <typename StreamDataType>
 | 
			
		||||
Expected<WsConnectionPtr, RequestError>
 | 
			
		||||
WsConnectionBuilder::connectImpl(StreamDataType&& streamData, asio::yield_context yield) const
 | 
			
		||||
{
 | 
			
		||||
    auto context = asio::get_associated_executor(yield);
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
 | 
			
		||||
    asio::ip::tcp::resolver resolver(context);
 | 
			
		||||
    auto const results = resolver.async_resolve(host_, port_, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Resolve error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    auto& ws = streamData.stream;
 | 
			
		||||
 | 
			
		||||
    beast::get_lowest_layer(ws).expires_after(timeout_);
 | 
			
		||||
    auto endpoint = beast::get_lowest_layer(ws).async_connect(results, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Connect error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    if constexpr (StreamDataType::sslEnabled) {
 | 
			
		||||
        beast::get_lowest_layer(ws).expires_after(timeout_);
 | 
			
		||||
        ws.next_layer().async_handshake(asio::ssl::stream_base::client, yield[errorCode]);
 | 
			
		||||
        if (errorCode)
 | 
			
		||||
            return Unexpected{RequestError{"SSL handshake error", errorCode}};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Turn off the timeout on the tcp_stream, because the websocket stream has its own timeout system
 | 
			
		||||
    beast::get_lowest_layer(ws).expires_never();
 | 
			
		||||
 | 
			
		||||
    ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client));
 | 
			
		||||
    ws.set_option(websocket::stream_base::decorator([this](websocket::request_type& req) {
 | 
			
		||||
        for (auto const& header : headers_)
 | 
			
		||||
            req.set(header.name, header.value);
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    std::string const host = fmt::format("{}:{}", host_, endpoint.port());
 | 
			
		||||
    ws.async_handshake(host, target_, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return Unexpected{RequestError{"Handshake error", errorCode}};
 | 
			
		||||
 | 
			
		||||
    if constexpr (StreamDataType::sslEnabled) {
 | 
			
		||||
        return std::make_unique<impl::SslWsConnection>(std::move(ws));
 | 
			
		||||
    } else {
 | 
			
		||||
        return std::make_unique<impl::PlainWsConnection>(std::move(ws));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests
 | 
			
		||||
							
								
								
									
										131
									
								
								src/util/requests/WsConnection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/util/requests/WsConnection.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream.hpp>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace util::requests {
 | 
			
		||||
 | 
			
		||||
class WsConnection {
 | 
			
		||||
public:
 | 
			
		||||
    virtual ~WsConnection() = default;
 | 
			
		||||
 | 
			
		||||
    virtual Expected<std::string, RequestError>
 | 
			
		||||
    read(boost::asio::yield_context yield) = 0;
 | 
			
		||||
 | 
			
		||||
    virtual std::optional<RequestError>
 | 
			
		||||
    write(std::string const& message, boost::asio::yield_context yield) = 0;
 | 
			
		||||
 | 
			
		||||
    virtual std::optional<RequestError>
 | 
			
		||||
    close(boost::asio::yield_context yield) = 0;
 | 
			
		||||
};
 | 
			
		||||
using WsConnectionPtr = std::unique_ptr<WsConnection>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Builder for WebSocket connections
 | 
			
		||||
 */
 | 
			
		||||
class WsConnectionBuilder {
 | 
			
		||||
    std::string host_;
 | 
			
		||||
    std::string port_;
 | 
			
		||||
    std::vector<HttpHeader> headers_;
 | 
			
		||||
    std::chrono::milliseconds timeout_{DEFAULT_TIMEOUT};
 | 
			
		||||
    std::string target_{"/"};
 | 
			
		||||
    bool sslEnabled_{false};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    WsConnectionBuilder(std::string host, std::string port);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Add a header to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param header header to add
 | 
			
		||||
     * @return RequestBuilder& this
 | 
			
		||||
     */
 | 
			
		||||
    WsConnectionBuilder&
 | 
			
		||||
    addHeader(HttpHeader header);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Add multiple headers to the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param headers headers to add
 | 
			
		||||
     * @return RequestBuilder& this
 | 
			
		||||
     */
 | 
			
		||||
    WsConnectionBuilder&
 | 
			
		||||
    addHeaders(std::vector<HttpHeader> headers);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the target of the request
 | 
			
		||||
     *
 | 
			
		||||
     * @param target target to set
 | 
			
		||||
     * @return RequestBuilder& this
 | 
			
		||||
     */
 | 
			
		||||
    WsConnectionBuilder&
 | 
			
		||||
    setTarget(std::string target);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set the timeout for connection establishing operations
 | 
			
		||||
     *
 | 
			
		||||
     * @param timeout timeout to set
 | 
			
		||||
     * @return RequestBuilder& this
 | 
			
		||||
     */
 | 
			
		||||
    WsConnectionBuilder&
 | 
			
		||||
    setConnectionTimeout(std::chrono::milliseconds timeout);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Set whether SSL is enabled
 | 
			
		||||
     *
 | 
			
		||||
     * @note Default is false
 | 
			
		||||
     *
 | 
			
		||||
     * @param enabled whether SSL is enabled
 | 
			
		||||
     * @return RequestBuilder& this
 | 
			
		||||
     */
 | 
			
		||||
    WsConnectionBuilder&
 | 
			
		||||
    setSslEnabled(bool enabled);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Connect to the host asynchronously
 | 
			
		||||
     *
 | 
			
		||||
     * @param yield yield context
 | 
			
		||||
     * @return Expected<WsConnection, RequestError> WebSocket connection or error
 | 
			
		||||
     */
 | 
			
		||||
    Expected<WsConnectionPtr, RequestError>
 | 
			
		||||
    connect(boost::asio::yield_context yield) const;
 | 
			
		||||
 | 
			
		||||
    static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT{5000};
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    template <class StreamDataType>
 | 
			
		||||
    Expected<WsConnectionPtr, RequestError>
 | 
			
		||||
    connectImpl(StreamDataType&& streamData, boost::asio::yield_context yield) const;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests
 | 
			
		||||
							
								
								
									
										90
									
								
								src/util/requests/impl/SslContext.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/util/requests/impl/SslContext.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/requests/impl/SslContext.h"
 | 
			
		||||
 | 
			
		||||
#include "util/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
#include <boost/asio/ssl/verify_mode.hpp>
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <fstream>
 | 
			
		||||
#include <ios>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace util::requests::impl {
 | 
			
		||||
 | 
			
		||||
namespace asio = boost::asio;
 | 
			
		||||
namespace ssl = asio::ssl;
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
// Taken from https://go.dev/src/crypto/x509/root_linux.go
 | 
			
		||||
 | 
			
		||||
constexpr std::array CERT_FILE_PATHS{
 | 
			
		||||
    "/etc/ssl/certs/ca-certificates.crt",                 // Debian/Ubuntu/Gentoo etc.
 | 
			
		||||
    "/etc/pki/tls/certs/ca-bundle.crt",                   // Fedora/RHEL 6
 | 
			
		||||
    "/etc/ssl/ca-bundle.pem",                             // OpenSUSE
 | 
			
		||||
    "/etc/pki/tls/cacert.pem",                            // OpenELEC
 | 
			
		||||
    "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",  // CentOS/RHEL 7
 | 
			
		||||
    "/etc/ssl/cert.pem",                                  // Alpine Linux
 | 
			
		||||
    "/etc/ssl/certs",                                     // SLES10/SLES11, https://golang.org/issue/12139
 | 
			
		||||
    "/etc/pki/tls/certs",                                 // Fedora/RHEL
 | 
			
		||||
    "/system/etc/security/cacerts",                       // Android
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Expected<std::string, RequestError>
 | 
			
		||||
getRootCertificate()
 | 
			
		||||
{
 | 
			
		||||
    for (auto const& path : CERT_FILE_PATHS) {
 | 
			
		||||
        if (std::filesystem::exists(path)) {
 | 
			
		||||
            std::ifstream fileStream{path, std::ios::in};
 | 
			
		||||
            if (not fileStream.is_open()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            std::stringstream buffer;
 | 
			
		||||
            buffer << fileStream.rdbuf();
 | 
			
		||||
            return std::move(buffer).str();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return Unexpected{RequestError{"SSL setup failed: could not find root certificate"}};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
Expected<boost::asio::ssl::context, RequestError>
 | 
			
		||||
makeSslContext()
 | 
			
		||||
{
 | 
			
		||||
    ssl::context context{ssl::context::sslv23_client};
 | 
			
		||||
    context.set_verify_mode(ssl::verify_peer);
 | 
			
		||||
    auto const rootCertificate = getRootCertificate();
 | 
			
		||||
    if (not rootCertificate.has_value()) {
 | 
			
		||||
        return Unexpected{rootCertificate.error()};
 | 
			
		||||
    }
 | 
			
		||||
    context.add_certificate_authority(asio::buffer(rootCertificate->data(), rootCertificate->size()));
 | 
			
		||||
    return context;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests::impl
 | 
			
		||||
							
								
								
									
										32
									
								
								src/util/requests/impl/SslContext.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/util/requests/impl/SslContext.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
 | 
			
		||||
namespace util::requests::impl {
 | 
			
		||||
 | 
			
		||||
Expected<boost::asio::ssl::context, RequestError>
 | 
			
		||||
makeSslContext();
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests::impl
 | 
			
		||||
							
								
								
									
										85
									
								
								src/util/requests/impl/StreamData.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/util/requests/impl/StreamData.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
#include "util/requests/impl/SslContext.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/associated_executor.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/asio/ssl/context.hpp>
 | 
			
		||||
#include <boost/asio/ssl/stream.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/ssl/ssl_stream.hpp>
 | 
			
		||||
#include <boost/beast/websocket.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream.hpp>
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace util::requests::impl {
 | 
			
		||||
 | 
			
		||||
template <typename StreamType>
 | 
			
		||||
struct PlainStreamData {
 | 
			
		||||
    static constexpr bool sslEnabled = false;
 | 
			
		||||
 | 
			
		||||
    explicit PlainStreamData(boost::asio::yield_context yield) : stream(boost::asio::get_associated_executor(yield))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    StreamType stream;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using TcpStreamData = PlainStreamData<boost::beast::tcp_stream>;
 | 
			
		||||
using WsStreamData = PlainStreamData<boost::beast::websocket::stream<boost::beast::tcp_stream>>;
 | 
			
		||||
 | 
			
		||||
template <typename StreamType>
 | 
			
		||||
class SslStreamData {
 | 
			
		||||
    boost::asio::ssl::context sslContext_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    static constexpr bool sslEnabled = true;
 | 
			
		||||
 | 
			
		||||
    static Expected<SslStreamData, RequestError>
 | 
			
		||||
    create(boost::asio::yield_context yield)
 | 
			
		||||
    {
 | 
			
		||||
        auto sslContext = makeSslContext();
 | 
			
		||||
        if (not sslContext.has_value()) {
 | 
			
		||||
            return Unexpected{std::move(sslContext.error())};
 | 
			
		||||
        }
 | 
			
		||||
        return SslStreamData{std::move(sslContext).value(), yield};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    StreamType stream;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    SslStreamData(boost::asio::ssl::context sslContext, boost::asio::yield_context yield)
 | 
			
		||||
        : sslContext_(std::move(sslContext)), stream(boost::asio::get_associated_executor(yield), sslContext_)
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using SslTcpStreamData = SslStreamData<boost::beast::ssl_stream<boost::beast::tcp_stream>>;
 | 
			
		||||
using SslWsStreamData =
 | 
			
		||||
    SslStreamData<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>>;
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests::impl
 | 
			
		||||
							
								
								
									
										92
									
								
								src/util/requests/impl/WsConnectionImpl.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/util/requests/impl/WsConnectionImpl.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Expected.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
#include "util/requests/WsConnection.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/ssl/ssl_stream.hpp>
 | 
			
		||||
#include <boost/beast/websocket/rfc6455.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream.hpp>
 | 
			
		||||
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace util::requests::impl {
 | 
			
		||||
 | 
			
		||||
template <typename StreamType>
 | 
			
		||||
class WsConnectionImpl : public WsConnection {
 | 
			
		||||
    StreamType ws_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    explicit WsConnectionImpl(StreamType ws) : ws_(std::move(ws))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Expected<std::string, RequestError>
 | 
			
		||||
    read(boost::asio::yield_context yield) override
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::error_code errorCode;
 | 
			
		||||
        boost::beast::flat_buffer buffer;
 | 
			
		||||
 | 
			
		||||
        ws_.async_read(buffer, yield[errorCode]);
 | 
			
		||||
 | 
			
		||||
        if (errorCode)
 | 
			
		||||
            return Unexpected{RequestError{"Read error", errorCode}};
 | 
			
		||||
 | 
			
		||||
        return boost::beast::buffers_to_string(std::move(buffer).data());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::optional<RequestError>
 | 
			
		||||
    write(std::string const& message, boost::asio::yield_context yield) override
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::error_code errorCode;
 | 
			
		||||
        ws_.async_write(boost::asio::buffer(message), yield[errorCode]);
 | 
			
		||||
 | 
			
		||||
        if (errorCode)
 | 
			
		||||
            return RequestError{"Write error", errorCode};
 | 
			
		||||
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::optional<RequestError>
 | 
			
		||||
    close(boost::asio::yield_context yield) override
 | 
			
		||||
    {
 | 
			
		||||
        boost::beast::error_code errorCode;
 | 
			
		||||
        ws_.async_close(boost::beast::websocket::close_code::normal, yield[errorCode]);
 | 
			
		||||
        if (errorCode)
 | 
			
		||||
            return RequestError{"Close error", errorCode};
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>;
 | 
			
		||||
using SslWsConnection =
 | 
			
		||||
    WsConnectionImpl<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>>;
 | 
			
		||||
 | 
			
		||||
}  // namespace util::requests::impl
 | 
			
		||||
@@ -27,6 +27,7 @@
 | 
			
		||||
 * Supported custom command line options for clio_tests:
 | 
			
		||||
 *   --backend_host=<host>         - sets the cassandra/scylladb host for backend tests
 | 
			
		||||
 *   --backend_keyspace=<keyspace> - sets the cassandra/scylladb keyspace for backend tests
 | 
			
		||||
 *   --clean-gcda                  - delete all gcda files defore running tests
 | 
			
		||||
 */
 | 
			
		||||
int
 | 
			
		||||
main(int argc, char* argv[])
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										124
									
								
								unittests/util/TestHttpServer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								unittests/util/TestHttpServer.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/TestHttpServer.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.hpp>
 | 
			
		||||
#include <boost/asio/detached.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/beast.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/http/error.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/message_generator.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace beast = boost::beast;
 | 
			
		||||
namespace http = beast::http;
 | 
			
		||||
namespace asio = boost::asio;
 | 
			
		||||
using tcp = boost::asio::ip::tcp;
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
doSession(beast::tcp_stream stream, TestHttpServer::RequestHandler requestHandler, asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
 | 
			
		||||
    // This buffer is required to persist across reads
 | 
			
		||||
    beast::flat_buffer buffer;
 | 
			
		||||
 | 
			
		||||
    // This lambda is used to send messages
 | 
			
		||||
    // Set the timeout.
 | 
			
		||||
    stream.expires_after(std::chrono::seconds(5));
 | 
			
		||||
 | 
			
		||||
    // Read a request
 | 
			
		||||
    http::request<http::string_body> req;
 | 
			
		||||
    http::async_read(stream, buffer, req, yield[errorCode]);
 | 
			
		||||
    if (errorCode == http::error::end_of_stream)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    ASSERT_FALSE(errorCode) << errorCode.message();
 | 
			
		||||
 | 
			
		||||
    auto response = requestHandler(req);
 | 
			
		||||
 | 
			
		||||
    if (not response)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    bool const keep_alive = response->keep_alive();
 | 
			
		||||
 | 
			
		||||
    http::message_generator messageGenerator{std::move(response).value()};
 | 
			
		||||
 | 
			
		||||
    // Send the response
 | 
			
		||||
    beast::async_write(stream, std::move(messageGenerator), yield[errorCode]);
 | 
			
		||||
 | 
			
		||||
    ASSERT_FALSE(errorCode) << errorCode.message();
 | 
			
		||||
 | 
			
		||||
    if (!keep_alive) {
 | 
			
		||||
        // This means we should close the connection, usually because
 | 
			
		||||
        // the response indicated the "Connection: close" semantic.
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Send a TCP shutdown
 | 
			
		||||
    stream.socket().shutdown(tcp::socket::shutdown_send, errorCode);
 | 
			
		||||
 | 
			
		||||
    // At this point the connection is closed gracefully
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string host, int const port) : acceptor_(context)
 | 
			
		||||
{
 | 
			
		||||
    boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::make_address(host), port);
 | 
			
		||||
    acceptor_.open(endpoint.protocol());
 | 
			
		||||
    acceptor_.set_option(asio::socket_base::reuse_address(true));
 | 
			
		||||
    acceptor_.bind(endpoint);
 | 
			
		||||
    acceptor_.listen(asio::socket_base::max_listen_connections);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler)
 | 
			
		||||
{
 | 
			
		||||
    boost::asio::spawn(
 | 
			
		||||
        acceptor_.get_executor(),
 | 
			
		||||
        [this, handler = std::move(handler)](asio::yield_context yield) mutable {
 | 
			
		||||
            boost::beast::error_code errorCode;
 | 
			
		||||
            tcp::socket socket(this->acceptor_.get_executor());
 | 
			
		||||
            acceptor_.async_accept(socket, yield[errorCode]);
 | 
			
		||||
 | 
			
		||||
            [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
 | 
			
		||||
 | 
			
		||||
            doSession(beast::tcp_stream{std::move(socket)}, std::move(handler), yield);
 | 
			
		||||
        },
 | 
			
		||||
        boost::asio::detached
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								unittests/util/TestHttpServer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								unittests/util/TestHttpServer.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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 <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/beast/http/message.hpp>
 | 
			
		||||
#include <boost/beast/http/string_body.hpp>
 | 
			
		||||
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Simple HTTP server for use in unit tests
 | 
			
		||||
 */
 | 
			
		||||
class TestHttpServer {
 | 
			
		||||
public:
 | 
			
		||||
    using RequestHandler = std::function<std::optional<boost::beast::http::response<
 | 
			
		||||
        boost::beast::http::string_body>>(boost::beast::http::request<boost::beast::http::string_body>)>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Construct a new TestHttpServer
 | 
			
		||||
     *
 | 
			
		||||
     * @param context boost::asio::io_context to use for networking
 | 
			
		||||
     * @param host host to bind to
 | 
			
		||||
     * @param port port to bind to
 | 
			
		||||
     */
 | 
			
		||||
    TestHttpServer(boost::asio::io_context& context, std::string host, int port);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Start the server
 | 
			
		||||
     *
 | 
			
		||||
     * @note This method schedules to process only one request
 | 
			
		||||
     *
 | 
			
		||||
     * @param handler RequestHandler to use for incoming request
 | 
			
		||||
     */
 | 
			
		||||
    void
 | 
			
		||||
    handleRequest(RequestHandler handler);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    boost::asio::ip::tcp::acceptor acceptor_;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										117
									
								
								unittests/util/TestWsServer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								unittests/util/TestWsServer.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/TestWsServer.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/buffer.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/beast/core/buffers_to_string.hpp>
 | 
			
		||||
#include <boost/beast/core/error.hpp>
 | 
			
		||||
#include <boost/beast/core/flat_buffer.hpp>
 | 
			
		||||
#include <boost/beast/core/role.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 <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <iostream>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace asio = boost::asio;
 | 
			
		||||
namespace beast = boost::beast;
 | 
			
		||||
namespace websocket = boost::beast::websocket;
 | 
			
		||||
 | 
			
		||||
TestWsConnection::TestWsConnection(asio::ip::tcp::socket&& socket, boost::asio::yield_context yield)
 | 
			
		||||
    : ws_(std::move(socket))
 | 
			
		||||
{
 | 
			
		||||
    ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    ws_.async_accept(yield[errorCode]);
 | 
			
		||||
    [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string>
 | 
			
		||||
TestWsConnection::send(std::string const& message, boost::asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    ws_.async_write(asio::buffer(message), yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return errorCode.message();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string>
 | 
			
		||||
TestWsConnection::receive(boost::asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    beast::flat_buffer buffer;
 | 
			
		||||
 | 
			
		||||
    ws_.async_read(buffer, yield[errorCode]);
 | 
			
		||||
    if (errorCode == websocket::error::closed)
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
 | 
			
		||||
    [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
 | 
			
		||||
    return beast::buffers_to_string(buffer.data());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::optional<std::string>
 | 
			
		||||
TestWsConnection::close(boost::asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    ws_.async_close(websocket::close_code::normal, yield[errorCode]);
 | 
			
		||||
    if (errorCode)
 | 
			
		||||
        return errorCode.message();
 | 
			
		||||
    return std::nullopt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TestWsServer::TestWsServer(asio::io_context& context, std::string const& host, int port) : acceptor_(context)
 | 
			
		||||
{
 | 
			
		||||
    auto endpoint = asio::ip::tcp::endpoint(boost::asio::ip::make_address(host), port);
 | 
			
		||||
    acceptor_.open(endpoint.protocol());
 | 
			
		||||
    acceptor_.set_option(asio::socket_base::reuse_address(true));
 | 
			
		||||
    acceptor_.bind(endpoint);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TestWsConnection
 | 
			
		||||
TestWsServer::acceptConnection(asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    acceptor_.listen(asio::socket_base::max_listen_connections);
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    asio::ip::tcp::socket socket(acceptor_.get_executor());
 | 
			
		||||
    acceptor_.async_accept(socket, yield[errorCode]);
 | 
			
		||||
    [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
 | 
			
		||||
    return TestWsConnection(std::move(socket), yield);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
TestWsServer::acceptConnectionAndDropIt(asio::yield_context yield)
 | 
			
		||||
{
 | 
			
		||||
    acceptor_.listen(asio::socket_base::max_listen_connections);
 | 
			
		||||
    beast::error_code errorCode;
 | 
			
		||||
    asio::ip::tcp::socket socket(acceptor_.get_executor());
 | 
			
		||||
    acceptor_.async_accept(socket, yield[errorCode]);
 | 
			
		||||
    [&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								unittests/util/TestWsServer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								unittests/util/TestWsServer.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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 <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/ip/tcp.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/core/tcp_stream.hpp>
 | 
			
		||||
#include <boost/beast/websocket/stream.hpp>
 | 
			
		||||
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
class TestWsConnection {
 | 
			
		||||
    boost::beast::websocket::stream<boost::beast::tcp_stream> ws_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    using SendCallback = std::function<void()>;
 | 
			
		||||
    using ReceiveCallback = std::function<void(std::string)>;
 | 
			
		||||
 | 
			
		||||
    TestWsConnection(boost::asio::ip::tcp::socket&& socket, boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
    // returns error message if error occurs
 | 
			
		||||
    std::optional<std::string>
 | 
			
		||||
    send(std::string const& message, boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
    // returns nullopt if the connection is closed
 | 
			
		||||
    std::optional<std::string>
 | 
			
		||||
    receive(boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
    std::optional<std::string>
 | 
			
		||||
    close(boost::asio::yield_context yield);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class TestWsServer {
 | 
			
		||||
    boost::asio::ip::tcp::acceptor acceptor_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    TestWsServer(boost::asio::io_context& context, std::string const& host, int port);
 | 
			
		||||
 | 
			
		||||
    TestWsConnection
 | 
			
		||||
    acceptConnection(boost::asio::yield_context yield);
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    acceptConnectionAndDropIt(boost::asio::yield_context yield);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										189
									
								
								unittests/util/requests/RequestBuilderTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								unittests/util/requests/RequestBuilderTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Expected.h"
 | 
			
		||||
#include "util/Fixtures.h"
 | 
			
		||||
#include "util/TestHttpServer.h"
 | 
			
		||||
#include "util/requests/RequestBuilder.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/io_context.hpp>
 | 
			
		||||
#include <boost/asio/spawn.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 <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace util::requests;
 | 
			
		||||
using namespace boost;
 | 
			
		||||
using namespace boost::beast;
 | 
			
		||||
 | 
			
		||||
struct RequestBuilderTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    http::verb method;
 | 
			
		||||
    std::vector<HttpHeader> headers;
 | 
			
		||||
    std::string target;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct RequestBuilderTest : SyncAsioContextTest, testing::WithParamInterface<RequestBuilderTestBundle> {
 | 
			
		||||
    TestHttpServer server{ctx, "0.0.0.0", 11111};
 | 
			
		||||
    RequestBuilder builder{"localhost", "11111"};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    RequestBuilderTest,
 | 
			
		||||
    RequestBuilderTest,
 | 
			
		||||
    testing::Values(
 | 
			
		||||
        RequestBuilderTestBundle{"GetSimple", http::verb::get, {}, "/"},
 | 
			
		||||
        RequestBuilderTestBundle{
 | 
			
		||||
            "GetWithHeaders",
 | 
			
		||||
            http::verb::get,
 | 
			
		||||
            {{http::field::accept, "text/html"}, {http::field::authorization, "password"}},
 | 
			
		||||
            "/"
 | 
			
		||||
        },
 | 
			
		||||
        RequestBuilderTestBundle{"GetWithTarget", http::verb::get, {}, "/test"},
 | 
			
		||||
        RequestBuilderTestBundle{"PostSimple", http::verb::post, {}, "/"},
 | 
			
		||||
        RequestBuilderTestBundle{
 | 
			
		||||
            "PostWithHeaders",
 | 
			
		||||
            http::verb::post,
 | 
			
		||||
            {{http::field::accept, "text/html"}, {http::field::authorization, "password"}},
 | 
			
		||||
            "/"
 | 
			
		||||
        },
 | 
			
		||||
        RequestBuilderTestBundle{"PostWithTarget", http::verb::post, {}, "/test"}
 | 
			
		||||
    ),
 | 
			
		||||
    [](auto const& info) { return info.param.testName; }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TEST_P(RequestBuilderTest, SimpleRequest)
 | 
			
		||||
{
 | 
			
		||||
    std::string const replyBody = "Hello, world!";
 | 
			
		||||
    builder.addHeaders(GetParam().headers);
 | 
			
		||||
    builder.setTarget(GetParam().target);
 | 
			
		||||
 | 
			
		||||
    server.handleRequest(
 | 
			
		||||
        [&replyBody](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
 | 
			
		||||
            [&]() {
 | 
			
		||||
                ASSERT_TRUE(request.target() == GetParam().target);
 | 
			
		||||
                ASSERT_TRUE(request.method() == GetParam().method);
 | 
			
		||||
            }();
 | 
			
		||||
            return http::response<http::string_body>{http::status::ok, 11, replyBody};
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    runSpawn([this, replyBody](asio::yield_context yield) {
 | 
			
		||||
        auto const response = [&]() -> util::Expected<std::string, RequestError> {
 | 
			
		||||
            switch (GetParam().method) {
 | 
			
		||||
                case http::verb::get:
 | 
			
		||||
                    return builder.get(yield);
 | 
			
		||||
                case http::verb::post:
 | 
			
		||||
                    return builder.post(yield);
 | 
			
		||||
                default:
 | 
			
		||||
                    return util::Unexpected{RequestError{"Invalid HTTP verb"}};
 | 
			
		||||
            }
 | 
			
		||||
        }();
 | 
			
		||||
        ASSERT_TRUE(response) << response.error().message;
 | 
			
		||||
        EXPECT_EQ(response.value(), replyBody);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestBuilderTest, Timeout)
 | 
			
		||||
{
 | 
			
		||||
    builder.setTimeout(std::chrono::milliseconds{10});
 | 
			
		||||
    server.handleRequest(
 | 
			
		||||
        [](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
 | 
			
		||||
            [&]() {
 | 
			
		||||
                ASSERT_TRUE(request.target() == "/");
 | 
			
		||||
                ASSERT_TRUE(request.method() == http::verb::get);
 | 
			
		||||
            }();
 | 
			
		||||
            std::this_thread::sleep_for(std::chrono::milliseconds{20});
 | 
			
		||||
            return std::nullopt;
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    runSpawn([this](asio::yield_context yield) {
 | 
			
		||||
        auto response = builder.get(yield);
 | 
			
		||||
        EXPECT_FALSE(response);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestBuilderTest, RequestWithBody)
 | 
			
		||||
{
 | 
			
		||||
    std::string const requestBody = "Hello, world!";
 | 
			
		||||
    std::string const replyBody = "Hello, client!";
 | 
			
		||||
    builder.addData(requestBody);
 | 
			
		||||
 | 
			
		||||
    server.handleRequest(
 | 
			
		||||
        [&](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
 | 
			
		||||
            [&]() {
 | 
			
		||||
                EXPECT_EQ(request.target(), "/");
 | 
			
		||||
                EXPECT_EQ(request.method(), http::verb::get);
 | 
			
		||||
                EXPECT_EQ(request.body(), requestBody);
 | 
			
		||||
            }();
 | 
			
		||||
 | 
			
		||||
            return http::response<http::string_body>{http::status::ok, 11, replyBody};
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto const response = builder.get(yield);
 | 
			
		||||
        ASSERT_TRUE(response) << response.error().message;
 | 
			
		||||
        EXPECT_EQ(response.value(), replyBody) << response.value();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestBuilderTest, ResolveError)
 | 
			
		||||
{
 | 
			
		||||
    builder = RequestBuilder{"wrong_host", "11111"};
 | 
			
		||||
    runSpawn([this](asio::yield_context yield) {
 | 
			
		||||
        auto const response = builder.get(yield);
 | 
			
		||||
        ASSERT_FALSE(response);
 | 
			
		||||
        EXPECT_TRUE(response.error().message.starts_with("Resolve error")) << response.error().message;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestBuilderTest, ConnectionError)
 | 
			
		||||
{
 | 
			
		||||
    builder = RequestBuilder{"localhost", "11112"};
 | 
			
		||||
    builder.setTimeout(std::chrono::milliseconds{1});
 | 
			
		||||
    runSpawn([this](asio::yield_context yield) {
 | 
			
		||||
        auto const response = builder.get(yield);
 | 
			
		||||
        ASSERT_FALSE(response);
 | 
			
		||||
        EXPECT_TRUE(response.error().message.starts_with("Connection error")) << response.error().message;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(RequestBuilderTest, WritingError)
 | 
			
		||||
{
 | 
			
		||||
    server.handleRequest(
 | 
			
		||||
        [](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
 | 
			
		||||
            [&]() {
 | 
			
		||||
                EXPECT_EQ(request.target(), "/");
 | 
			
		||||
                EXPECT_EQ(request.method(), http::verb::get);
 | 
			
		||||
            }();
 | 
			
		||||
            return std::nullopt;
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								unittests/util/requests/SslContextTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								unittests/util/requests/SslContextTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/requests/impl/SslContext.h"
 | 
			
		||||
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
using namespace util::requests::impl;
 | 
			
		||||
 | 
			
		||||
TEST(SslContext, Create)
 | 
			
		||||
{
 | 
			
		||||
    auto ctx = makeSslContext();
 | 
			
		||||
    EXPECT_TRUE(ctx);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										236
									
								
								unittests/util/requests/WsConnectionTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								unittests/util/requests/WsConnectionTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,236 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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/Fixtures.h"
 | 
			
		||||
#include "util/TestWsServer.h"
 | 
			
		||||
#include "util/requests/Types.h"
 | 
			
		||||
#include "util/requests/WsConnection.h"
 | 
			
		||||
 | 
			
		||||
#include <boost/asio/executor_work_guard.hpp>
 | 
			
		||||
#include <boost/asio/post.hpp>
 | 
			
		||||
#include <boost/asio/spawn.hpp>
 | 
			
		||||
#include <boost/beast/http/field.hpp>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace util::requests;
 | 
			
		||||
namespace asio = boost::asio;
 | 
			
		||||
namespace http = boost::beast::http;
 | 
			
		||||
 | 
			
		||||
struct WsConnectionTestsBase : SyncAsioContextTest {
 | 
			
		||||
    WsConnectionBuilder builder{"localhost", "11112"};
 | 
			
		||||
    TestWsServer server{ctx, "0.0.0.0", 11112};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct WsConnectionTestBundle {
 | 
			
		||||
    std::string testName;
 | 
			
		||||
    std::vector<HttpHeader> headers;
 | 
			
		||||
    std::optional<std::string> target;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct WsConnectionTests : WsConnectionTestsBase, testing::WithParamInterface<WsConnectionTestBundle> {
 | 
			
		||||
    WsConnectionTests()
 | 
			
		||||
    {
 | 
			
		||||
        [this]() { ASSERT_EQ(clientMessages.size(), serverMessages.size()); }();
 | 
			
		||||
    }
 | 
			
		||||
    std::vector<std::string> const clientMessages{"hello", "world"};
 | 
			
		||||
 | 
			
		||||
    std::vector<std::string> const serverMessages{"goodbye", "point"};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_CASE_P(
 | 
			
		||||
    WsConnectionTestsGroup,
 | 
			
		||||
    WsConnectionTests,
 | 
			
		||||
    testing::Values(
 | 
			
		||||
        WsConnectionTestBundle{"noHeaders", {}, std::nullopt},
 | 
			
		||||
        WsConnectionTestBundle{"singleHeader", {{http::field::accept, "text/html"}}, std::nullopt},
 | 
			
		||||
        WsConnectionTestBundle{
 | 
			
		||||
            "multiple headers",
 | 
			
		||||
            {{http::field::accept, "text/html"}, {http::field::authorization, "password"}},
 | 
			
		||||
            std::nullopt
 | 
			
		||||
        },
 | 
			
		||||
        WsConnectionTestBundle{"target", {}, "/target"}
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TEST_P(WsConnectionTests, SendAndReceive)
 | 
			
		||||
{
 | 
			
		||||
    auto target = GetParam().target;
 | 
			
		||||
    if (target) {
 | 
			
		||||
        builder.setTarget(*target);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (auto const& header : GetParam().headers) {
 | 
			
		||||
        builder.addHeader(header);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    asio::spawn(ctx, [&](asio::yield_context yield) {
 | 
			
		||||
        auto serverConnection = server.acceptConnection(yield);
 | 
			
		||||
 | 
			
		||||
        for (size_t i = 0; i < clientMessages.size(); ++i) {
 | 
			
		||||
            auto message = serverConnection.receive(yield);
 | 
			
		||||
            EXPECT_EQ(clientMessages.at(i), message);
 | 
			
		||||
 | 
			
		||||
            auto error = serverConnection.send(serverMessages.at(i), yield);
 | 
			
		||||
            ASSERT_FALSE(error) << *error;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto maybeConnection = builder.connect(yield);
 | 
			
		||||
        ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message;
 | 
			
		||||
        auto& connection = *maybeConnection;
 | 
			
		||||
 | 
			
		||||
        for (size_t i = 0; i < serverMessages.size(); ++i) {
 | 
			
		||||
            auto error = connection->write(clientMessages.at(i), yield);
 | 
			
		||||
            ASSERT_FALSE(error) << error->message;
 | 
			
		||||
 | 
			
		||||
            auto message = connection->read(yield);
 | 
			
		||||
            ASSERT_TRUE(message.has_value()) << message.error().message;
 | 
			
		||||
            EXPECT_EQ(serverMessages.at(i), message.value());
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(WsConnectionTests, Timeout)
 | 
			
		||||
{
 | 
			
		||||
    builder.setConnectionTimeout(std::chrono::milliseconds{1});
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto connection = builder.connect(yield);
 | 
			
		||||
        ASSERT_FALSE(connection.has_value());
 | 
			
		||||
 | 
			
		||||
        EXPECT_TRUE(connection.error().message.starts_with("Connect error"));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(WsConnectionTests, ResolveError)
 | 
			
		||||
{
 | 
			
		||||
    builder = WsConnectionBuilder{"wrong_host", "11112"};
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto connection = builder.connect(yield);
 | 
			
		||||
        ASSERT_FALSE(connection.has_value());
 | 
			
		||||
        EXPECT_TRUE(connection.error().message.starts_with("Resolve error")) << connection.error().message;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(WsConnectionTests, WsHandshakeError)
 | 
			
		||||
{
 | 
			
		||||
    builder.setConnectionTimeout(std::chrono::milliseconds{1});
 | 
			
		||||
    asio::spawn(ctx, [&](asio::yield_context yield) { server.acceptConnectionAndDropIt(yield); });
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto connection = builder.connect(yield);
 | 
			
		||||
        ASSERT_FALSE(connection.has_value());
 | 
			
		||||
        EXPECT_TRUE(connection.error().message.starts_with("Handshake error")) << connection.error().message;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(WsConnectionTests, CloseConnection)
 | 
			
		||||
{
 | 
			
		||||
    asio::spawn(ctx, [&](asio::yield_context yield) {
 | 
			
		||||
        auto serverConnection = server.acceptConnection(yield);
 | 
			
		||||
 | 
			
		||||
        auto message = serverConnection.receive(yield);
 | 
			
		||||
        EXPECT_EQ(std::nullopt, message);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto connection = builder.connect(yield);
 | 
			
		||||
        ASSERT_TRUE(connection.has_value()) << connection.error().message;
 | 
			
		||||
 | 
			
		||||
        auto error = connection->operator*().close(yield);
 | 
			
		||||
        EXPECT_FALSE(error.has_value()) << error->message;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(WsConnectionTests, MultipleConnections)
 | 
			
		||||
{
 | 
			
		||||
    for (size_t i = 0; i < 2; ++i) {
 | 
			
		||||
        asio::spawn(ctx, [&](asio::yield_context yield) {
 | 
			
		||||
            auto serverConnection = server.acceptConnection(yield);
 | 
			
		||||
            auto message = serverConnection.receive(yield);
 | 
			
		||||
 | 
			
		||||
            ASSERT_TRUE(message.has_value());
 | 
			
		||||
            EXPECT_EQ(*message, "hello");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
            auto connection = builder.connect(yield);
 | 
			
		||||
            ASSERT_TRUE(connection.has_value()) << connection.error().message;
 | 
			
		||||
 | 
			
		||||
            auto error = connection->operator*().write("hello", yield);
 | 
			
		||||
            ASSERT_FALSE(error) << error->message;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum class WsConnectionErrorTestsBundle : int { Read = 1, Write = 2 };
 | 
			
		||||
 | 
			
		||||
struct WsConnectionErrorTests : WsConnectionTestsBase, testing::WithParamInterface<WsConnectionErrorTestsBundle> {};
 | 
			
		||||
 | 
			
		||||
INSTANTIATE_TEST_SUITE_P(
 | 
			
		||||
    WsConnectionErrorTestsGroup,
 | 
			
		||||
    WsConnectionErrorTests,
 | 
			
		||||
    testing::Values(WsConnectionErrorTestsBundle::Read, WsConnectionErrorTestsBundle::Write),
 | 
			
		||||
    [](auto const& info) {
 | 
			
		||||
        switch (info.param) {
 | 
			
		||||
            case WsConnectionErrorTestsBundle::Read:
 | 
			
		||||
                return "Read";
 | 
			
		||||
            case WsConnectionErrorTestsBundle::Write:
 | 
			
		||||
                return "Write";
 | 
			
		||||
        }
 | 
			
		||||
        return "Unknown";
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
TEST_P(WsConnectionErrorTests, WriteError)
 | 
			
		||||
{
 | 
			
		||||
    asio::spawn(ctx, [&](asio::yield_context yield) {
 | 
			
		||||
        auto serverConnection = server.acceptConnection(yield);
 | 
			
		||||
 | 
			
		||||
        auto error = serverConnection.close(yield);
 | 
			
		||||
        EXPECT_FALSE(error.has_value()) << *error;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    runSpawn([&](asio::yield_context yield) {
 | 
			
		||||
        auto maybeConnection = builder.connect(yield);
 | 
			
		||||
        ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message;
 | 
			
		||||
        auto& connection = *maybeConnection;
 | 
			
		||||
 | 
			
		||||
        auto error = connection->close(yield);
 | 
			
		||||
        EXPECT_FALSE(error.has_value()) << error->message;
 | 
			
		||||
 | 
			
		||||
        switch (GetParam()) {
 | 
			
		||||
            case WsConnectionErrorTestsBundle::Read: {
 | 
			
		||||
                auto const expected = connection->read(yield);
 | 
			
		||||
                EXPECT_FALSE(expected.has_value());
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case WsConnectionErrorTestsBundle::Write: {
 | 
			
		||||
                error = connection->write("hello", yield);
 | 
			
		||||
                EXPECT_TRUE(error.has_value());
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user