mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55: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