mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-04 20:05:51 +00:00
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:
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
76
src/util/CoroutineGroup.cpp
Normal file
76
src/util/CoroutineGroup.cpp
Normal 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
|
||||
88
src/util/CoroutineGroup.hpp
Normal file
88
src/util/CoroutineGroup.hpp
Normal 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
71
src/util/WithTimeout.hpp
Normal 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
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
148
src/web/ng/Connection.hpp
Normal 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
31
src/web/ng/Error.hpp
Normal 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
|
||||
37
src/web/ng/MessageHandler.hpp
Normal file
37
src/web/ng/MessageHandler.hpp
Normal 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
131
src/web/ng/Request.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#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
145
src/web/ng/Request.hpp
Normal 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
116
src/web/ng/Response.cpp
Normal 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
106
src/web/ng/Response.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
35
src/web/ng/impl/Concepts.hpp
Normal file
35
src/web/ng/impl/Concepts.hpp
Normal 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
|
||||
285
src/web/ng/impl/ConnectionHandler.cpp
Normal file
285
src/web/ng/impl/ConnectionHandler.cpp
Normal 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
|
||||
130
src/web/ng/impl/ConnectionHandler.hpp
Normal file
130
src/web/ng/impl/ConnectionHandler.hpp
Normal 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
|
||||
219
src/web/ng/impl/HttpConnection.hpp
Normal file
219
src/web/ng/impl/HttpConnection.hpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
77
src/web/ng/impl/WsConnection.cpp
Normal file
77
src/web/ng/impl/WsConnection.cpp
Normal 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
|
||||
178
src/web/ng/impl/WsConnection.hpp
Normal file
178
src/web/ng/impl/WsConnection.hpp
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
250
tests/common/util/TestHttpClient.cpp
Normal file
250
tests/common/util/TestHttpClient.cpp
Normal 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);
|
||||
}
|
||||
99
tests/common/util/TestHttpClient.hpp
Normal file
99
tests/common/util/TestHttpClient.hpp
Normal 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();
|
||||
};
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
225
tests/common/util/TestWebSocketClient.cpp
Normal file
225
tests/common/util/TestWebSocketClient.cpp
Normal 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();
|
||||
}
|
||||
94
tests/common/util/TestWebSocketClient.hpp
Normal file
94
tests/common/util/TestWebSocketClient.hpp
Normal 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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
62
tests/common/web/ng/MockConnection.hpp
Normal file
62
tests/common/web/ng/MockConnection.hpp
Normal 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>>;
|
||||
@@ -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)
|
||||
|
||||
|
||||
105
tests/unit/test_data/SslCert.cpp
Normal file
105
tests/unit/test_data/SslCert.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
167
tests/unit/util/CoroutineGroupTests.cpp
Normal file
167
tests/unit/util/CoroutineGroupTests.cpp
Normal 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();
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
77
tests/unit/util/WithTimeout.cpp
Normal file
77
tests/unit/util/WithTimeout.cpp
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
"",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
);
|
||||
224
tests/unit/web/ng/RequestTests.cpp
Normal file
224
tests/unit/web/ng/RequestTests.cpp
Normal 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);
|
||||
}
|
||||
126
tests/unit/web/ng/ResponseTests.cpp
Normal file
126
tests/unit/web/ng/ResponseTests.cpp
Normal 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));
|
||||
}
|
||||
332
tests/unit/web/ng/ServerTests.cpp
Normal file
332
tests/unit/web/ng/ServerTests.cpp
Normal 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();
|
||||
}
|
||||
453
tests/unit/web/ng/impl/ConnectionHandlerTests.cpp
Normal file
453
tests/unit/web/ng/impl/ConnectionHandlerTests.cpp
Normal 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);
|
||||
});
|
||||
}
|
||||
296
tests/unit/web/ng/impl/HttpConnectionTests.cpp
Normal file
296
tests/unit/web/ng/impl/HttpConnectionTests.cpp
Normal 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(); }();
|
||||
});
|
||||
}
|
||||
181
tests/unit/web/ng/impl/ServerSslContextTests.cpp
Normal file
181
tests/unit/web/ng/impl/ServerSslContextTests.cpp
Normal 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
|
||||
);
|
||||
250
tests/unit/web/ng/impl/WsConnectionTests.cpp
Normal file
250
tests/unit/web/ng/impl/WsConnectionTests.cpp
Normal 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});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user