mirror of
https://github.com/XRPLF/clio.git
synced 2026-04-29 15:37:53 +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:
@@ -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/Server.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
namespace web::ng {
|
||||
|
||||
std::expected<std::optional<boost::asio::ssl::context>, std::string>
|
||||
makeServerSslContext(util::Config const& config)
|
||||
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)}
|
||||
{
|
||||
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 keyFilename = config.value<std::string>("ssl_key_file");
|
||||
|
||||
return impl::makeServerSslContext(certFilename, keyFilename);
|
||||
}
|
||||
} // namespace web
|
||||
|
||||
ConnectionContext
|
||||
Connection::context() const
|
||||
{
|
||||
return ConnectionContext{*this};
|
||||
}
|
||||
|
||||
std::string const&
|
||||
Connection::ip() const
|
||||
{
|
||||
return ip_;
|
||||
}
|
||||
|
||||
ConnectionContext::ConnectionContext(Connection const& connection) : connection_{connection}
|
||||
{
|
||||
}
|
||||
|
||||
} // 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
|
||||
Reference in New Issue
Block a user