refactor: Coroutine based webserver (#1699)

Code of new coroutine-based web server. The new server is not connected
to Clio and not ready to use yet.
For #919.
This commit is contained in:
Sergey Kuznetsov
2024-10-24 16:50:26 +01:00
committed by GitHub
parent cf081e7e25
commit cffda52ba6
58 changed files with 5581 additions and 488 deletions

View File

@@ -149,13 +149,6 @@ jobs:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
path: build/clio_*tests
- name: Upload test data
if: ${{ !matrix.code_coverage }}
uses: actions/upload-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ steps.conan.outputs.conan_profile }}
path: build/tests/unit/test_data
- name: Save cache
uses: ./.github/actions/save_cache
with:
@@ -219,11 +212,6 @@ jobs:
with:
name: clio_tests_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
- uses: actions/download-artifact@v4
with:
name: clio_test_data_${{ runner.os }}_${{ matrix.build_type }}_${{ matrix.conan_profile }}
path: tests/unit/test_data
- name: Run clio_tests
run: |
chmod +x ./clio_tests

View File

@@ -70,7 +70,14 @@
"admin_password": "xrp",
// If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests
// It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time
"local_admin": false
"local_admin": false,
"processing_policy": "parallel", // Could be "sequent" or "parallel".
// For sequent policy request from one client connection will be processed one by one and the next one will not be read before
// the previous one is processed. For parallel policy Clio will take all requests and process them in parallel and
// send a reply for each request whenever it is ready.
"parallel_requests_limit": 10 // Optional parameter, used only if "processing_strategy" is "parallel".
It limits the number of requests for one client connection processed in parallel. Infinite if not specified.
},
// Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet.
"graceful_period": 10.0,

View File

@@ -4,6 +4,7 @@ target_sources(
clio_util
PRIVATE build/Build.cpp
config/Config.cpp
CoroutineGroup.cpp
log/Logger.cpp
prometheus/Http.cpp
prometheus/Label.cpp

View File

@@ -0,0 +1,76 @@
//------------------------------------------------------------------------------
/*
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/CoroutineGroup.hpp"
#include "util/Assert.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <cstddef>
#include <functional>
#include <optional>
#include <utility>
namespace util {
CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren)
: timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren}
{
}
CoroutineGroup::~CoroutineGroup()
{
ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish");
}
bool
CoroutineGroup::spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn)
{
if (maxChildren_.has_value() && childrenCounter_ >= *maxChildren_)
return false;
++childrenCounter_;
boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) {
fn(yield);
--childrenCounter_;
if (childrenCounter_ == 0)
timer_.cancel();
});
return true;
}
void
CoroutineGroup::asyncWait(boost::asio::yield_context yield)
{
if (childrenCounter_ == 0)
return;
boost::system::error_code error;
timer_.async_wait(yield[error]);
}
size_t
CoroutineGroup::size() const
{
return childrenCounter_;
}
} // namespace util

View File

@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
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/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <cstddef>
#include <functional>
#include <optional>
namespace util {
/**
* @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and
* wait for all of them to finish.
*/
class CoroutineGroup {
boost::asio::steady_timer timer_;
std::optional<int> maxChildren_;
int childrenCounter_{0};
public:
/**
* @brief Construct a new Coroutine Group object
*
* @param yield The yield context to use for the internal timer
* @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there
* is no limit
*/
CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren = std::nullopt);
/**
* @brief Destroy the Coroutine Group object
*
* @note asyncWait() must be called before the object is destroyed
*/
~CoroutineGroup();
/**
* @brief Spawn a new coroutine in the group
*
* @param yield The yield context to use for the coroutine (it should be the same as the one used in the
* constructor)
* @param fn The function to execute
* @return true If the coroutine was spawned successfully. false if the maximum number of coroutines has been
* reached
*/
bool
spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn);
/**
* @brief Wait for all the coroutines in the group to finish
*
* @note This method must be called before the object is destroyed
*
* @param yield The yield context to use for the internal timer
*/
void
asyncWait(boost::asio::yield_context yield);
/**
* @brief Get the number of coroutines in the group
*
* @return size_t The number of coroutines in the group
*/
size_t
size() const;
};
} // namespace util

71
src/util/WithTimeout.hpp Normal file
View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
/*
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/associated_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/system/errc.hpp>
#include <chrono>
#include <ctime>
#include <memory>
namespace util {
/**
* @brief Perform a coroutine operation with a timeout.
*
* @tparam Operation The operation type to perform. Must be a callable accepting yield context with bound cancellation
* token.
* @param operation The operation to perform.
* @param yield The yield context.
* @param timeout The timeout duration.
* @return The error code of the operation.
*/
template <typename Operation>
boost::system::error_code
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
auto operationCompleted = std::make_shared<bool>(false);
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]);
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
timer.async_wait([&cancellationSignal, operationCompleted](boost::system::error_code errorCode) {
if (!errorCode and !*operationCompleted)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
*operationCompleted = true;
// Map error code to timeout
if (error == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return error;
}
} // namespace util

View File

@@ -19,6 +19,7 @@
#pragma once
#include "util/WithTimeout.hpp"
#include "util/requests/Types.hpp"
#include "util/requests/WsConnection.hpp"
@@ -67,15 +68,13 @@ public:
auto operation = [&](auto&& token) { ws_.async_read(buffer, token); };
if (timeout) {
withTimeout(operation, yield[errorCode], *timeout);
errorCode = util::withTimeout(operation, yield[errorCode], *timeout);
} else {
operation(yield[errorCode]);
}
if (errorCode) {
errorCode = mapError(errorCode);
if (errorCode)
return std::unexpected{RequestError{"Read error", errorCode}};
}
return boost::beast::buffers_to_string(std::move(buffer).data());
}
@@ -90,15 +89,13 @@ public:
boost::beast::error_code errorCode;
auto operation = [&](auto&& token) { ws_.async_write(boost::asio::buffer(message), token); };
if (timeout) {
withTimeout(operation, yield[errorCode], *timeout);
errorCode = util::withTimeout(operation, yield, *timeout);
} else {
operation(yield[errorCode]);
}
if (errorCode) {
errorCode = mapError(errorCode);
if (errorCode)
return RequestError{"Write error", errorCode};
}
return std::nullopt;
}
@@ -119,36 +116,6 @@ public:
return RequestError{"Close error", errorCode};
return std::nullopt;
}
private:
template <typename Operation>
static void
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
auto isCompleted = std::make_shared<bool>(false);
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield);
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
// The timer below can be called with no error code even if the operation is completed before the timeout, so we
// need an additional flag here
timer.async_wait([&cancellationSignal, isCompleted](boost::system::error_code errorCode) {
if (!errorCode and !*isCompleted)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
*isCompleted = true;
}
static boost::system::error_code
mapError(boost::system::error_code const ec)
{
if (ec == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return ec;
}
};
using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>;

View File

@@ -3,13 +3,17 @@ add_library(clio_web)
target_sources(
clio_web
PRIVATE Resolver.cpp
Server.cpp
dosguard/DOSGuard.cpp
dosguard/IntervalSweepHandler.cpp
dosguard/WhitelistHandler.cpp
impl/AdminVerificationStrategy.cpp
impl/ServerSslContext.cpp
ng/Connection.cpp
ng/impl/ConnectionHandler.cpp
ng/impl/ServerSslContext.cpp
ng/impl/WsConnection.cpp
ng/Server.cpp
ng/Request.cpp
ng/Response.cpp
)
target_link_libraries(clio_web PUBLIC clio_util)

View File

@@ -24,8 +24,8 @@
#include "web/HttpSession.hpp"
#include "web/SslHttpSession.hpp"
#include "web/dosguard/DOSGuardInterface.hpp"
#include "web/impl/ServerSslContext.hpp"
#include "web/interface/Concepts.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp>
@@ -59,15 +59,6 @@
*/
namespace web {
/**
* @brief A helper function to create a server SSL context.
*
* @param config The config to create the context
* @return Optional SSL context or error message if any
*/
std::expected<std::optional<boost::asio::ssl::context>, std::string>
makeServerSslContext(util::Config const& config);
/**
* @brief The Detector class to detect if the connection is a ssl or not.
*
@@ -329,7 +320,7 @@ make_HttpServer(
{
static util::Logger const log{"WebServer"};
auto expectedSslContext = makeServerSslContext(config);
auto expectedSslContext = ng::impl::makeServerSslContext(config);
if (not expectedSslContext) {
LOG(log.error()) << "Failed to create SSL context: " << expectedSslContext.error();
return nullptr;

View File

@@ -20,6 +20,7 @@
#include "web/impl/AdminVerificationStrategy.hpp"
#include "util/JsonUtils.hpp"
#include "util/config/Config.hpp"
#include <boost/beast/http/field.hpp>
#include <xrpl/basics/base_uint.h>
@@ -79,4 +80,20 @@ make_AdminVerificationStrategy(std::optional<std::string> password)
return std::make_shared<IPAdminVerificationStrategy>();
}
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
make_AdminVerificationStrategy(util::Config const& serverConfig)
{
auto adminPassword = serverConfig.maybeValue<std::string>("admin_password");
auto const localAdmin = serverConfig.maybeValue<bool>("local_admin");
bool const localAdminEnabled = localAdmin && localAdmin.value();
if (localAdminEnabled == adminPassword.has_value()) {
if (adminPassword.has_value())
return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."};
return std::unexpected{"Admin config error, either local_admin and admin_password must be specified."};
}
return make_AdminVerificationStrategy(std::move(adminPassword));
}
} // namespace web::impl

View File

@@ -19,10 +19,13 @@
#pragma once
#include "util/config/Config.hpp"
#include <boost/beast/http.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <expected>
#include <memory>
#include <optional>
#include <string>
@@ -82,4 +85,7 @@ public:
std::shared_ptr<AdminVerificationStrategy>
make_AdminVerificationStrategy(std::optional<std::string> password);
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
make_AdminVerificationStrategy(util::Config const& serverConfig);
} // namespace web::impl

View File

@@ -17,32 +17,41 @@
*/
//==============================================================================
#include "web/impl/ServerSslContext.hpp"
#include "web/ng/Connection.hpp"
#include <gtest/gtest.h>
#include "util/Taggable.hpp"
using namespace web::impl;
#include <boost/beast/core/flat_buffer.hpp>
TEST(ServerSslContext, makeServerSslContext)
#include <cstddef>
#include <string>
#include <utility>
namespace web::ng {
Connection::Connection(
std::string ip,
boost::beast::flat_buffer buffer,
util::TagDecoratorFactory const& tagDecoratorFactory
)
: util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}, buffer_{std::move(buffer)}
{
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, TEST_DATA_SSL_KEY_PATH);
ASSERT_TRUE(sslContext);
}
TEST(ServerSslContext, makeServerSslContext_WrongCertPath)
ConnectionContext
Connection::context() const
{
auto const sslContext = makeServerSslContext("wrong_path", TEST_DATA_SSL_KEY_PATH);
ASSERT_FALSE(sslContext);
return ConnectionContext{*this};
}
TEST(ServerSslContext, makeServerSslContext_WrongKeyPath)
std::string const&
Connection::ip() const
{
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, "wrong_path");
ASSERT_FALSE(sslContext);
return ip_;
}
TEST(ServerSslContext, makeServerSslContext_CertKeyMismatch)
ConnectionContext::ConnectionContext(Connection const& connection) : connection_{connection}
{
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_KEY_PATH, TEST_DATA_SSL_CERT_PATH);
ASSERT_FALSE(sslContext);
}
} // namespace web::ng

148
src/web/ng/Connection.hpp Normal file
View File

@@ -0,0 +1,148 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/Taggable.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <chrono>
#include <cstddef>
#include <expected>
#include <functional>
#include <memory>
#include <optional>
#include <string>
namespace web::ng {
/**
* @brief A forward declaration of ConnectionContext.
*/
class ConnectionContext;
/**
*@brief A class representing a connection to a client.
*/
class Connection : public util::Taggable {
protected:
std::string ip_; // client ip
boost::beast::flat_buffer buffer_;
public:
/**
* @brief The default timeout for send, receive, and close operations.
*/
static constexpr std::chrono::steady_clock::duration DEFAULT_TIMEOUT = std::chrono::seconds{30};
/**
* @brief Construct a new Connection object
*
* @param ip The client ip.
* @param buffer The buffer to use for reading and writing.
* @param tagDecoratorFactory The factory for creating tag decorators.
*/
Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory);
/**
* @brief Whether the connection was upgraded. Upgraded connections are websocket connections.
*
* @return true if the connection was upgraded.
*/
virtual bool
wasUpgraded() const = 0;
/**
* @brief Send a response to the client.
*
* @param response The response to send.
* @param yield The yield context.
* @param timeout The timeout for the operation.
* @return An error if the operation failed or nullopt if it succeeded.
*/
virtual std::optional<Error>
send(
Response response,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
) = 0;
/**
* @brief Receive a request from the client.
*
* @param yield The yield context.
* @param timeout The timeout for the operation.
* @return The request if it was received or an error if the operation failed.
*/
virtual std::expected<Request, Error>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0;
/**
* @brief Gracefully close the connection.
*
* @param yield The yield context.
* @param timeout The timeout for the operation.
*/
virtual void
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0;
/**
* @brief Get the connection context.
*
* @return The connection context.
*/
ConnectionContext
context() const;
/**
* @brief Get the ip of the client.
*
* @return The ip of the client.
*/
std::string const&
ip() const;
};
/**
* @brief A pointer to a connection.
*/
using ConnectionPtr = std::unique_ptr<Connection>;
/**
* @brief A class representing the context of a connection.
*/
class ConnectionContext {
std::reference_wrapper<Connection const> connection_;
public:
/**
* @brief Construct a new ConnectionContext object.
*
* @param connection The connection.
*/
explicit ConnectionContext(Connection const& connection);
};
} // namespace web::ng

31
src/web/ng/Error.hpp Normal file
View File

@@ -0,0 +1,31 @@
//------------------------------------------------------------------------------
/*
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/system/detail/error_code.hpp>
namespace web::ng {
/**
* @brief Error of any async operation.
*/
using Error = boost::system::error_code;
} // namespace web::ng

View File

@@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
/*
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 "web/ng/Connection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <functional>
namespace web::ng {
/**
* @brief Handler for messages.
*/
using MessageHandler = std::function<Response(Request const&, ConnectionContext, boost::asio::yield_context)>;
} // namespace web::ng

131
src/web/ng/Request.cpp Normal file
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.
*/
//==============================================================================
#include "web/ng/Request.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 <functional>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
namespace web::ng {
namespace {
template <typename HeadersType, typename HeaderNameType>
std::optional<std::string_view>
getHeaderValue(HeadersType const& headers, HeaderNameType const& headerName)
{
auto const it = headers.find(headerName);
if (it == headers.end())
return std::nullopt;
return it->value();
}
} // namespace
Request::Request(boost::beast::http::request<boost::beast::http::string_body> request) : data_{std::move(request)}
{
}
Request::Request(std::string request, HttpHeaders const& headers)
: data_{WsData{.request = std::move(request), .headers = headers}}
{
}
Request::Method
Request::method() const
{
if (not isHttp())
return Method::Websocket;
switch (httpRequest().method()) {
case boost::beast::http::verb::get:
return Method::Get;
case boost::beast::http::verb::post:
return Method::Post;
default:
return Method::Unsupported;
}
}
bool
Request::isHttp() const
{
return std::holds_alternative<HttpRequest>(data_);
}
std::optional<std::reference_wrapper<boost::beast::http::request<boost::beast::http::string_body> const>>
Request::asHttpRequest() const
{
if (not isHttp())
return std::nullopt;
return httpRequest();
}
std::string_view
Request::message() const
{
if (not isHttp())
return std::get<WsData>(data_).request;
return httpRequest().body();
}
std::optional<std::string_view>
Request::target() const
{
if (not isHttp())
return std::nullopt;
return httpRequest().target();
}
std::optional<std::string_view>
Request::headerValue(boost::beast::http::field headerName) const
{
if (not isHttp())
return getHeaderValue(std::get<WsData>(data_).headers.get(), headerName);
return getHeaderValue(httpRequest(), headerName);
}
std::optional<std::string_view>
Request::headerValue(std::string const& headerName) const
{
if (not isHttp())
return getHeaderValue(std::get<WsData>(data_).headers.get(), headerName);
return getHeaderValue(httpRequest(), headerName);
}
Request::HttpRequest const&
Request::httpRequest() const
{
return std::get<HttpRequest>(data_);
}
} // namespace web::ng

145
src/web/ng/Request.hpp Normal file
View File

@@ -0,0 +1,145 @@
//------------------------------------------------------------------------------
/*
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/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace web::ng {
/**
* @brief Represents an HTTP or WebSocket request.
*/
class Request {
public:
/**
* @brief The headers of an HTTP request.
*/
using HttpHeaders = boost::beast::http::request<boost::beast::http::string_body>::header_type;
private:
struct WsData {
std::string request;
std::reference_wrapper<HttpHeaders const> headers;
};
using HttpRequest = boost::beast::http::request<boost::beast::http::string_body>;
std::variant<HttpRequest, WsData> data_;
public:
/**
* @brief Construct from an HTTP request.
*
* @param request The HTTP request.
*/
explicit Request(boost::beast::http::request<boost::beast::http::string_body> request);
/**
* @brief Construct from a WebSocket request.
*
* @param request The WebSocket request.
* @param headers The headers of the HTTP request initiated the WebSocket connection
*/
Request(std::string request, HttpHeaders const& headers);
/**
* @brief Method of the request.
* @note Websocket is not a real method, it is used to distinguish WebSocket requests from HTTP requests.
*/
enum class Method { Get, Post, Websocket, Unsupported };
/**
* @brief Get the method of the request.
*
* @return The method of the request.
*/
Method
method() const;
/**
* @brief Check if the request is an HTTP request.
*
* @return true if the request is an HTTP request, false otherwise.
*/
bool
isHttp() const;
/**
* @brief Get the HTTP request.
*
* @return The HTTP request or std::nullopt if the request is a WebSocket request.
*/
std::optional<std::reference_wrapper<boost::beast::http::request<boost::beast::http::string_body> const>>
asHttpRequest() const;
/**
* @brief Get the body (in case of an HTTP request) or the message (in case of a WebSocket request).
*
* @return The message of the request.
*/
std::string_view
message() const;
/**
* @brief Get the target of the request.
*
* @return The target of the request or std::nullopt if the request is a WebSocket request.
*/
std::optional<std::string_view>
target() const;
/**
* @brief Get the value of a header.
*
* @param headerName The name of the header.
* @return The value of the header or std::nullopt if the header does not exist.
*/
std::optional<std::string_view>
headerValue(boost::beast::http::field headerName) const;
/**
* @brief Get the value of a header.
*
* @param headerName The name of the header.
* @return The value of the header or std::nullopt if the header does not exist.
*/
std::optional<std::string_view>
headerValue(std::string const& headerName) const;
private:
/**
* @brief Get the HTTP request.
* @note This function assumes that the request is an HTTP request. So if data_ is not an HTTP request,
* the behavior is undefined.
*
* @return The HTTP request.
*/
HttpRequest const&
httpRequest() const;
};
} // namespace web::ng

116
src/web/ng/Response.cpp Normal file
View File

@@ -0,0 +1,116 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/ng/Response.hpp"
#include "util/Assert.hpp"
#include "util/build/Build.hpp"
#include "web/ng/Request.hpp"
#include <boost/asio/buffer.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/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <fmt/core.h>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
namespace http = boost::beast::http;
namespace web::ng {
namespace {
std::string_view
asString(Response::HttpData::ContentType type)
{
switch (type) {
case Response::HttpData::ContentType::TextHtml:
return "text/html";
case Response::HttpData::ContentType::ApplicationJson:
return "application/json";
}
ASSERT(false, "Unknown content type");
std::unreachable();
}
template <typename MessageType>
std::optional<Response::HttpData>
makeHttpData(http::status status, Request const& request)
{
if (request.isHttp()) {
auto const& httpRequest = request.asHttpRequest()->get();
auto constexpr contentType = std::is_same_v<std::remove_cvref_t<MessageType>, std::string>
? Response::HttpData::ContentType::TextHtml
: Response::HttpData::ContentType::ApplicationJson;
return Response::HttpData{
.status = status,
.contentType = contentType,
.keepAlive = httpRequest.keep_alive(),
.version = httpRequest.version()
};
}
return std::nullopt;
}
} // namespace
Response::Response(boost::beast::http::status status, std::string message, Request const& request)
: message_(std::move(message)), httpData_{makeHttpData<decltype(message)>(status, request)}
{
}
Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request)
: message_(boost::json::serialize(message)), httpData_{makeHttpData<decltype(message)>(status, request)}
{
}
std::string const&
Response::message() const
{
return message_;
}
http::response<http::string_body>
Response::intoHttpResponse() &&
{
ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response");
http::response<http::string_body> result{httpData_->status, httpData_->version};
result.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString()));
result.set(http::field::content_type, asString(httpData_->contentType));
result.keep_alive(httpData_->keepAlive);
result.body() = std::move(message_);
result.prepare_payload();
return result;
}
boost::asio::const_buffer
Response::asConstBuffer() const&
{
ASSERT(not httpData_.has_value(), "Losing existing http data");
return boost::asio::buffer(message_.data(), message_.size());
}
} // namespace web::ng

106
src/web/ng/Response.hpp Normal file
View File

@@ -0,0 +1,106 @@
//------------------------------------------------------------------------------
/*
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 "web/ng/Request.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/json/object.hpp>
#include <optional>
#include <string>
namespace web::ng {
/**
* @brief Represents an HTTP or Websocket response.
*/
class Response {
public:
/**
* @brief The data for an HTTP response.
*/
struct HttpData {
/**
* @brief The content type of the response.
*/
enum class ContentType { ApplicationJson, TextHtml };
boost::beast::http::status status; ///< The HTTP status.
ContentType contentType; ///< The content type.
bool keepAlive; ///< Whether the connection should be kept alive.
unsigned int version; ///< The HTTP version.
};
private:
std::string message_;
std::optional<HttpData> httpData_;
public:
/**
* @brief Construct a Response from string. Content type will be text/html.
*
* @param status The HTTP status.
* @param message The message to send.
* @param request The request that triggered this response. Used to determine whether the response should contain
* HTTP or WebSocket data.
*/
Response(boost::beast::http::status status, std::string message, Request const& request);
/**
* @brief Construct a Response from JSON object. Content type will be application/json.
*
* @param status The HTTP status.
* @param message The message to send.
* @param request The request that triggered this response. Used to determine whether the response should contain
* HTTP or WebSocket
*/
Response(boost::beast::http::status status, boost::json::object const& message, Request const& request);
/**
* @brief Get the message of the response.
*
* @return The message of the response.
*/
std::string const&
message() const;
/**
* @brief Convert the Response to an HTTP response.
* @note The Response must be constructed with an HTTP request.
*
* @return The HTTP response.
*/
boost::beast::http::response<boost::beast::http::string_body>
intoHttpResponse() &&;
/**
* @brief Get the message of the response as a const buffer.
* @note The response must be constructed with a WebSocket request.
*
* @return The message of the response as a const buffer.
*/
boost::asio::const_buffer
asConstBuffer() const&;
};
} // namespace web::ng

View File

@@ -0,0 +1,322 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/ng/Server.hpp"
#include "util/Assert.hpp"
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "util/log/Logger.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/MessageHandler.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include "web/ng/impl/ServerSslContext.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/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/beast/core/detect_ssl.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/system/system_error.hpp>
#include <fmt/compile.h>
#include <fmt/core.h>
#include <cstddef>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <utility>
namespace web::ng {
namespace {
std::expected<boost::asio::ip::tcp::endpoint, std::string>
makeEndpoint(util::Config const& serverConfig)
{
auto const ip = serverConfig.maybeValue<std::string>("ip");
if (not ip.has_value())
return std::unexpected{"Missing 'ip` in server config."};
boost::system::error_code error;
auto const address = boost::asio::ip::make_address(*ip, error);
if (error)
return std::unexpected{fmt::format("Error parsing provided IP: {}", error.message())};
auto const port = serverConfig.maybeValue<unsigned short>("port");
if (not port.has_value())
return std::unexpected{"Missing 'port` in server config."};
return boost::asio::ip::tcp::endpoint{address, *port};
}
std::expected<boost::asio::ip::tcp::acceptor, std::string>
makeAcceptor(boost::asio::io_context& context, boost::asio::ip::tcp::endpoint const& endpoint)
{
boost::asio::ip::tcp::acceptor acceptor{context};
try {
acceptor.open(endpoint.protocol());
acceptor.set_option(boost::asio::socket_base::reuse_address(true));
acceptor.bind(endpoint);
acceptor.listen(boost::asio::socket_base::max_listen_connections);
} catch (boost::system::system_error const& error) {
return std::unexpected{fmt::format("Error creating TCP acceptor: {}", error.what())};
}
return acceptor;
}
std::expected<std::string, boost::system::system_error>
extractIp(boost::asio::ip::tcp::socket const& socket)
{
std::string ip;
try {
ip = socket.remote_endpoint().address().to_string();
} catch (boost::system::system_error const& error) {
return std::unexpected{error};
}
return ip;
}
struct SslDetectionResult {
boost::asio::ip::tcp::socket socket;
bool isSsl;
boost::beast::flat_buffer buffer;
};
std::expected<std::optional<SslDetectionResult>, std::string>
detectSsl(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield)
{
boost::beast::tcp_stream tcpStream{std::move(socket)};
boost::beast::flat_buffer buffer;
boost::beast::error_code errorCode;
bool const isSsl = boost::beast::async_detect_ssl(tcpStream, buffer, yield[errorCode]);
if (errorCode == boost::asio::ssl::error::stream_truncated)
return std::nullopt;
if (errorCode)
return std::unexpected{fmt::format("Detector failed (detect): {}", errorCode.message())};
return SslDetectionResult{.socket = tcpStream.release_socket(), .isSsl = isSsl, .buffer = std::move(buffer)};
}
std::expected<ConnectionPtr, std::string>
makeConnection(
SslDetectionResult sslDetectionResult,
std::optional<boost::asio::ssl::context>& sslContext,
std::string ip,
util::TagDecoratorFactory& tagDecoratorFactory,
boost::asio::yield_context yield
)
{
impl::UpgradableConnectionPtr connection;
if (sslDetectionResult.isSsl) {
if (not sslContext.has_value())
return std::unexpected{"SSL is not supported by this server"};
connection = std::make_unique<impl::SslHttpConnection>(
std::move(sslDetectionResult.socket),
std::move(ip),
std::move(sslDetectionResult.buffer),
*sslContext,
tagDecoratorFactory
);
} else {
connection = std::make_unique<impl::PlainHttpConnection>(
std::move(sslDetectionResult.socket),
std::move(ip),
std::move(sslDetectionResult.buffer),
tagDecoratorFactory
);
}
auto const expectedIsUpgrade = connection->isUpgradeRequested(yield);
if (not expectedIsUpgrade.has_value()) {
return std::unexpected{
fmt::format("Error checking whether upgrade requested: {}", expectedIsUpgrade.error().message())
};
}
if (*expectedIsUpgrade) {
auto expectedUpgradedConnection = connection->upgrade(sslContext, tagDecoratorFactory, yield);
if (expectedUpgradedConnection.has_value())
return std::move(expectedUpgradedConnection).value();
return std::unexpected{fmt::format("Error upgrading connection: {}", expectedUpgradedConnection.error().what())
};
}
return connection;
}
} // namespace
Server::Server(
boost::asio::io_context& ctx,
boost::asio::ip::tcp::endpoint endpoint,
std::optional<boost::asio::ssl::context> sslContext,
impl::ConnectionHandler connectionHandler,
util::TagDecoratorFactory tagDecoratorFactory
)
: ctx_{ctx}
, sslContext_{std::move(sslContext)}
, connectionHandler_{std::move(connectionHandler)}
, endpoint_{std::move(endpoint)}
, tagDecoratorFactory_{tagDecoratorFactory}
{
}
void
Server::onGet(std::string const& target, MessageHandler handler)
{
ASSERT(not running_, "Adding a GET handler is not allowed when Server is running.");
connectionHandler_.onGet(target, std::move(handler));
}
void
Server::onPost(std::string const& target, MessageHandler handler)
{
ASSERT(not running_, "Adding a POST handler is not allowed when Server is running.");
connectionHandler_.onPost(target, std::move(handler));
}
void
Server::onWs(MessageHandler handler)
{
ASSERT(not running_, "Adding a Websocket handler is not allowed when Server is running.");
connectionHandler_.onWs(std::move(handler));
}
std::optional<std::string>
Server::run()
{
auto acceptor = makeAcceptor(ctx_.get(), endpoint_);
if (not acceptor.has_value())
return std::move(acceptor).error();
running_ = true;
boost::asio::spawn(
ctx_.get(),
[this, acceptor = std::move(acceptor).value()](boost::asio::yield_context yield) mutable {
while (true) {
boost::beast::error_code errorCode;
boost::asio::ip::tcp::socket socket{ctx_.get().get_executor()};
acceptor.async_accept(socket, yield[errorCode]);
if (errorCode) {
LOG(log_.debug()) << "Error accepting a connection: " << errorCode.what();
continue;
}
boost::asio::spawn(
ctx_.get(),
[this, socket = std::move(socket)](boost::asio::yield_context yield) mutable {
handleConnection(std::move(socket), yield);
},
boost::asio::detached
);
}
}
);
return std::nullopt;
}
void
Server::stop()
{
}
void
Server::handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield)
{
auto sslDetectionResultExpected = detectSsl(std::move(socket), yield);
if (not sslDetectionResultExpected) {
LOG(log_.info()) << sslDetectionResultExpected.error();
return;
}
auto sslDetectionResult = std::move(sslDetectionResultExpected).value();
if (not sslDetectionResult)
return; // stream truncated, probably user disconnected
auto ip = extractIp(sslDetectionResult->socket);
if (not ip.has_value()) {
LOG(log_.info()) << "Cannot get remote endpoint: " << ip.error().what();
return;
}
// TODO(kuznetsss): check ip with dosguard here
auto connectionExpected = makeConnection(
std::move(sslDetectionResult).value(), sslContext_, std::move(ip).value(), tagDecoratorFactory_, yield
);
if (not connectionExpected.has_value()) {
LOG(log_.info()) << "Error creating a connection: " << connectionExpected.error();
return;
}
boost::asio::spawn(
ctx_.get(),
[this, connection = std::move(connectionExpected).value()](boost::asio::yield_context yield) mutable {
connectionHandler_.processConnection(std::move(connection), yield);
}
);
}
std::expected<Server, std::string>
make_Server(util::Config const& config, boost::asio::io_context& context)
{
auto const serverConfig = config.section("server");
auto endpoint = makeEndpoint(serverConfig);
if (not endpoint.has_value())
return std::unexpected{std::move(endpoint).error()};
auto expectedSslContext = impl::makeServerSslContext(config);
if (not expectedSslContext)
return std::unexpected{std::move(expectedSslContext).error()};
impl::ConnectionHandler::ProcessingPolicy processingPolicy{impl::ConnectionHandler::ProcessingPolicy::Parallel};
std::optional<size_t> parallelRequestLimit;
auto const processingStrategyStr = serverConfig.valueOr<std::string>("processing_policy", "parallel");
if (processingStrategyStr == "sequent") {
processingPolicy = impl::ConnectionHandler::ProcessingPolicy::Sequential;
} else if (processingStrategyStr == "parallel") {
parallelRequestLimit = serverConfig.maybeValue<size_t>("parallel_requests_limit");
} else {
return std::unexpected{fmt::format("Invalid 'server.processing_strategy': {}", processingStrategyStr)};
}
return Server{
context,
std::move(endpoint).value(),
std::move(expectedSslContext).value(),
impl::ConnectionHandler{processingPolicy, parallelRequestLimit},
util::TagDecoratorFactory(config)
};
}
} // namespace web::ng

View File

@@ -0,0 +1,147 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "util/log/Logger.hpp"
#include "web/impl/AdminVerificationStrategy.hpp"
#include "web/ng/MessageHandler.hpp"
#include "web/ng/impl/ConnectionHandler.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <cstddef>
#include <functional>
#include <optional>
#include <string>
namespace web::ng {
/**
* @brief Web server class.
*/
class Server {
util::Logger log_{"WebServer"};
util::Logger perfLog_{"Performance"};
std::reference_wrapper<boost::asio::io_context> ctx_;
std::optional<boost::asio::ssl::context> sslContext_;
impl::ConnectionHandler connectionHandler_;
boost::asio::ip::tcp::endpoint endpoint_;
util::TagDecoratorFactory tagDecoratorFactory_;
bool running_{false};
public:
/**
* @brief Construct a new Server object.
*
* @param ctx The boost::asio::io_context to use.
* @param endpoint The endpoint to listen on.
* @param sslContext The SSL context to use (optional).
* @param connectionHandler The connection handler.
* @param tagDecoratorFactory The tag decorator factory.
*/
Server(
boost::asio::io_context& ctx,
boost::asio::ip::tcp::endpoint endpoint,
std::optional<boost::asio::ssl::context> sslContext,
impl::ConnectionHandler connectionHandler,
util::TagDecoratorFactory tagDecoratorFactory
);
/**
* @brief Copy constructor is deleted. The Server couldn't be copied.
*/
Server(Server const&) = delete;
/**
* @brief Move constructor is defaulted.
*/
Server(Server&&) = default;
/**
* @brief Set handler for GET requests.
* @note This method can't be called after run() is called.
*
* @param target The target of the request.
* @param handler The handler to set.
*/
void
onGet(std::string const& target, MessageHandler handler);
/**
* @brief Set handler for POST requests.
* @note This method can't be called after run() is called.
*
* @param target The target of the request.
* @param handler The handler to set.
*/
void
onPost(std::string const& target, MessageHandler handler);
/**
* @brief Set handler for WebSocket requests.
* @note This method can't be called after run() is called.
*
* @param handler The handler to set.
*/
void
onWs(MessageHandler handler);
/**
* @brief Run the server.
*
* @return std::nullopt if the server started successfully, otherwise an error message.
*/
std::optional<std::string>
run();
/**
* @brief Stop the server.
** @note Stopping the server cause graceful shutdown of all connections. And rejecting new connections.
*/
void
stop();
private:
void
handleConnection(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield);
};
/**
* @brief Create a new Server.
*
* @param config The configuration.
* @param context The boost::asio::io_context to use.
*
* @return The Server or an error message.
*/
std::expected<Server, std::string>
make_Server(util::Config const& config, boost::asio::io_context& context);
} // namespace web::ng

View File

@@ -0,0 +1,35 @@
//------------------------------------------------------------------------------
/*
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/basic_stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <type_traits>
namespace web::ng::impl {
template <typename T>
concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
template <typename T>
concept IsSslTcpStream = std::is_same_v<std::decay_t<T>, boost::asio::ssl::stream<boost::beast::tcp_stream>>;
} // namespace web::ng::impl

View File

@@ -0,0 +1,285 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/ng/impl/ConnectionHandler.hpp"
#include "util/Assert.hpp"
#include "util/CoroutineGroup.hpp"
#include "util/log/Logger.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/MessageHandler.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/websocket/error.hpp>
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
namespace web::ng::impl {
namespace {
Response
handleHttpRequest(
ConnectionContext const& connectionContext,
ConnectionHandler::TargetToHandlerMap const& handlers,
Request const& request,
boost::asio::yield_context yield
)
{
ASSERT(request.target().has_value(), "Got not a HTTP request");
auto it = handlers.find(*request.target());
if (it == handlers.end()) {
return Response{boost::beast::http::status::bad_request, "Bad target", request};
}
return it->second(request, connectionContext, yield);
}
Response
handleWsRequest(
ConnectionContext connectionContext,
std::optional<MessageHandler> const& handler,
Request const& request,
boost::asio::yield_context yield
)
{
if (not handler.has_value()) {
return Response{boost::beast::http::status::bad_request, "WebSocket is not supported by this server", request};
}
return handler->operator()(request, connectionContext, yield);
}
} // namespace
size_t
ConnectionHandler::StringHash::operator()(char const* str) const
{
return hash_type{}(str);
}
size_t
ConnectionHandler::StringHash::operator()(std::string_view str) const
{
return hash_type{}(str);
}
size_t
ConnectionHandler::StringHash::operator()(std::string const& str) const
{
return hash_type{}(str);
}
ConnectionHandler::ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests)
: processingPolicy_{processingPolicy}, maxParallelRequests_{maxParallelRequests}
{
}
void
ConnectionHandler::onGet(std::string const& target, MessageHandler handler)
{
getHandlers_[target] = std::move(handler);
}
void
ConnectionHandler::onPost(std::string const& target, MessageHandler handler)
{
postHandlers_[target] = std::move(handler);
}
void
ConnectionHandler::onWs(MessageHandler handler)
{
wsHandler_ = std::move(handler);
}
void
ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::yield_context yield)
{
auto& connectionRef = *connectionPtr;
auto signalConnection = onStop_.connect([&connectionRef, yield]() { connectionRef.close(yield); });
bool shouldCloseGracefully = false;
switch (processingPolicy_) {
case ProcessingPolicy::Sequential:
shouldCloseGracefully = sequentRequestResponseLoop(connectionRef, yield);
break;
case ProcessingPolicy::Parallel:
shouldCloseGracefully = parallelRequestResponseLoop(connectionRef, yield);
break;
}
if (shouldCloseGracefully)
connectionRef.close(yield);
signalConnection.disconnect();
}
void
ConnectionHandler::stop()
{
onStop_();
}
bool
ConnectionHandler::handleError(Error const& error, Connection const& connection) const
{
// ssl::error::stream_truncated, also known as an SSL "short read",
// indicates the peer closed the connection without performing the
// required closing handshake (for example, Google does this to
// improve performance). Generally this can be a security issue,
// but if your communication protocol is self-terminated (as
// it is with both HTTP and WebSocket) then you may simply
// ignore the lack of close_notify.
//
// https://github.com/boostorg/beast/issues/38
//
// https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
//
// When a short read would cut off the end of an HTTP message,
// Beast returns the error boost::beast::http::error::partial_message.
// Therefore, if we see a short read here, it has occurred
// after the message has been completed, so it is safe to ignore it.
if (error == boost::beast::http::error::end_of_stream || error == boost::asio::ssl::error::stream_truncated)
return false;
// WebSocket connection was gracefully closed
if (error == boost::beast::websocket::error::closed)
return false;
if (error != boost::asio::error::operation_aborted) {
LOG(log_.error()) << connection.tag() << ": " << error.message() << ": " << error.value();
}
return true;
}
bool
ConnectionHandler::sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield)
{
// The loop here is infinite because:
// - For websocket connection is persistent so Clio will try to read and respond infinite unless client
// disconnected.
// - When client disconnected connection.send() or connection.receive() will return an error.
// - For http it is still a loop to reuse the connection if keep alive is set. Otherwise client will disconnect and
// an error appears.
// - When server is shutting down it will cancel all operations on the connection so an error appears.
while (true) {
auto expectedRequest = connection.receive(yield);
if (not expectedRequest)
return handleError(expectedRequest.error(), connection);
LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip();
auto maybeReturnValue = processRequest(connection, std::move(expectedRequest).value(), yield);
if (maybeReturnValue.has_value())
return maybeReturnValue.value();
}
}
bool
ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield)
{
// atomic_bool is not needed here because everything happening on coroutine's strand
bool stop = false;
bool closeConnectionGracefully = true;
util::CoroutineGroup tasksGroup{yield, maxParallelRequests_};
while (not stop) {
auto expectedRequest = connection.receive(yield);
if (not expectedRequest) {
auto const closeGracefully = handleError(expectedRequest.error(), connection);
stop = true;
closeConnectionGracefully &= closeGracefully;
break;
}
bool const spawnSuccess = tasksGroup.spawn(
yield, // spawn on the same strand
[this, &stop, &closeConnectionGracefully, &connection, request = std::move(expectedRequest).value()](
boost::asio::yield_context innerYield
) mutable {
auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield);
if (maybeCloseConnectionGracefully.has_value()) {
stop = true;
closeConnectionGracefully &= maybeCloseConnectionGracefully.value();
}
}
);
if (not spawnSuccess) {
connection.send(
Response{
boost::beast::http::status::too_many_requests,
"Too many requests for one connection",
expectedRequest.value()
},
yield
);
}
}
tasksGroup.asyncWait(yield);
return closeConnectionGracefully;
}
std::optional<bool>
ConnectionHandler::processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield)
{
auto response = handleRequest(connection.context(), request, yield);
auto const maybeError = connection.send(std::move(response), yield);
if (maybeError.has_value()) {
return handleError(maybeError.value(), connection);
}
return std::nullopt;
}
Response
ConnectionHandler::handleRequest(
ConnectionContext const& connectionContext,
Request const& request,
boost::asio::yield_context yield
)
{
switch (request.method()) {
case Request::Method::Get:
return handleHttpRequest(connectionContext, getHandlers_, request, yield);
case Request::Method::Post:
return handleHttpRequest(connectionContext, postHandlers_, request, yield);
case Request::Method::Websocket:
return handleWsRequest(connectionContext, wsHandler_, request, yield);
default:
return Response{boost::beast::http::status::bad_request, "Unsupported http method", request};
}
}
} // namespace web::ng::impl

View File

@@ -0,0 +1,130 @@
//------------------------------------------------------------------------------
/*
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/log/Logger.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/MessageHandler.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <cstddef>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
namespace web::ng::impl {
class ConnectionHandler {
public:
enum class ProcessingPolicy { Sequential, Parallel };
struct StringHash {
using hash_type = std::hash<std::string_view>;
using is_transparent = void;
std::size_t
operator()(char const* str) const;
std::size_t
operator()(std::string_view str) const;
std::size_t
operator()(std::string const& str) const;
};
using TargetToHandlerMap = std::unordered_map<std::string, MessageHandler, StringHash, std::equal_to<>>;
private:
util::Logger log_{"WebServer"};
util::Logger perfLog_{"Performance"};
ProcessingPolicy processingPolicy_;
std::optional<size_t> maxParallelRequests_;
TargetToHandlerMap getHandlers_;
TargetToHandlerMap postHandlers_;
std::optional<MessageHandler> wsHandler_;
boost::signals2::signal<void()> onStop_;
public:
ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests);
void
onGet(std::string const& target, MessageHandler handler);
void
onPost(std::string const& target, MessageHandler handler);
void
onWs(MessageHandler handler);
void
processConnection(ConnectionPtr connection, boost::asio::yield_context yield);
void
stop();
private:
/**
* @brief Handle an error.
*
* @param error The error to handle.
* @param connection The connection that caused the error.
* @return True if the connection should be gracefully closed, false otherwise.
*/
bool
handleError(Error const& error, Connection const& connection) const;
/**
* @brief The request-response loop.
*
* @param connection The connection to handle.
* @param yield The yield context.
* @return True if the connection should be gracefully closed, false otherwise.
*/
bool
sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
bool
parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
std::optional<bool>
processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield);
/**
* @brief Handle a request.
*
* @param connectionContext The connection context.
* @param request The request to handle.
* @param yield The yield context.
* @return The response to send.
*/
Response
handleRequest(ConnectionContext const& connectionContext, Request const& request, boost::asio::yield_context yield);
};
} // namespace web::ng::impl

View File

@@ -0,0 +1,219 @@
//------------------------------------------------------------------------------
/*
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/Assert.hpp"
#include "util/Taggable.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/Concepts.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core/basic_stream.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.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/websocket.hpp>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <utility>
namespace web::ng::impl {
class UpgradableConnection : public Connection {
public:
using Connection::Connection;
virtual std::expected<bool, Error>
isUpgradeRequested(
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
) = 0;
virtual std::expected<ConnectionPtr, Error>
upgrade(
std::optional<boost::asio::ssl::context>& sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
) = 0;
};
using UpgradableConnectionPtr = std::unique_ptr<UpgradableConnection>;
template <typename StreamType>
class HttpConnection : public UpgradableConnection {
StreamType stream_;
std::optional<boost::beast::http::request<boost::beast::http::string_body>> request_;
public:
HttpConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
util::TagDecoratorFactory const& tagDecoratorFactory
)
requires IsTcpStream<StreamType>
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory), stream_{std::move(socket)}
{
}
HttpConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::asio::ssl::context& sslCtx,
util::TagDecoratorFactory const& tagDecoratorFactory
)
requires IsSslTcpStream<StreamType>
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
, stream_{std::move(socket), sslCtx}
{
}
bool
wasUpgraded() const override
{
return false;
}
std::optional<Error>
send(
Response response,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
) override
{
auto const httpResponse = std::move(response).intoHttpResponse();
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
boost::beast::http::async_write(stream_, httpResponse, yield[error]);
if (error)
return error;
return std::nullopt;
}
std::expected<Request, Error>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
{
if (request_.has_value()) {
Request result{std::move(request_).value()};
request_.reset();
return result;
}
auto expectedRequest = fetch(yield, timeout);
if (expectedRequest.has_value())
return Request{std::move(expectedRequest).value()};
return std::unexpected{std::move(expectedRequest).error()};
}
void
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
{
[[maybe_unused]] boost::system::error_code error;
if constexpr (IsSslTcpStream<StreamType>) {
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.async_shutdown(yield[error]);
}
if constexpr (IsTcpStream<StreamType>) {
stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error);
} else {
boost::beast::get_lowest_layer(stream_).socket().shutdown(
boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error
);
}
}
std::expected<bool, Error>
isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT)
override
{
auto expectedRequest = fetch(yield, timeout);
if (not expectedRequest.has_value())
return std::unexpected{std::move(expectedRequest).error()};
request_ = std::move(expectedRequest).value();
return boost::beast::websocket::is_upgrade(request_.value());
}
std::expected<ConnectionPtr, Error>
upgrade(
[[maybe_unused]] std::optional<boost::asio::ssl::context>& sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
) override
{
ASSERT(request_.has_value(), "Request must be present to upgrade the connection");
if constexpr (IsSslTcpStream<StreamType>) {
ASSERT(sslContext.has_value(), "SSL context must be present to upgrade the connection");
return make_SslWsConnection(
boost::beast::get_lowest_layer(stream_).release_socket(),
std::move(ip_),
std::move(buffer_),
std::move(request_).value(),
sslContext.value(),
tagDecoratorFactory,
yield
);
} else {
return make_PlainWsConnection(
stream_.release_socket(),
std::move(ip_),
std::move(buffer_),
std::move(request_).value(),
tagDecoratorFactory,
yield
);
}
}
private:
std::expected<boost::beast::http::request<boost::beast::http::string_body>, Error>
fetch(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::http::request<boost::beast::http::string_body> request{};
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
boost::beast::http::async_read(stream_, buffer_, request, yield[error]);
if (error)
return std::unexpected{error};
return request;
}
};
using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
using SslHttpConnection = HttpConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
} // namespace web::ng::impl

View File

@@ -17,7 +17,9 @@
*/
//==============================================================================
#include "web/impl/ServerSslContext.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include "util/config/Config.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/ssl/context.hpp>
@@ -31,7 +33,7 @@
#include <string>
#include <utility>
namespace web::impl {
namespace web::ng::impl {
namespace {
@@ -49,32 +51,47 @@ readFile(std::string const& path)
} // namespace
std::expected<boost::asio::ssl::context, std::string>
makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath)
std::expected<std::optional<boost::asio::ssl::context>, std::string>
makeServerSslContext(util::Config const& config)
{
auto const certContent = readFile(certFilePath);
bool const configHasCertFile = config.contains("ssl_cert_file");
bool const configHasKeyFile = config.contains("ssl_key_file");
if (configHasCertFile != configHasKeyFile)
return std::unexpected{"Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together."};
if (not configHasCertFile)
return std::nullopt;
auto const certFilename = config.value<std::string>("ssl_cert_file");
auto const certContent = readFile(certFilename);
if (!certContent)
return std::unexpected{"Can't read SSL certificate: " + certFilePath};
return std::unexpected{"Can't read SSL certificate: " + certFilename};
auto const keyContent = readFile(keyFilePath);
auto const keyFilename = config.value<std::string>("ssl_key_file");
auto const keyContent = readFile(keyFilename);
if (!keyContent)
return std::unexpected{"Can't read SSL key: " + keyFilePath};
return std::unexpected{"Can't read SSL key: " + keyFilename};
return impl::makeServerSslContext(*certContent, *keyContent);
}
std::expected<boost::asio::ssl::context, std::string>
makeServerSslContext(std::string const& certData, std::string const& keyData)
{
using namespace boost::asio;
ssl::context ctx{ssl::context::tls_server};
ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2);
try {
ctx.use_certificate_chain(buffer(certContent->data(), certContent->size()));
ctx.use_private_key(buffer(keyContent->data(), keyContent->size()), ssl::context::file_format::pem);
ctx.use_certificate_chain(buffer(certData.data(), certData.size()));
ctx.use_private_key(buffer(keyData.data(), keyData.size()), ssl::context::file_format::pem);
} catch (...) {
return std::unexpected{
fmt::format("Error loading SSL certificate ({}) or SSL key ({}).", certFilePath, keyFilePath)
};
return std::unexpected{fmt::format("Error loading SSL certificate or SSL key.")};
}
return ctx;
}
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -19,14 +19,20 @@
#pragma once
#include "util/config/Config.hpp"
#include <boost/asio/ssl/context.hpp>
#include <expected>
#include <optional>
#include <string>
namespace web::impl {
namespace web::ng::impl {
std::expected<std::optional<boost::asio::ssl::context>, std::string>
makeServerSslContext(util::Config const& config);
std::expected<boost::asio::ssl::context, std::string>
makeServerSslContext(std::string const& certFilePath, std::string const& keyFilePath);
makeServerSslContext(std::string const& certData, std::string const& keyData);
} // namespace web::impl
} // namespace web::ng::impl

View File

@@ -0,0 +1,77 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/ng/impl/WsConnection.hpp"
#include "util/Taggable.hpp"
#include "web/ng/Error.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <memory>
#include <string>
#include <utility>
namespace web::ng::impl {
std::expected<std::unique_ptr<PlainWsConnection>, Error>
make_PlainWsConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::beast::http::request<boost::beast::http::string_body> request,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
)
{
auto connection = std::make_unique<PlainWsConnection>(
std::move(socket), std::move(ip), std::move(buffer), std::move(request), tagDecoratorFactory
);
auto maybeError = connection->performHandshake(yield);
if (maybeError.has_value())
return std::unexpected{maybeError.value()};
return connection;
}
std::expected<std::unique_ptr<SslWsConnection>, Error>
make_SslWsConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::ssl::context& sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
)
{
auto connection = std::make_unique<SslWsConnection>(
std::move(socket), std::move(ip), std::move(buffer), sslContext, std::move(request), tagDecoratorFactory
);
auto maybeError = connection->performHandshake(yield);
if (maybeError.has_value())
return std::unexpected{maybeError.value()};
return connection;
}
} // namespace web::ng::impl

View File

@@ -0,0 +1,178 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/Taggable.hpp"
#include "util/WithTimeout.hpp"
#include "util/build/Build.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/Concepts.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/role.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <utility>
namespace web::ng::impl {
template <typename StreamType>
class WsConnection : public Connection {
boost::beast::websocket::stream<StreamType> stream_;
boost::beast::http::request<boost::beast::http::string_body> initialRequest_;
public:
WsConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::beast::http::request<boost::beast::http::string_body> initialRequest,
util::TagDecoratorFactory const& tagDecoratorFactory
)
requires IsTcpStream<StreamType>
: Connection(std::move(ip), std::move(buffer), tagDecoratorFactory)
, stream_(std::move(socket))
, initialRequest_(std::move(initialRequest))
{
}
WsConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::asio::ssl::context& sslContext,
boost::beast::http::request<boost::beast::http::string_body> initialRequest,
util::TagDecoratorFactory const& tagDecoratorFactory
)
requires IsSslTcpStream<StreamType>
: Connection(std::move(ip), std::move(buffer), tagDecoratorFactory)
, stream_(std::move(socket), sslContext)
, initialRequest_(std::move(initialRequest))
{
// Disable the timeout. The websocket::stream uses its own timeout settings.
boost::beast::get_lowest_layer(stream_).expires_never();
stream_.set_option(boost::beast::websocket::stream_base::timeout::suggested(boost::beast::role_type::server));
stream_.set_option(
boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::response_type& res) {
res.set(boost::beast::http::field::server, util::build::getClioFullVersionString());
})
);
}
std::optional<Error>
performHandshake(boost::asio::yield_context yield)
{
Error error;
stream_.async_accept(initialRequest_, yield[error]);
if (error)
return error;
return std::nullopt;
}
bool
wasUpgraded() const override
{
return true;
}
std::optional<Error>
send(
Response response,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
) override
{
auto error = util::withTimeout(
[this, &response](auto&& yield) { stream_.async_write(response.asConstBuffer(), yield); }, yield, timeout
);
if (error)
return error;
return std::nullopt;
}
std::expected<Request, Error>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
{
auto error = util::withTimeout([this](auto&& yield) { stream_.async_read(buffer_, yield); }, yield, timeout);
if (error)
return std::unexpected{error};
auto request = boost::beast::buffers_to_string(buffer_.data());
buffer_.consume(buffer_.size());
return Request{std::move(request), initialRequest_};
}
void
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
{
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
stream_.async_close(boost::beast::websocket::close_code::normal, yield);
}
};
using PlainWsConnection = WsConnection<boost::beast::tcp_stream>;
using SslWsConnection = WsConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
std::expected<std::unique_ptr<PlainWsConnection>, Error>
make_PlainWsConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::beast::http::request<boost::beast::http::string_body> request,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
);
std::expected<std::unique_ptr<SslWsConnection>, Error>
make_SslWsConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::ssl::context& sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
);
} // namespace web::ng::impl

View File

@@ -1,8 +1,15 @@
add_library(clio_testing_common)
target_sources(
clio_testing_common PRIVATE util/StringUtils.cpp util/TestHttpServer.cpp util/TestWsServer.cpp util/TestObject.cpp
util/AssignRandomPort.cpp util/WithTimeout.cpp
clio_testing_common
PRIVATE util/AssignRandomPort.cpp
util/CallWithTimeout.cpp
util/StringUtils.cpp
util/TestHttpClient.cpp
util/TestHttpServer.cpp
util/TestObject.cpp
util/TestWebSocketClient.cpp
util/TestWsServer.cpp
)
include(deps/gtest)

View File

@@ -17,7 +17,7 @@
*/
//==============================================================================
#include "util/WithTimeout.hpp"
#include "util/CallWithTimeout.hpp"
#include <gtest/gtest.h>
@@ -30,7 +30,7 @@
namespace tests::common::util {
void
withTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function)
callWithTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function)
{
std::promise<void> promise;
auto future = promise.get_future();

View File

@@ -31,6 +31,6 @@ namespace tests::common::util {
* @param function The function to run
*/
void
withTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function);
callWithTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function);
} // namespace tests::common::util

View File

@@ -0,0 +1,250 @@
//------------------------------------------------------------------------------
/*
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/TestHttpClient.hpp"
#include "util/Assert.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.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/stream_traits.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.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 <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
namespace {
std::string
syncRequest(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders,
http::verb method,
std::string target = "/"
)
{
boost::asio::io_context ioc;
net::ip::tcp::resolver resolver(ioc);
boost::beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{method, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
for (auto& header : additionalHeaders) {
req.set(header.name, header.value);
}
req.target(target);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
}
} // namespace
WebHeader::WebHeader(http::field name, std::string value) : name(name), value(std::move(value))
{
}
std::string
HttpSyncClient::post(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
}
std::string
HttpSyncClient::get(
std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target);
}
bool
HttpsSyncClient::verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */)
{
return true;
}
std::string
HttpsSyncClient::syncPost(std::string const& host, std::string const& port, std::string const& body)
{
net::io_context ioc;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
// We can't fix this so have to ignore
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
#pragma GCC diagnostic pop
{
boost::beast::error_code const ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
throw boost::beast::system_error{ec};
}
auto const results = resolver.resolve(host, port);
boost::beast::get_lowest_layer(stream).connect(results);
stream.handshake(ssl::stream_base::client);
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.shutdown(ec);
return res.body();
}
HttpAsyncClient::HttpAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext}
{
}
std::optional<boost::system::error_code>
HttpAsyncClient::connect(
std::string_view host,
std::string_view port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
)
{
boost::system::error_code error;
boost::asio::ip::tcp::resolver resolver{stream_.get_executor()};
auto const resolverResults = resolver.resolve(host, port, error);
if (error)
return error;
ASSERT(resolverResults.size() > 0, "No results from resolver");
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.async_connect(resolverResults.begin()->endpoint(), yield[error]);
if (error)
return error;
return std::nullopt;
}
std::optional<boost::system::error_code>
HttpAsyncClient::send(
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
)
{
request.prepare_payload();
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
http::async_write(stream_, request, yield[error]);
if (error)
return error;
return std::nullopt;
}
std::expected<boost::beast::http::response<boost::beast::http::string_body>, boost::system::error_code>
HttpAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
http::response<http::string_body> response;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
http::async_read(stream_, buffer_, response, yield[error]);
if (error)
return std::unexpected{error};
return response;
}
void
HttpAsyncClient::gracefulShutdown()
{
boost::system::error_code error;
stream_.socket().shutdown(tcp::socket::shutdown_both, error);
}
void
HttpAsyncClient::disconnect()
{
boost::system::error_code error;
stream_.socket().close(error);
}

View File

@@ -0,0 +1,99 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <chrono>
#include <expected>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
struct WebHeader {
WebHeader(boost::beast::http::field name, std::string value);
boost::beast::http::field name;
std::string value;
};
struct HttpSyncClient {
static std::string
post(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders = {}
);
static std::string
get(std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders = {});
};
struct HttpsSyncClient {
static bool
verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */);
static std::string
syncPost(std::string const& host, std::string const& port, std::string const& body);
};
class HttpAsyncClient {
boost::beast::tcp_stream stream_;
boost::beast::flat_buffer buffer_;
public:
HttpAsyncClient(boost::asio::io_context& ioContext);
std::optional<boost::system::error_code>
connect(
std::string_view host,
std::string_view port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
);
std::optional<boost::system::error_code>
send(
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
);
std::expected<boost::beast::http::response<boost::beast::http::string_body>, boost::system::error_code>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout);
void
gracefulShutdown();
void
disconnect();
};

View File

@@ -19,6 +19,8 @@
#include "util/TestHttpServer.hpp"
#include "util/Assert.hpp"
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp>
@@ -36,6 +38,7 @@
#include <gtest/gtest.h>
#include <chrono>
#include <expected>
#include <string>
#include <utility>
@@ -107,13 +110,27 @@ doSession(
TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string host) : acceptor_(context)
{
boost::asio::ip::tcp::endpoint const endpoint(boost::asio::ip::make_address(host), 0);
boost::asio::ip::tcp::resolver resolver{context};
auto const results = resolver.resolve(host, "0");
ASSERT(!results.empty(), "Failed to resolve host");
boost::asio::ip::tcp::endpoint const& endpoint = results.begin()->endpoint();
acceptor_.open(endpoint.protocol());
acceptor_.set_option(asio::socket_base::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen(asio::socket_base::max_listen_connections);
}
std::expected<boost::asio::ip::tcp::socket, boost::system::error_code>
TestHttpServer::accept(boost::asio::yield_context yield)
{
boost::beast::error_code errorCode;
tcp::socket socket(this->acceptor_.get_executor());
acceptor_.async_accept(socket, yield[errorCode]);
if (errorCode)
return std::unexpected{errorCode};
return socket;
}
void
TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler, bool const allowToFail)
{

View File

@@ -21,9 +21,11 @@
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <expected>
#include <functional>
#include <optional>
#include <string>
@@ -44,6 +46,15 @@ public:
*/
TestHttpServer(boost::asio::io_context& context, std::string host);
/**
* @brief Accept a new connection
*
* @param yield boost::asio::yield_context to use for networking
* @return Either a socket with the new connection or an error code
*/
std::expected<boost::asio::ip::tcp::socket, boost::system::error_code>
accept(boost::asio::yield_context yield);
/**
* @brief Start the server
*

View File

@@ -1,270 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.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/stream_traits.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.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 <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
struct WebHeader {
WebHeader(http::field name, std::string value) : name(name), value(std::move(value))
{
}
http::field name;
std::string value;
};
struct HttpSyncClient {
static std::string
syncPost(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders = {}
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
}
static std::string
syncGet(
std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders = {}
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target);
}
private:
static std::string
syncRequest(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders,
http::verb method,
std::string target = "/"
)
{
boost::asio::io_context ioc;
net::ip::tcp::resolver resolver(ioc);
boost::beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{method, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
for (auto& header : additionalHeaders) {
req.set(header.name, header.value);
}
req.target(target);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
}
};
class WebSocketSyncClient {
net::io_context ioc_;
tcp::resolver resolver_{ioc_};
boost::beast::websocket::stream<tcp::socket> ws_{ioc_};
public:
void
connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders = {})
{
auto const results = resolver_.resolve(host, port);
auto const ep = net::connect(ws_.next_layer(), results);
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
auto const hostPort = host + ':' + std::to_string(ep.port());
ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
ws_.handshake(hostPort, "/");
}
void
disconnect()
{
ws_.close(boost::beast::websocket::close_code::normal);
}
std::string
syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_.write(net::buffer(std::string(body)));
ws_.read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
};
struct HttpsSyncClient {
static bool
verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */)
{
return true;
}
static std::string
syncPost(std::string const& host, std::string const& port, std::string const& body)
{
net::io_context ioc;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
// We can't fix this so have to ignore
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
#pragma GCC diagnostic pop
{
boost::beast::error_code const ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
throw boost::beast::system_error{ec};
}
auto const results = resolver.resolve(host, port);
boost::beast::get_lowest_layer(stream).connect(results);
stream.handshake(ssl::stream_base::client);
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.shutdown(ec);
return res.body();
}
};
class WebServerSslSyncClient {
net::io_context ioc_;
std::optional<boost::beast::websocket::stream<boost::beast::ssl_stream<tcp::socket>>> ws_;
public:
void
connect(std::string const& host, std::string const& port)
{
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver{ioc_};
ws_.emplace(ioc_, ctx);
auto const results = resolver.resolve(host, port);
net::connect(ws_->next_layer().next_layer(), results.begin(), results.end());
ws_->next_layer().handshake(ssl::stream_base::client);
ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws_->handshake(host, "/");
}
void
disconnect()
{
ws_->close(boost::beast::websocket::close_code::normal);
}
std::string
syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_->write(net::buffer(std::string(body)));
ws_->read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
};

View File

@@ -0,0 +1,225 @@
//------------------------------------------------------------------------------
/*
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/TestWebSocketClient.hpp"
#include "util/Assert.hpp"
#include "util/TestHttpClient.hpp"
#include "util/WithTimeout.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.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/stream_traits.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.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 <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <fmt/core.h>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
void
WebSocketSyncClient::connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders)
{
auto const results = resolver_.resolve(host, port);
auto const ep = net::connect(ws_.next_layer(), results);
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
auto const hostPort = host + ':' + std::to_string(ep.port());
ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
ws_.handshake(hostPort, "/");
}
void
WebSocketSyncClient::disconnect()
{
ws_.close(boost::beast::websocket::close_code::normal);
}
std::string
WebSocketSyncClient::syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_.write(net::buffer(std::string(body)));
ws_.read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
void
WebServerSslSyncClient::connect(std::string const& host, std::string const& port)
{
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver{ioc_};
ws_.emplace(ioc_, ctx);
auto const results = resolver.resolve(host, port);
net::connect(ws_->next_layer().next_layer(), results.begin(), results.end());
ws_->next_layer().handshake(ssl::stream_base::client);
ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws_->handshake(host, "/");
}
void
WebServerSslSyncClient::disconnect()
{
ws_->close(boost::beast::websocket::close_code::normal);
}
std::string
WebServerSslSyncClient::syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_->write(net::buffer(std::string(body)));
ws_->read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
WebSocketAsyncClient::WebSocketAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext}
{
}
std::optional<boost::system::error_code>
WebSocketAsyncClient::connect(
std::string const& host,
std::string const& port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout,
std::vector<WebHeader> additionalHeaders
)
{
auto const results = boost::asio::ip::tcp::resolver{yield.get_executor()}.resolve(host, port);
ASSERT(not results.empty(), "Could not resolve {}:{}", host, port);
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.next_layer().async_connect(results, yield[error]);
if (error)
return error;
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
boost::beast::get_lowest_layer(stream_).expires_never();
stream_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
stream_.async_handshake(fmt::format("{}:{}", host, port), "/", yield[error]);
if (error)
return error;
return std::nullopt;
}
std::optional<boost::system::error_code>
WebSocketAsyncClient::send(
boost::asio::yield_context yield,
std::string_view message,
std::chrono::steady_clock::duration timeout
)
{
auto const error = util::withTimeout(
[this, &message](auto&& cyield) { stream_.async_write(net::buffer(message), cyield); }, yield, timeout
);
if (error)
return error;
return std::nullopt;
}
std::expected<std::string, boost::system::error_code>
WebSocketAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::flat_buffer buffer{};
auto error =
util::withTimeout([this, &buffer](auto&& cyield) { stream_.async_read(buffer, cyield); }, yield, timeout);
if (error)
return std::unexpected{error};
return boost::beast::buffers_to_string(buffer.data());
}
void
WebSocketAsyncClient::gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
stream_.async_close(boost::beast::websocket::close_code::normal, yield);
}
void
WebSocketAsyncClient::close()
{
boost::beast::get_lowest_layer(stream_).close();
}

View File

@@ -0,0 +1,94 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/TestHttpClient.hpp"
#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/ssl/ssl_stream.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
class WebSocketSyncClient {
boost::asio::io_context ioc_;
boost::asio::ip::tcp::resolver resolver_{ioc_};
boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws_{ioc_};
public:
void
connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders = {});
void
disconnect();
std::string
syncPost(std::string const& body);
};
class WebSocketAsyncClient {
boost::beast::websocket::stream<boost::beast::tcp_stream> stream_;
public:
WebSocketAsyncClient(boost::asio::io_context& ioContext);
std::optional<boost::system::error_code>
connect(
std::string const& host,
std::string const& port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout,
std::vector<WebHeader> additionalHeaders = {}
);
std::optional<boost::system::error_code>
send(boost::asio::yield_context yield, std::string_view message, std::chrono::steady_clock::duration timeout);
std::expected<std::string, boost::system::error_code>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout);
void
gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout);
void
close();
};
class WebServerSslSyncClient {
boost::asio::io_context ioc_;
std::optional<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>> ws_;
public:
void
connect(std::string const& host, std::string const& port);
void
disconnect();
std::string
syncPost(std::string const& body);
};

View File

@@ -25,9 +25,10 @@
#include <ios>
#include <string>
#include <string_view>
#include <utility>
struct TmpFile {
std::string const path;
std::string path;
TmpFile(std::string_view content) : path{std::tmpnam(nullptr)}
{
@@ -36,8 +37,25 @@ struct TmpFile {
ofs << content;
}
TmpFile(TmpFile const&) = delete;
TmpFile(TmpFile&& other) : path{std::move(other.path)}
{
other.path.clear();
}
TmpFile&
operator=(TmpFile const&) = delete;
TmpFile&
operator=(TmpFile&& other)
{
if (this != &other)
*this = std::move(other);
return *this;
}
~TmpFile()
{
std::filesystem::remove(path);
if (not path.empty())
std::filesystem::remove(path);
}
};

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
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 "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
struct MockConnectionImpl : web::ng::Connection {
using web::ng::Connection::Connection;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(
SendReturnType,
send,
(web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
MOCK_METHOD(
ReceiveReturnType,
receive,
(boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration));
};
using MockConnection = testing::NiceMock<MockConnectionImpl>;
using MockConnectionPtr = std::unique_ptr<testing::NiceMock<MockConnectionImpl>>;
using StrictMockConnection = testing::StrictMock<MockConnectionImpl>;
using StrictMockConnectionPtr = std::unique_ptr<testing::StrictMock<MockConnectionImpl>>;

View File

@@ -94,6 +94,7 @@ target_sources(
rpc/RPCEngineTests.cpp
rpc/RPCHelpersTests.cpp
rpc/WorkQueueTests.cpp
test_data/SslCert.cpp
util/AccountUtilsTests.cpp
util/AssertTests.cpp
# Async framework
@@ -103,6 +104,7 @@ target_sources(
util/async/AnyStrandTests.cpp
util/async/AsyncExecutionContextTests.cpp
util/BatchingTests.cpp
util/CoroutineGroupTests.cpp
util/LedgerUtilsTests.cpp
# Prometheus support
util/prometheus/BoolTests.cpp
@@ -125,12 +127,19 @@ target_sources(
util/SignalsHandlerTests.cpp
util/TimeUtilsTests.cpp
util/TxUtilTests.cpp
util/WithTimeout.cpp
# Webserver
web/AdminVerificationTests.cpp
web/dosguard/DOSGuardTests.cpp
web/dosguard/IntervalSweepHandlerTests.cpp
web/dosguard/WhitelistHandlerTests.cpp
web/impl/ServerSslContextTests.cpp
web/impl/AdminVerificationTests.cpp
web/ng/ResponseTests.cpp
web/ng/RequestTests.cpp
web/ng/ServerTests.cpp
web/ng/impl/ConnectionHandlerTests.cpp
web/ng/impl/HttpConnectionTests.cpp
web/ng/impl/ServerSslContextTests.cpp
web/ng/impl/WsConnectionTests.cpp
web/RPCServerHandlerTests.cpp
web/ServerTests.cpp
# New Config
@@ -143,12 +152,6 @@ target_sources(
util/newconfig/ValueViewTests.cpp
)
configure_file(test_data/cert.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/cert.pem COPYONLY)
target_compile_definitions(clio_tests PRIVATE TEST_DATA_SSL_CERT_PATH="tests/unit/test_data/cert.pem")
configure_file(test_data/key.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/key.pem COPYONLY)
target_compile_definitions(clio_tests PRIVATE TEST_DATA_SSL_KEY_PATH="tests/unit/test_data/key.pem")
# See https://github.com/google/googletest/issues/3475
gtest_discover_tests(clio_tests DISCOVERY_TIMEOUT 90)

View File

@@ -0,0 +1,105 @@
//------------------------------------------------------------------------------
/*
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/TmpFile.hpp"
#include <test_data/SslCert.hpp>
#include <string_view>
namespace tests {
std::string_view
sslCert()
{
static auto constexpr CERT = R"(
-----BEGIN CERTIFICATE-----
MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV
BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz
Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN
MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m
pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y
7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w
A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk
PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979
WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p
AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x
MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN
BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn
svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO
p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb
8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp
LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+
PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw==
-----END CERTIFICATE-----
)";
return CERT;
}
TmpFile
sslCertFile()
{
return TmpFile{sslCert()};
}
std::string_view
sslKey()
{
static auto constexpr KEY = R"(
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4
32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd
g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x
evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC
tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC
oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4
YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1
vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ
ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh
+lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk
naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK
nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn
K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z
dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz
fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs
fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7
zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl
6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf
pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV
zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI
9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya
LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD
KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du
O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7
Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA==
-----END RSA PRIVATE KEY-----
)";
return KEY;
}
TmpFile
sslKeyFile()
{
return TmpFile{sslKey()};
}
} // namespace tests

View File

@@ -17,32 +17,24 @@
*/
//==============================================================================
#include "web/Server.hpp"
#pragma once
#include "util/config/Config.hpp"
#include "util/TmpFile.hpp"
#include <boost/asio/ssl/context.hpp>
#include <string_view>
#include <optional>
#include <string>
namespace tests {
namespace web {
std::string_view
sslCert();
std::expected<std::optional<boost::asio::ssl::context>, std::string>
makeServerSslContext(util::Config const& config)
{
bool const configHasCertFile = config.contains("ssl_cert_file");
bool const configHasKeyFile = config.contains("ssl_key_file");
TmpFile
sslCertFile();
if (configHasCertFile != configHasKeyFile)
return std::unexpected{"Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together."};
std::string_view
sslKey();
if (not configHasCertFile)
return std::nullopt;
TmpFile
sslKeyFile();
auto const certFilename = config.value<std::string>("ssl_cert_file");
auto const keyFilename = config.value<std::string>("ssl_key_file");
return impl::makeServerSslContext(certFilename, keyFilename);
}
} // namespace web
} // namespace tests

View File

@@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV
BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz
Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN
MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m
pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y
7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w
A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk
PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979
WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p
AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x
MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN
BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn
svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO
p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb
8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp
LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+
PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw==
-----END CERTIFICATE-----

View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4
32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd
g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x
evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC
tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC
oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4
YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1
vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ
ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh
+lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk
naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK
nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn
K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z
dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz
fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs
fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7
zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl
6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf
pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV
zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI
9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya
LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD
KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du
O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7
Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA==
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,167 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/CoroutineGroup.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
using namespace util;
struct CoroutineGroupTests : SyncAsioContextTest {
testing::StrictMock<testing::MockFunction<void()>> callback1_;
testing::StrictMock<testing::MockFunction<void()>> callback2_;
testing::StrictMock<testing::MockFunction<void()>> callback3_;
};
TEST_F(CoroutineGroupTests, SpawnWait)
{
testing::Sequence sequence;
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback2_, Call).InSequence(sequence);
EXPECT_CALL(callback3_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 2};
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback1_.Call();
});
EXPECT_EQ(group.size(), 1);
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
callback2_.Call();
});
EXPECT_EQ(group.size(), 2);
group.asyncWait(yield);
EXPECT_EQ(group.size(), 0);
callback3_.Call();
});
}
TEST_F(CoroutineGroupTests, SpawnWaitSpawnWait)
{
testing::Sequence sequence;
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback2_, Call).InSequence(sequence);
EXPECT_CALL(callback3_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 2};
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback1_.Call();
});
EXPECT_EQ(group.size(), 1);
group.asyncWait(yield);
EXPECT_EQ(group.size(), 0);
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback2_.Call();
});
EXPECT_EQ(group.size(), 1);
group.asyncWait(yield);
EXPECT_EQ(group.size(), 0);
callback3_.Call();
});
}
TEST_F(CoroutineGroupTests, ChildCoroutinesFinishBeforeWait)
{
testing::Sequence sequence;
EXPECT_CALL(callback2_, Call).InSequence(sequence);
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback3_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 2};
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
callback1_.Call();
});
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback2_.Call();
});
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{3}};
timer.async_wait(yield);
group.asyncWait(yield);
callback3_.Call();
});
}
TEST_F(CoroutineGroupTests, EmptyGroup)
{
EXPECT_CALL(callback1_, Call);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield};
group.asyncWait(yield);
callback1_.Call();
});
}
TEST_F(CoroutineGroupTests, TooManyCoroutines)
{
EXPECT_CALL(callback1_, Call);
EXPECT_CALL(callback2_, Call);
EXPECT_CALL(callback3_, Call);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 1};
EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context innerYield) {
boost::asio::steady_timer timer{innerYield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(innerYield);
callback1_.Call();
}));
EXPECT_FALSE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
group.asyncWait(yield);
callback3_.Call();
});
}

View File

@@ -18,8 +18,8 @@
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/CallWithTimeout.hpp"
#include "util/Repeat.hpp"
#include "util/WithTimeout.hpp"
#include <boost/asio/executor_work_guard.hpp>
#include <gmock/gmock.h>
@@ -41,7 +41,7 @@ struct RepeatTests : SyncAsioContextTest {
void
withRunningContext(std::function<void()> func)
{
tests::common::util::withTimeout(std::chrono::seconds{1000}, [this, func = std::move(func)]() {
tests::common::util::callWithTimeout(std::chrono::seconds{1}, [this, func = std::move(func)]() {
auto workGuard = boost::asio::make_work_guard(ctx);
std::thread thread{[this]() { ctx.run(); }};
func();

View File

@@ -0,0 +1,77 @@
//------------------------------------------------------------------------------
/*
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/WithTimeout.hpp"
#include "util/AsioContextTestFixture.hpp"
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
struct WithTimeoutTests : SyncAsioContextTest {
using CYieldType = boost::asio::cancellation_slot_binder<
boost::asio::basic_yield_context<boost::asio::any_io_executor>,
boost::asio::cancellation_slot>;
testing::StrictMock<testing::MockFunction<void(CYieldType)>> operationMock;
};
TEST_F(WithTimeoutTests, CallsOperation)
{
EXPECT_CALL(operationMock, Call);
runSpawn([&](boost::asio::yield_context yield) {
auto const error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::seconds{1});
EXPECT_EQ(error, boost::system::error_code{});
});
}
TEST_F(WithTimeoutTests, TimesOut)
{
EXPECT_CALL(operationMock, Call).WillOnce([](auto cyield) {
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield)};
timer.expires_after(std::chrono::milliseconds{10});
timer.async_wait(cyield);
});
runSpawn([&](boost::asio::yield_context yield) {
auto error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::milliseconds{1});
EXPECT_EQ(error.value(), boost::system::errc::timed_out);
});
}
TEST_F(WithTimeoutTests, OperationFailed)
{
EXPECT_CALL(operationMock, Call).WillOnce([](auto cyield) {
boost::asio::ip::tcp::socket socket{boost::asio::get_associated_executor(cyield)};
socket.async_send(boost::asio::buffer("test"), cyield);
});
runSpawn([&](boost::asio::yield_context yield) {
auto error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::seconds{1});
EXPECT_EQ(error.value(), boost::system::errc::bad_file_descriptor);
});
}

View File

@@ -20,7 +20,9 @@
#include "util/AssignRandomPort.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/TestHttpSyncClient.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/TmpFile.hpp"
#include "util/config/Config.hpp"
#include "util/prometheus/Gauge.hpp"
#include "util/prometheus/Label.hpp"
@@ -45,6 +47,7 @@
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <test_data/SslCert.hpp>
#include <condition_variable>
#include <cstdint>
@@ -103,14 +106,6 @@ generateJSONDataOverload(std::string_view port)
));
}
boost::json::value
addSslConfig(boost::json::value config)
{
config.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH;
config.as_object()["ssl_cert_file"] = TEST_DATA_SSL_CERT_PATH;
return config;
}
struct WebServerTest : NoLoggerFixture {
~WebServerTest() override
{
@@ -126,6 +121,14 @@ struct WebServerTest : NoLoggerFixture {
runner.emplace([this] { ctx.run(); });
}
boost::json::value
addSslConfig(boost::json::value config) const
{
config.as_object()["ssl_key_file"] = sslKeyFile.path;
config.as_object()["ssl_cert_file"] = sslCertFile.path;
return config;
}
// this ctx is for dos timer
boost::asio::io_context ctxSync;
std::string const port = std::to_string(tests::util::generateFreePort());
@@ -141,6 +144,9 @@ struct WebServerTest : NoLoggerFixture {
// this ctx is for http server
boost::asio::io_context ctx;
TmpFile sslCertFile{tests::sslCertFile()};
TmpFile sslKeyFile{tests::sslKeyFile()};
private:
std::optional<boost::asio::io_service::work> work;
std::optional<std::thread> runner;
@@ -212,7 +218,7 @@ TEST_F(WebServerTestsWithMockPrometheus, Http)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", port, R"({"Hello":1})");
auto const res = HttpSyncClient::post("localhost", port, R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})");
}
@@ -236,7 +242,7 @@ TEST_F(WebServerTestsWithMockPrometheus, HttpInternalError)
{
auto e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", port, R"({})");
auto const res = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(
res,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})"
@@ -286,7 +292,7 @@ TEST_F(WebServerTestsWithMockPrometheus, IncompleteSslConfig)
auto e = std::make_shared<EchoExecutor>();
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH;
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e);
EXPECT_EQ(server, nullptr);
@@ -297,7 +303,7 @@ TEST_F(WebServerTestsWithMockPrometheus, WrongSslConfig)
auto e = std::make_shared<EchoExecutor>();
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH;
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
jsonConfig.as_object()["ssl_cert_file"] = "wrong_path";
auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e);
@@ -334,9 +340,9 @@ TEST_F(WebServerTestsWithMockPrometheus, HttpRequestOverload)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto res = HttpSyncClient::syncPost("localhost", port, R"({})");
auto res = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(res, "{}");
res = HttpSyncClient::syncPost("localhost", port, R"({})");
res = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(
res,
R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})"
@@ -372,7 +378,7 @@ TEST_F(WebServerTestsWithMockPrometheus, HttpPayloadOverload)
std::string const s100(100, 'a');
auto e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto const res = HttpSyncClient::syncPost("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100));
auto const res = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100));
EXPECT_EQ(
res,
R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})"
@@ -535,7 +541,7 @@ TEST_P(WebServerAdminTest, HttpAdminCheck)
auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e);
std::string const request = "Why hello";
uint32_t const webServerPort = serverConfig.value<uint32_t>("server.port");
auto const res = HttpSyncClient::syncPost("localhost", std::to_string(webServerPort), request, GetParam().headers);
auto const res = HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers);
EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
}
@@ -653,7 +659,7 @@ TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
uint32_t const webServerPort = tests::util::generateFreePort();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncGet("localhost", std::to_string(webServerPort), "", "/metrics");
auto const res = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics");
EXPECT_EQ(res, "Only admin is allowed to collect metrics");
}
@@ -676,7 +682,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)};
PrometheusService::init(serverConfig);
auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncGet(
auto const res = HttpSyncClient::get(
"localhost",
std::to_string(webServerPort),
"",
@@ -697,7 +703,7 @@ TEST_F(WebServerPrometheusTest, validResponse)
auto e = std::make_shared<EchoExecutor>();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncGet(
auto const res = HttpSyncClient::get(
"localhost",
std::to_string(webServerPort),
"",

View File

@@ -18,16 +18,17 @@
//==============================================================================
#include "util/LoggerFixtures.hpp"
#include "util/config/Config.hpp"
#include "web/impl/AdminVerificationStrategy.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <optional>
#include <string>
#include <utility>
namespace http = boost::beast::http;
@@ -81,16 +82,7 @@ TEST_F(PasswordAdminVerificationStrategyTest, IsAdminReturnsTrueOnlyForValidPass
}
struct MakeAdminVerificationStrategyTestParams {
MakeAdminVerificationStrategyTestParams(
std::optional<std::string> passwordOpt,
bool expectIpStrategy,
bool expectPasswordStrategy
)
: passwordOpt(std::move(passwordOpt))
, expectIpStrategy(expectIpStrategy)
, expectPasswordStrategy(expectPasswordStrategy)
{
}
std::string testName;
std::optional<std::string> passwordOpt;
bool expectIpStrategy;
bool expectPasswordStrategy;
@@ -111,8 +103,78 @@ INSTANTIATE_TEST_CASE_P(
MakeAdminVerificationStrategyTest,
MakeAdminVerificationStrategyTest,
testing::Values(
MakeAdminVerificationStrategyTestParams(std::nullopt, true, false),
MakeAdminVerificationStrategyTestParams("p", false, true),
MakeAdminVerificationStrategyTestParams("", false, true)
MakeAdminVerificationStrategyTestParams{
.testName = "NoPassword",
.passwordOpt = std::nullopt,
.expectIpStrategy = true,
.expectPasswordStrategy = false
},
MakeAdminVerificationStrategyTestParams{
.testName = "HasPassword",
.passwordOpt = "p",
.expectIpStrategy = false,
.expectPasswordStrategy = true
},
MakeAdminVerificationStrategyTestParams{
.testName = "EmptyPassword",
.passwordOpt = "",
.expectIpStrategy = false,
.expectPasswordStrategy = true
}
)
);
struct MakeAdminVerificationStrategyFromConfigTestParams {
std::string testName;
std::string config;
bool expectedError;
};
struct MakeAdminVerificationStrategyFromConfigTest
: public testing::TestWithParam<MakeAdminVerificationStrategyFromConfigTestParams> {};
TEST_P(MakeAdminVerificationStrategyFromConfigTest, ChecksConfig)
{
util::Config serverConfig{boost::json::parse(GetParam().config)};
auto const result = web::impl::make_AdminVerificationStrategy(serverConfig);
if (GetParam().expectedError) {
EXPECT_FALSE(result.has_value());
}
}
INSTANTIATE_TEST_SUITE_P(
MakeAdminVerificationStrategyFromConfigTest,
MakeAdminVerificationStrategyFromConfigTest,
testing::Values(
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "NoPasswordNoLocalAdmin",
.config = "{}",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyPassword",
.config = R"({"admin_password": "password"})",
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyLocalAdmin",
.config = R"({"local_admin": true})",
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyLocalAdminDisabled",
.config = R"({"local_admin": false})",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "LocalAdminAndPassword",
.config = R"({"local_admin": true, "admin_password": "password"})",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "LocalAdminDisabledAndPassword",
.config = R"({"local_admin": false, "admin_password": "password"})",
.expectedError = false
}
)
);

View File

@@ -0,0 +1,224 @@
//------------------------------------------------------------------------------
/*
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/NameGenerator.hpp"
#include "web/ng/Request.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 <gtest/gtest.h>
#include <optional>
#include <string>
using namespace web::ng;
namespace http = boost::beast::http;
struct RequestTest : public ::testing::Test {};
struct RequestMethodTestBundle {
std::string testName;
Request request;
Request::Method expectedMethod;
};
struct RequestMethodTest : RequestTest, ::testing::WithParamInterface<RequestMethodTestBundle> {};
TEST_P(RequestMethodTest, method)
{
EXPECT_EQ(GetParam().request.method(), GetParam().expectedMethod);
}
INSTANTIATE_TEST_SUITE_P(
RequestMethodTest,
RequestMethodTest,
testing::Values(
RequestMethodTestBundle{
.testName = "HttpGet",
.request = Request{http::request<http::string_body>{http::verb::get, "/", 11}},
.expectedMethod = Request::Method::Get,
},
RequestMethodTestBundle{
.testName = "HttpPost",
.request = Request{http::request<http::string_body>{http::verb::post, "/", 11}},
.expectedMethod = Request::Method::Post,
},
RequestMethodTestBundle{
.testName = "WebSocket",
.request = Request{"websocket message", Request::HttpHeaders{}},
.expectedMethod = Request::Method::Websocket,
},
RequestMethodTestBundle{
.testName = "Unsupported",
.request = Request{http::request<http::string_body>{http::verb::acl, "/", 11}},
.expectedMethod = Request::Method::Unsupported,
}
),
tests::util::NameGenerator
);
struct RequestIsHttpTestBundle {
std::string testName;
Request request;
bool expectedIsHttp;
};
struct RequestIsHttpTest : RequestTest, testing::WithParamInterface<RequestIsHttpTestBundle> {};
TEST_P(RequestIsHttpTest, isHttp)
{
EXPECT_EQ(GetParam().request.isHttp(), GetParam().expectedIsHttp);
}
INSTANTIATE_TEST_SUITE_P(
RequestIsHttpTest,
RequestIsHttpTest,
testing::Values(
RequestIsHttpTestBundle{
.testName = "HttpRequest",
.request = Request{http::request<http::string_body>{http::verb::get, "/", 11}},
.expectedIsHttp = true,
},
RequestIsHttpTestBundle{
.testName = "WebSocketRequest",
.request = Request{"websocket message", Request::HttpHeaders{}},
.expectedIsHttp = false,
}
),
tests::util::NameGenerator
);
struct RequestAsHttpRequestTest : RequestTest {};
TEST_F(RequestAsHttpRequestTest, HttpRequest)
{
http::request<http::string_body> const httpRequest{http::verb::get, "/some", 11};
Request const request{httpRequest};
auto const maybeHttpRequest = request.asHttpRequest();
ASSERT_TRUE(maybeHttpRequest.has_value());
auto const& actualHttpRequest = maybeHttpRequest->get();
EXPECT_EQ(actualHttpRequest.method(), httpRequest.method());
EXPECT_EQ(actualHttpRequest.target(), httpRequest.target());
EXPECT_EQ(actualHttpRequest.version(), httpRequest.version());
}
TEST_F(RequestAsHttpRequestTest, WebSocketRequest)
{
Request const request{"websocket message", Request::HttpHeaders{}};
auto const maybeHttpRequest = request.asHttpRequest();
EXPECT_FALSE(maybeHttpRequest.has_value());
}
struct RequestMessageTest : RequestTest {};
TEST_F(RequestMessageTest, HttpRequest)
{
std::string const body = "some body";
http::request<http::string_body> const httpRequest{http::verb::post, "/some", 11, body};
Request const request{httpRequest};
EXPECT_EQ(request.message(), httpRequest.body());
}
TEST_F(RequestMessageTest, WebSocketRequest)
{
std::string const message = "websocket message";
Request const request{message, Request::HttpHeaders{}};
EXPECT_EQ(request.message(), message);
}
struct RequestTargetTestBundle {
std::string testName;
Request request;
std::optional<std::string> expectedTarget;
};
struct RequestTargetTest : RequestTest, ::testing::WithParamInterface<RequestTargetTestBundle> {};
TEST_P(RequestTargetTest, target)
{
auto const maybeTarget = GetParam().request.target();
EXPECT_EQ(maybeTarget, GetParam().expectedTarget);
}
INSTANTIATE_TEST_SUITE_P(
RequestTargetTest,
RequestTargetTest,
testing::Values(
RequestTargetTestBundle{
.testName = "HttpRequest",
.request = Request{http::request<http::string_body>{http::verb::get, "/some", 11}},
.expectedTarget = "/some",
},
RequestTargetTestBundle{
.testName = "WebSocketRequest",
.request = Request{"websocket message", Request::HttpHeaders{}},
.expectedTarget = std::nullopt,
}
),
tests::util::NameGenerator
);
struct RequestHeaderValueTest : RequestTest {};
TEST_F(RequestHeaderValueTest, headerValue)
{
http::request<http::string_body> httpRequest{http::verb::get, "/some", 11};
http::field const headerName = http::field::user_agent;
std::string const headerValue = "clio";
httpRequest.set(headerName, headerValue);
Request const request{httpRequest};
auto const maybeHeaderValue = request.headerValue(headerName);
ASSERT_TRUE(maybeHeaderValue.has_value());
EXPECT_EQ(maybeHeaderValue.value(), headerValue);
}
TEST_F(RequestHeaderValueTest, headerValueString)
{
http::request<http::string_body> httpRequest{http::verb::get, "/some", 11};
std::string const headerName = "Custom";
std::string const headerValue = "some value";
httpRequest.set(headerName, headerValue);
Request const request{httpRequest};
auto const maybeHeaderValue = request.headerValue(headerName);
ASSERT_TRUE(maybeHeaderValue.has_value());
EXPECT_EQ(maybeHeaderValue.value(), headerValue);
}
TEST_F(RequestHeaderValueTest, headerValueNotFound)
{
http::request<http::string_body> httpRequest{http::verb::get, "/some", 11};
Request const request{httpRequest};
auto const maybeHeaderValue = request.headerValue(http::field::user_agent);
EXPECT_FALSE(maybeHeaderValue.has_value());
}
TEST_F(RequestHeaderValueTest, headerValueWebsocketRequest)
{
Request::HttpHeaders headers;
http::field const headerName = http::field::user_agent;
std::string const headerValue = "clio";
headers.set(headerName, headerValue);
Request const request{"websocket message", headers};
auto const maybeHeaderValue = request.headerValue(headerName);
ASSERT_TRUE(maybeHeaderValue.has_value());
EXPECT_EQ(maybeHeaderValue.value(), headerValue);
}

View File

@@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
/*
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/build/Build.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <fmt/core.h>
#include <fmt/format.h>
#include <gtest/gtest.h>
#include <string>
#include <utility>
using namespace web::ng;
namespace http = boost::beast::http;
struct ResponseDeathTest : testing::Test {};
TEST_F(ResponseDeathTest, intoHttpResponseWithoutHttpData)
{
Request const request{"some messsage", Request::HttpHeaders{}};
web::ng::Response response{boost::beast::http::status::ok, "message", request};
EXPECT_DEATH(std::move(response).intoHttpResponse(), "");
}
TEST_F(ResponseDeathTest, asConstBufferWithHttpData)
{
Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::ng::Response response{boost::beast::http::status::ok, "message", request};
EXPECT_DEATH(response.asConstBuffer(), "");
}
struct ResponseTest : testing::Test {
int const httpVersion_ = 11;
http::status const responseStatus_ = http::status::ok;
};
TEST_F(ResponseTest, intoHttpResponse)
{
Request const request{http::request<http::string_body>{http::verb::post, "/", httpVersion_, "some message"}};
std::string const responseMessage = "response message";
web::ng::Response response{responseStatus_, responseMessage, request};
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), responseStatus_);
EXPECT_EQ(httpResponse.body(), responseMessage);
EXPECT_EQ(httpResponse.version(), httpVersion_);
EXPECT_EQ(httpResponse.keep_alive(), request.asHttpRequest()->get().keep_alive());
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::content_type], "text/html");
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::server], fmt::format("clio-server-{}", util::build::getClioVersionString()));
}
TEST_F(ResponseTest, intoHttpResponseJson)
{
Request const request{http::request<http::string_body>{http::verb::post, "/", httpVersion_, "some message"}};
boost::json::object const responseMessage{{"key", "value"}};
web::ng::Response response{responseStatus_, responseMessage, request};
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), responseStatus_);
EXPECT_EQ(httpResponse.body(), boost::json::serialize(responseMessage));
EXPECT_EQ(httpResponse.version(), httpVersion_);
EXPECT_EQ(httpResponse.keep_alive(), request.asHttpRequest()->get().keep_alive());
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::content_type], "application/json");
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::server], fmt::format("clio-server-{}", util::build::getClioVersionString()));
}
TEST_F(ResponseTest, asConstBuffer)
{
Request const request("some request", Request::HttpHeaders{});
std::string const responseMessage = "response message";
web::ng::Response response{responseStatus_, responseMessage, request};
auto const buffer = response.asConstBuffer();
EXPECT_EQ(buffer.size(), responseMessage.size());
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
EXPECT_EQ(messageFromBuffer, responseMessage);
}
TEST_F(ResponseTest, asConstBufferJson)
{
Request const request("some request", Request::HttpHeaders{});
boost::json::object const responseMessage{{"key", "value"}};
web::ng::Response response{responseStatus_, responseMessage, request};
auto const buffer = response.asConstBuffer();
EXPECT_EQ(buffer.size(), boost::json::serialize(responseMessage).size());
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
EXPECT_EQ(messageFromBuffer, boost::json::serialize(responseMessage));
}

View File

@@ -0,0 +1,332 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/AssignRandomPort.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "util/Taggable.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/Server.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <optional>
#include <ranges>
#include <string>
#include <utility>
using namespace web::ng;
namespace http = boost::beast::http;
struct MakeServerTestBundle {
std::string testName;
std::string configJson;
bool expectSuccess;
};
struct MakeServerTest : NoLoggerFixture, testing::WithParamInterface<MakeServerTestBundle> {
boost::asio::io_context ioContext_;
};
TEST_P(MakeServerTest, Make)
{
util::Config const config{boost::json::parse(GetParam().configJson)};
auto const expectedServer = make_Server(config, ioContext_);
EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess);
}
INSTANTIATE_TEST_CASE_P(
MakeServerTests,
MakeServerTest,
testing::Values(
MakeServerTestBundle{
"NoIp",
R"json(
{
"server": {"port": 12345}
}
)json",
false
},
MakeServerTestBundle{
"BadEndpoint",
R"json(
{
"server": {"ip": "wrong", "port": 12345}
}
)json",
false
},
MakeServerTestBundle{
"PortMissing",
R"json(
{
"server": {"ip": "127.0.0.1"}
}
)json",
false
},
MakeServerTestBundle{
"BadSslConfig",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345},
"ssl_cert_file": "somг_file"
}
)json",
false
},
MakeServerTestBundle{
"BadProcessingPolicy",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "wrong"}
}
)json",
false
},
MakeServerTestBundle{
"CorrectConfig_ParallelPolicy",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "parallel"}
}
)json",
true
},
MakeServerTestBundle{
"CorrectConfig_SequentPolicy",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "sequent"}
}
)json",
true
}
),
tests::util::NameGenerator
);
struct ServerTest : SyncAsioContextTest {
ServerTest()
{
[&]() { ASSERT_TRUE(server_.has_value()); }();
server_->onGet("/", getHandler_.AsStdFunction());
server_->onPost("/", postHandler_.AsStdFunction());
server_->onWs(wsHandler_.AsStdFunction());
}
uint32_t const serverPort_ = tests::util::generateFreePort();
util::Config const config_{
boost::json::object{{"server", boost::json::object{{"ip", "127.0.0.1"}, {"port", serverPort_}}}}
};
std::expected<Server, std::string> server_ = make_Server(config_, ctx);
std::string requestMessage_ = "some request";
std::string const headerName_ = "Some-header";
std::string const headerValue_ = "some value";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
getHandler_;
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
postHandler_;
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandler_;
};
TEST_F(ServerTest, BadEndpoint)
{
boost::asio::ip::tcp::endpoint endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0};
impl::ConnectionHandler connectionHandler{impl::ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt};
util::TagDecoratorFactory tagDecoratorFactory{util::Config{boost::json::value{}}};
Server server{ctx, endpoint, std::nullopt, std::move(connectionHandler), tagDecoratorFactory};
auto maybeError = server.run();
ASSERT_TRUE(maybeError.has_value());
EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
}
struct ServerHttpTestBundle {
std::string testName;
http::verb method;
Request::Method
expectedMethod() const
{
switch (method) {
case http::verb::get:
return Request::Method::Get;
case http::verb::post:
return Request::Method::Post;
default:
return Request::Method::Unsupported;
}
}
};
struct ServerHttpTest : ServerTest, testing::WithParamInterface<ServerHttpTestBundle> {};
TEST_F(ServerHttpTest, ClientDisconnects)
{
HttpAsyncClient client{ctx};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.disconnect();
ctx.stop();
});
server_->run();
runContext();
}
TEST_P(ServerHttpTest, RequestResponse)
{
HttpAsyncClient client{ctx};
http::request<http::string_body> request{GetParam().method, "/", 11, requestMessage_};
request.set(headerName_, headerValue_);
Response const response{http::status::ok, "some response", Request{request}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
maybeError = client.send(request, yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
EXPECT_EQ(expectedResponse->result(), http::status::ok);
EXPECT_EQ(expectedResponse->body(), response.message());
}
client.gracefulShutdown();
ctx.stop();
});
auto& handler = GetParam().method == http::verb::get ? getHandler_ : postHandler_;
EXPECT_CALL(handler, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
EXPECT_TRUE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod());
EXPECT_EQ(receivedRequest.message(), request.body());
EXPECT_EQ(receivedRequest.target(), request.target());
EXPECT_EQ(receivedRequest.headerValue(headerName_), request.at(headerName_));
return response;
});
server_->run();
runContext();
}
INSTANTIATE_TEST_SUITE_P(
ServerHttpTests,
ServerHttpTest,
testing::Values(ServerHttpTestBundle{"GET", http::verb::get}, ServerHttpTestBundle{"POST", http::verb::post}),
tests::util::NameGenerator
);
TEST_F(ServerTest, WsClientDisconnects)
{
WebSocketAsyncClient client{ctx};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.close();
ctx.stop();
});
server_->run();
runContext();
}
TEST_F(ServerTest, WsRequestResponse)
{
WebSocketAsyncClient client{ctx};
Response const response{http::status::ok, "some response", Request{requestMessage_, Request::HttpHeaders{}}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
maybeError = client.send(yield, requestMessage_, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
EXPECT_EQ(expectedResponse.value(), response.message());
}
client.gracefulClose(yield, std::chrono::milliseconds{100});
ctx.stop();
});
EXPECT_CALL(wsHandler_, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
EXPECT_FALSE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket);
EXPECT_EQ(receivedRequest.message(), requestMessage_);
EXPECT_EQ(receivedRequest.target(), std::nullopt);
return response;
});
server_->run();
runContext();
}

View File

@@ -0,0 +1,453 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/Taggable.hpp"
#include "util/UnsupportedType.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/ConnectionHandler.hpp"
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/websocket/error.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <concepts>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
using testing::Return;
namespace beast = boost::beast;
namespace http = boost::beast::http;
namespace websocket = boost::beast::websocket;
struct ConnectionHandlerTest : SyncAsioContextTest {
ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy policy, std::optional<size_t> maxParallelConnections)
: connectionHandler_{policy, maxParallelConnections}
{
}
template <typename BoostErrorType>
static std::unexpected<Error>
makeError(BoostErrorType error)
{
if constexpr (std::same_as<BoostErrorType, http::error>) {
return std::unexpected{http::make_error_code(error)};
} else if constexpr (std::same_as<BoostErrorType, websocket::error>) {
return std::unexpected{websocket::make_error_code(error)};
} else if constexpr (std::same_as<BoostErrorType, boost::asio::error::basic_errors> ||
std::same_as<BoostErrorType, boost::asio::error::misc_errors> ||
std::same_as<BoostErrorType, boost::asio::error::addrinfo_errors> ||
std::same_as<BoostErrorType, boost::asio::error::netdb_errors>) {
return std::unexpected{boost::asio::error::make_error_code(error)};
} else {
static_assert(util::Unsupported<BoostErrorType>, "Wrong error type");
}
}
template <typename... Args>
static std::expected<Request, Error>
makeRequest(Args&&... args)
{
return Request{std::forward<Args>(args)...};
}
ConnectionHandler connectionHandler_;
util::TagDecoratorFactory tagDecoratorFactory_{util::Config(boost::json::object{{"log_tag_style", "uint"}})};
StrictMockConnectionPtr mockConnection_ =
std::make_unique<StrictMockConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
};
struct ConnectionHandlerSequentialProcessingTest : ConnectionHandlerTest {
ConnectionHandlerSequentialProcessingTest()
: ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt)
{
}
};
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
EXPECT_CALL(*mockConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_NoHandler_Send)
{
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest("some_request", Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), "WebSocket is not supported by this server");
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
{
std::string const target = "/some/target";
std::string const requestMessage = "some message";
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), "Bad target");
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
EXPECT_EQ(httpResponse.version(), 11);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadMethod_Send)
{
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::acl, "/", 11})))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), "Unsupported http method");
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
postHandlerMock;
connectionHandler_.onPost(target, postHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest =
Return(makeRequest(http::request<http::string_body>{http::verb::post, target, 11, requestMessage}));
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(http::error::partial_message)));
EXPECT_CALL(postHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
getHandlerMock;
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
connectionHandler_.onGet(target, getHandlerMock.AsStdFunction());
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})));
EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return makeError(http::error::end_of_stream).error();
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
bool connectionClosed = false;
EXPECT_CALL(*mockConnection_, receive)
.Times(4)
.WillRepeatedly([&](auto&&, auto&&) -> std::expected<Request, Error> {
if (connectionClosed) {
return makeError(websocket::error::closed);
}
return makeRequest(requestMessage, Request::HttpHeaders{});
});
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
size_t numCalls = 0;
EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
++numCalls;
if (numCalls == 3)
connectionHandler_.stop();
return std::nullopt;
});
EXPECT_CALL(*mockConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
static size_t constexpr maxParallelRequests = 3;
ConnectionHandlerParallelProcessingTest()
: ConnectionHandlerTest(
ConnectionHandler::ProcessingPolicy::Parallel,
ConnectionHandlerParallelProcessingTest::maxParallelRequests
)
{
}
static void
asyncSleep(boost::asio::yield_context yield, std::chrono::steady_clock::duration duration)
{
boost::asio::steady_timer timer{yield.get_executor()};
timer.expires_after(duration);
timer.async_wait(yield);
}
};
TEST_F(ConnectionHandlerParallelProcessingTest, ReceiveError)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).Times(2).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooManyRequest)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
testing::Sequence sequence;
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call)
.Times(3)
.WillRepeatedly([&](Request const& request, auto&&, boost::asio::yield_context yield) {
EXPECT_EQ(request.message(), requestMessage);
asyncSleep(yield, std::chrono::milliseconds{3});
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(
*mockConnection_,
send(
testing::ResultOf([](Response response) { return response.message(); }, responseMessage),
testing::_,
testing::_
)
)
.Times(3)
.WillRepeatedly(Return(std::nullopt));
EXPECT_CALL(
*mockConnection_,
send(
testing::ResultOf(
[](Response response) { return response.message(); }, "Too many requests for one connection"
),
testing::_,
testing::_
)
)
.Times(2)
.WillRepeatedly(Return(std::nullopt));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}

View File

@@ -0,0 +1,296 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/Taggable.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestHttpServer.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include <boost/asio/error.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <optional>
#include <ranges>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
namespace http = boost::beast::http;
struct HttpConnectionTests : SyncAsioContextTest {
util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}};
TestHttpServer httpServer_{ctx, "localhost"};
HttpAsyncClient httpClient_{ctx};
http::request<http::string_body> request_{http::verb::post, "/some_target", 11, "some data"};
PlainHttpConnection
acceptConnection(boost::asio::yield_context yield)
{
auto expectedSocket = httpServer_.accept(yield);
[&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }();
auto ip = expectedSocket->remote_endpoint().address().to_string();
return PlainHttpConnection{
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
};
}
};
TEST_F(HttpConnectionTests, wasUpgraded)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_FALSE(connection.wasUpgraded());
});
}
TEST_F(HttpConnectionTests, Receive)
{
request_.set(boost::beast::http::field::user_agent, "test_client");
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip();
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100});
ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message();
ASSERT_TRUE(expectedRequest->isHttp());
auto const& receivedRequest = expectedRequest.value().asHttpRequest()->get();
EXPECT_EQ(receivedRequest.method(), request_.method());
EXPECT_EQ(receivedRequest.target(), request_.target());
EXPECT_EQ(receivedRequest.body(), request_.body());
EXPECT_EQ(
receivedRequest.at(boost::beast::http::field::user_agent),
request_.at(boost::beast::http::field::user_agent)
);
});
}
TEST_F(HttpConnectionTests, ReceiveTimeout)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(expectedRequest.has_value());
});
}
TEST_F(HttpConnectionTests, ReceiveClientDisconnected)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
httpClient_.disconnect();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(expectedRequest.has_value());
});
}
TEST_F(HttpConnectionTests, Send)
{
Request const request{request_};
Response const response{http::status::ok, "some response data", request};
boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }();
auto const receivedResponse = expectedResponse.value();
auto const sentResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(receivedResponse.result(), sentResponse.result());
EXPECT_EQ(receivedResponse.body(), sentResponse.body());
EXPECT_EQ(receivedResponse.version(), request_.version());
EXPECT_TRUE(receivedResponse.keep_alive());
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
}
TEST_F(HttpConnectionTests, SendMultipleTimes)
{
Request const request{request_};
Response const response{http::status::ok, "some response data", request};
boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }();
auto const receivedResponse = expectedResponse.value();
auto const sentResponse = Response{response}.intoHttpResponse();
EXPECT_EQ(receivedResponse.result(), sentResponse.result());
EXPECT_EQ(receivedResponse.body(), sentResponse.body());
EXPECT_EQ(receivedResponse.version(), request_.version());
EXPECT_TRUE(receivedResponse.keep_alive());
}
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
}
});
}
TEST_F(HttpConnectionTests, SendClientDisconnected)
{
Response const response{http::status::ok, "some response data", Request{request_}};
boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
httpClient_.disconnect();
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{1});
size_t counter{1};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = connection.send(response, yield, std::chrono::milliseconds{1});
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
}
TEST_F(HttpConnectionTests, Close)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
size_t counter{0};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{1});
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.close(yield, std::chrono::milliseconds{1});
});
}
TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{1});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(result.has_value()) << result.error().message(); }();
EXPECT_FALSE(result.value());
});
}
TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(result.has_value());
});
}
TEST_F(HttpConnectionTests, Upgrade)
{
WebSocketAsyncClient wsClient_{ctx};
boost::asio::spawn(ctx, [this, &wsClient_](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto const expectedResult = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResult.has_value()) << expectedResult.error().message(); }();
[&]() { ASSERT_TRUE(expectedResult.value()); }();
std::optional<boost::asio::ssl::context> sslContext;
auto expectedWsConnection = connection.upgrade(sslContext, tagDecoratorFactory_, yield);
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
});
}

View File

@@ -0,0 +1,181 @@
//------------------------------------------------------------------------------
/*
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/NameGenerator.hpp"
#include "util/TmpFile.hpp"
#include "util/config/Config.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/compile.h>
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <test_data/SslCert.hpp>
#include <optional>
#include <string>
using namespace web::ng::impl;
struct MakeServerSslContextFromConfigTestBundle {
std::string testName;
std::optional<std::string> certFile;
std::optional<std::string> keyFile;
std::optional<std::string> expectedError;
bool expectContext;
boost::json::value
configJson() const
{
boost::json::object result;
if (certFile.has_value()) {
result["ssl_cert_file"] = *certFile;
}
if (keyFile.has_value()) {
result["ssl_key_file"] = *keyFile;
}
return result;
}
};
struct MakeServerSslContextFromConfigTest : testing::TestWithParam<MakeServerSslContextFromConfigTestBundle> {};
TEST_P(MakeServerSslContextFromConfigTest, makeFromConfig)
{
auto const config = util::Config{GetParam().configJson()};
auto const expectedServerSslContext = makeServerSslContext(config);
if (GetParam().expectedError.has_value()) {
ASSERT_FALSE(expectedServerSslContext.has_value());
EXPECT_THAT(expectedServerSslContext.error(), testing::HasSubstr(*GetParam().expectedError));
} else {
EXPECT_EQ(expectedServerSslContext.value().has_value(), GetParam().expectContext);
}
}
INSTANTIATE_TEST_SUITE_P(
MakeServerSslContextFromConfigTest,
MakeServerSslContextFromConfigTest,
testing::ValuesIn(
{MakeServerSslContextFromConfigTestBundle{
.testName = "NoCertNoKey",
.certFile = std::nullopt,
.keyFile = std::nullopt,
.expectedError = std::nullopt,
.expectContext = false
},
MakeServerSslContextFromConfigTestBundle{
.testName = "CertOnly",
.certFile = "some_path",
.keyFile = std::nullopt,
.expectedError = "Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together.",
.expectContext = false
},
MakeServerSslContextFromConfigTestBundle{
.testName = "KeyOnly",
.certFile = std::nullopt,
.keyFile = "some_path",
.expectedError = "Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together.",
.expectContext = false
},
MakeServerSslContextFromConfigTestBundle{
.testName = "BothKeyAndCert",
.certFile = "some_path",
.keyFile = "some_other_path",
.expectedError = "Can't read SSL certificate",
.expectContext = false
}}
),
tests::util::NameGenerator
);
struct MakeServerSslContextFromConfigRealFilesTest : testing::Test {};
TEST_F(MakeServerSslContextFromConfigRealFilesTest, WrongKeyFile)
{
auto const certFile = tests::sslCertFile();
boost::json::object configJson = {{"ssl_cert_file", certFile.path}, {"ssl_key_file", "some_path"}};
util::Config const config{configJson};
auto const expectedServerSslContext = makeServerSslContext(config);
ASSERT_FALSE(expectedServerSslContext.has_value());
EXPECT_THAT(expectedServerSslContext.error(), testing::HasSubstr("Can't read SSL key"));
}
TEST_F(MakeServerSslContextFromConfigRealFilesTest, BothFilesValid)
{
auto const certFile = tests::sslCertFile();
auto const keyFile = tests::sslKeyFile();
boost::json::object configJson = {{"ssl_cert_file", certFile.path}, {"ssl_key_file", keyFile.path}};
util::Config const config{configJson};
auto const expectedServerSslContext = makeServerSslContext(config);
EXPECT_TRUE(expectedServerSslContext.has_value());
}
struct MakeServerSslContextFromDataTestBundle {
std::string testName;
std::string certData;
std::string keyData;
bool expectedSuccess;
};
struct MakeServerSslContextFromDataTest : testing::TestWithParam<MakeServerSslContextFromDataTestBundle> {};
TEST_P(MakeServerSslContextFromDataTest, makeFromData)
{
auto const& data = GetParam();
auto const expectedServerSslContext = makeServerSslContext(data.certData, data.keyData);
EXPECT_EQ(expectedServerSslContext.has_value(), data.expectedSuccess);
}
INSTANTIATE_TEST_SUITE_P(
MakeServerSslContextFromDataTest,
MakeServerSslContextFromDataTest,
testing::ValuesIn(
{MakeServerSslContextFromDataTestBundle{
.testName = "EmptyData",
.certData = "",
.keyData = "",
.expectedSuccess = false
},
MakeServerSslContextFromDataTestBundle{
.testName = "CertOnly",
.certData = std::string{tests::sslCert()},
.keyData = "",
.expectedSuccess = false
},
MakeServerSslContextFromDataTestBundle{
.testName = "KeyOnly",
.certData = "",
.keyData = std::string{tests::sslKey()},
.expectedSuccess = false
},
MakeServerSslContextFromDataTestBundle{
.testName = "BothKeyAndCert",
.certData = std::string{tests::sslCert()},
.keyData = std::string{tests::sslKey()},
.expectedSuccess = true
}}
),
tests::util::NameGenerator
);

View File

@@ -0,0 +1,250 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/Taggable.hpp"
#include "util/TestHttpServer.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/websocket/error.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <memory>
#include <optional>
#include <ranges>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
struct web_WsConnectionTests : SyncAsioContextTest {
util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}};
TestHttpServer httpServer_{ctx, "localhost"};
WebSocketAsyncClient wsClient_{ctx};
Request request_{"some request", Request::HttpHeaders{}};
std::unique_ptr<PlainWsConnection>
acceptConnection(boost::asio::yield_context yield)
{
auto expectedSocket = httpServer_.accept(yield);
[&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }();
auto ip = expectedSocket->remote_endpoint().address().to_string();
PlainHttpConnection httpConnection{
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
};
auto expectedTrue = httpConnection.isUpgradeRequested(yield);
[&]() {
ASSERT_TRUE(expectedTrue.has_value()) << expectedTrue.error().message();
ASSERT_TRUE(expectedTrue.value()) << "Expected upgrade request";
}();
std::optional<boost::asio::ssl::context> sslContext;
auto expectedWsConnection = httpConnection.upgrade(sslContext, tagDecoratorFactory_, yield);
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
auto connection = std::move(expectedWsConnection).value();
auto wsConnectionPtr = dynamic_cast<PlainWsConnection*>(connection.release());
[&]() { ASSERT_NE(wsConnectionPtr, nullptr) << "Expected PlainWsConnection"; }();
return std::unique_ptr<PlainWsConnection>{wsConnectionPtr};
}
};
TEST_F(web_WsConnectionTests, WasUpgraded)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
EXPECT_TRUE(wsConnection->wasUpgraded());
});
}
TEST_F(web_WsConnectionTests, Send)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
boost::asio::spawn(ctx, [this, &response](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
auto const expectedMessage = wsClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedMessage.has_value()) << expectedMessage.error().message(); }();
EXPECT_EQ(expectedMessage.value(), response.message());
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
}
TEST_F(web_WsConnectionTests, MultipleSend)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
boost::asio::spawn(ctx, [this, &response](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto const expectedMessage = wsClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedMessage.has_value()) << expectedMessage.error().message(); }();
EXPECT_EQ(expectedMessage.value(), response.message());
}
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
}
});
}
TEST_F(web_WsConnectionTests, SendFailed)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
wsClient_.close();
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
std::optional<Error> maybeError;
size_t counter = 0;
while (not maybeError.has_value() and counter < 100) {
maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{1});
++counter;
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
}
TEST_F(web_WsConnectionTests, Receive)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
maybeError = wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }();
EXPECT_EQ(maybeRequest->message(), request_.message());
});
}
TEST_F(web_WsConnectionTests, MultipleReceive)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
maybeError = wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
}
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }();
EXPECT_EQ(maybeRequest->message(), request_.message());
}
});
}
TEST_F(web_WsConnectionTests, ReceiveTimeout)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(maybeRequest.has_value());
EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::timed_out);
});
}
TEST_F(web_WsConnectionTests, ReceiveFailed)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
wsClient_.close();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeRequest.has_value());
EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::eof);
});
}
TEST_F(web_WsConnectionTests, Close)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
auto const maybeMessage = wsClient_.receive(yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeMessage.has_value());
EXPECT_THAT(maybeMessage.error().message(), testing::HasSubstr("was gracefully closed"));
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
wsConnection->close(yield, std::chrono::milliseconds{100});
});
}