Requests library (#1140)

For #51.

First part of improving forwarding - library for easy async requests.
This commit is contained in:
Sergey Kuznetsov
2024-02-05 11:35:10 +00:00
committed by GitHub
parent 8f89a5913d
commit 957aadd25a
23 changed files with 1938 additions and 28 deletions

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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;

View File

@@ -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)}

View File

@@ -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

View 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

View 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

View 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
View 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

View File

@@ -0,0 +1,172 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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[])

View 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
);
}

View 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_;
};

View 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(); }();
}

View 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);
};

View 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;
}
);
}

View 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);
}

View 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;
}
}
});
}