refactor: Coroutine based webserver (#1699)

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

View File

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

View File

@@ -0,0 +1,76 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/CoroutineGroup.hpp"
#include "util/Assert.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <cstddef>
#include <functional>
#include <optional>
#include <utility>
namespace util {
CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren)
: timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren}
{
}
CoroutineGroup::~CoroutineGroup()
{
ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish");
}
bool
CoroutineGroup::spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn)
{
if (maxChildren_.has_value() && childrenCounter_ >= *maxChildren_)
return false;
++childrenCounter_;
boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) {
fn(yield);
--childrenCounter_;
if (childrenCounter_ == 0)
timer_.cancel();
});
return true;
}
void
CoroutineGroup::asyncWait(boost::asio::yield_context yield)
{
if (childrenCounter_ == 0)
return;
boost::system::error_code error;
timer_.async_wait(yield[error]);
}
size_t
CoroutineGroup::size() const
{
return childrenCounter_;
}
} // namespace util

View File

@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <cstddef>
#include <functional>
#include <optional>
namespace util {
/**
* @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and
* wait for all of them to finish.
*/
class CoroutineGroup {
boost::asio::steady_timer timer_;
std::optional<int> maxChildren_;
int childrenCounter_{0};
public:
/**
* @brief Construct a new Coroutine Group object
*
* @param yield The yield context to use for the internal timer
* @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there
* is no limit
*/
CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren = std::nullopt);
/**
* @brief Destroy the Coroutine Group object
*
* @note asyncWait() must be called before the object is destroyed
*/
~CoroutineGroup();
/**
* @brief Spawn a new coroutine in the group
*
* @param yield The yield context to use for the coroutine (it should be the same as the one used in the
* constructor)
* @param fn The function to execute
* @return true If the coroutine was spawned successfully. false if the maximum number of coroutines has been
* reached
*/
bool
spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn);
/**
* @brief Wait for all the coroutines in the group to finish
*
* @note This method must be called before the object is destroyed
*
* @param yield The yield context to use for the internal timer
*/
void
asyncWait(boost::asio::yield_context yield);
/**
* @brief Get the number of coroutines in the group
*
* @return size_t The number of coroutines in the group
*/
size_t
size() const;
};
} // namespace util

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

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/system/errc.hpp>
#include <chrono>
#include <ctime>
#include <memory>
namespace util {
/**
* @brief Perform a coroutine operation with a timeout.
*
* @tparam Operation The operation type to perform. Must be a callable accepting yield context with bound cancellation
* token.
* @param operation The operation to perform.
* @param yield The yield context.
* @param timeout The timeout duration.
* @return The error code of the operation.
*/
template <typename Operation>
boost::system::error_code
withTimeout(Operation&& operation, boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
auto operationCompleted = std::make_shared<bool>(false);
boost::asio::cancellation_signal cancellationSignal;
auto cyield = boost::asio::bind_cancellation_slot(cancellationSignal.slot(), yield[error]);
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield), timeout};
timer.async_wait([&cancellationSignal, operationCompleted](boost::system::error_code errorCode) {
if (!errorCode and !*operationCompleted)
cancellationSignal.emit(boost::asio::cancellation_type::terminal);
});
operation(cyield);
*operationCompleted = true;
// Map error code to timeout
if (error == boost::system::errc::operation_canceled) {
return boost::system::errc::make_error_code(boost::system::errc::timed_out);
}
return error;
}
} // namespace util

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,32 +17,41 @@
*/
//==============================================================================
#include "web/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
View File

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

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

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

View File

@@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "web/ng/Connection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <functional>
namespace web::ng {
/**
* @brief Handler for messages.
*/
using MessageHandler = std::function<Response(Request const&, ConnectionContext, boost::asio::yield_context)>;
} // namespace web::ng

131
src/web/ng/Request.cpp Normal file
View File

@@ -0,0 +1,131 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/ng/Request.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
namespace web::ng {
namespace {
template <typename HeadersType, typename HeaderNameType>
std::optional<std::string_view>
getHeaderValue(HeadersType const& headers, HeaderNameType const& headerName)
{
auto const it = headers.find(headerName);
if (it == headers.end())
return std::nullopt;
return it->value();
}
} // namespace
Request::Request(boost::beast::http::request<boost::beast::http::string_body> request) : data_{std::move(request)}
{
}
Request::Request(std::string request, HttpHeaders const& headers)
: data_{WsData{.request = std::move(request), .headers = headers}}
{
}
Request::Method
Request::method() const
{
if (not isHttp())
return Method::Websocket;
switch (httpRequest().method()) {
case boost::beast::http::verb::get:
return Method::Get;
case boost::beast::http::verb::post:
return Method::Post;
default:
return Method::Unsupported;
}
}
bool
Request::isHttp() const
{
return std::holds_alternative<HttpRequest>(data_);
}
std::optional<std::reference_wrapper<boost::beast::http::request<boost::beast::http::string_body> const>>
Request::asHttpRequest() const
{
if (not isHttp())
return std::nullopt;
return httpRequest();
}
std::string_view
Request::message() const
{
if (not isHttp())
return std::get<WsData>(data_).request;
return httpRequest().body();
}
std::optional<std::string_view>
Request::target() const
{
if (not isHttp())
return std::nullopt;
return httpRequest().target();
}
std::optional<std::string_view>
Request::headerValue(boost::beast::http::field headerName) const
{
if (not isHttp())
return getHeaderValue(std::get<WsData>(data_).headers.get(), headerName);
return getHeaderValue(httpRequest(), headerName);
}
std::optional<std::string_view>
Request::headerValue(std::string const& headerName) const
{
if (not isHttp())
return getHeaderValue(std::get<WsData>(data_).headers.get(), headerName);
return getHeaderValue(httpRequest(), headerName);
}
Request::HttpRequest const&
Request::httpRequest() const
{
return std::get<HttpRequest>(data_);
}
} // namespace web::ng

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

@@ -0,0 +1,145 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace web::ng {
/**
* @brief Represents an HTTP or WebSocket request.
*/
class Request {
public:
/**
* @brief The headers of an HTTP request.
*/
using HttpHeaders = boost::beast::http::request<boost::beast::http::string_body>::header_type;
private:
struct WsData {
std::string request;
std::reference_wrapper<HttpHeaders const> headers;
};
using HttpRequest = boost::beast::http::request<boost::beast::http::string_body>;
std::variant<HttpRequest, WsData> data_;
public:
/**
* @brief Construct from an HTTP request.
*
* @param request The HTTP request.
*/
explicit Request(boost::beast::http::request<boost::beast::http::string_body> request);
/**
* @brief Construct from a WebSocket request.
*
* @param request The WebSocket request.
* @param headers The headers of the HTTP request initiated the WebSocket connection
*/
Request(std::string request, HttpHeaders const& headers);
/**
* @brief Method of the request.
* @note Websocket is not a real method, it is used to distinguish WebSocket requests from HTTP requests.
*/
enum class Method { Get, Post, Websocket, Unsupported };
/**
* @brief Get the method of the request.
*
* @return The method of the request.
*/
Method
method() const;
/**
* @brief Check if the request is an HTTP request.
*
* @return true if the request is an HTTP request, false otherwise.
*/
bool
isHttp() const;
/**
* @brief Get the HTTP request.
*
* @return The HTTP request or std::nullopt if the request is a WebSocket request.
*/
std::optional<std::reference_wrapper<boost::beast::http::request<boost::beast::http::string_body> const>>
asHttpRequest() const;
/**
* @brief Get the body (in case of an HTTP request) or the message (in case of a WebSocket request).
*
* @return The message of the request.
*/
std::string_view
message() const;
/**
* @brief Get the target of the request.
*
* @return The target of the request or std::nullopt if the request is a WebSocket request.
*/
std::optional<std::string_view>
target() const;
/**
* @brief Get the value of a header.
*
* @param headerName The name of the header.
* @return The value of the header or std::nullopt if the header does not exist.
*/
std::optional<std::string_view>
headerValue(boost::beast::http::field headerName) const;
/**
* @brief Get the value of a header.
*
* @param headerName The name of the header.
* @return The value of the header or std::nullopt if the header does not exist.
*/
std::optional<std::string_view>
headerValue(std::string const& headerName) const;
private:
/**
* @brief Get the HTTP request.
* @note This function assumes that the request is an HTTP request. So if data_ is not an HTTP request,
* the behavior is undefined.
*
* @return The HTTP request.
*/
HttpRequest const&
httpRequest() const;
};
} // namespace web::ng

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

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

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

@@ -0,0 +1,106 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "web/ng/Request.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/json/object.hpp>
#include <optional>
#include <string>
namespace web::ng {
/**
* @brief Represents an HTTP or Websocket response.
*/
class Response {
public:
/**
* @brief The data for an HTTP response.
*/
struct HttpData {
/**
* @brief The content type of the response.
*/
enum class ContentType { ApplicationJson, TextHtml };
boost::beast::http::status status; ///< The HTTP status.
ContentType contentType; ///< The content type.
bool keepAlive; ///< Whether the connection should be kept alive.
unsigned int version; ///< The HTTP version.
};
private:
std::string message_;
std::optional<HttpData> httpData_;
public:
/**
* @brief Construct a Response from string. Content type will be text/html.
*
* @param status The HTTP status.
* @param message The message to send.
* @param request The request that triggered this response. Used to determine whether the response should contain
* HTTP or WebSocket data.
*/
Response(boost::beast::http::status status, std::string message, Request const& request);
/**
* @brief Construct a Response from JSON object. Content type will be application/json.
*
* @param status The HTTP status.
* @param message The message to send.
* @param request The request that triggered this response. Used to determine whether the response should contain
* HTTP or WebSocket
*/
Response(boost::beast::http::status status, boost::json::object const& message, Request const& request);
/**
* @brief Get the message of the response.
*
* @return The message of the response.
*/
std::string const&
message() const;
/**
* @brief Convert the Response to an HTTP response.
* @note The Response must be constructed with an HTTP request.
*
* @return The HTTP response.
*/
boost::beast::http::response<boost::beast::http::string_body>
intoHttpResponse() &&;
/**
* @brief Get the message of the response as a const buffer.
* @note The response must be constructed with a WebSocket request.
*
* @return The message of the response as a const buffer.
*/
boost::asio::const_buffer
asConstBuffer() const&;
};
} // namespace web::ng

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/beast/core/basic_stream.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <type_traits>
namespace web::ng::impl {
template <typename T>
concept IsTcpStream = std::is_same_v<std::decay_t<T>, boost::beast::tcp_stream>;
template <typename T>
concept IsSslTcpStream = std::is_same_v<std::decay_t<T>, boost::asio::ssl::stream<boost::beast::tcp_stream>>;
} // namespace web::ng::impl

View File

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

View File

@@ -0,0 +1,130 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/log/Logger.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/MessageHandler.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <cstddef>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
namespace web::ng::impl {
class ConnectionHandler {
public:
enum class ProcessingPolicy { Sequential, Parallel };
struct StringHash {
using hash_type = std::hash<std::string_view>;
using is_transparent = void;
std::size_t
operator()(char const* str) const;
std::size_t
operator()(std::string_view str) const;
std::size_t
operator()(std::string const& str) const;
};
using TargetToHandlerMap = std::unordered_map<std::string, MessageHandler, StringHash, std::equal_to<>>;
private:
util::Logger log_{"WebServer"};
util::Logger perfLog_{"Performance"};
ProcessingPolicy processingPolicy_;
std::optional<size_t> maxParallelRequests_;
TargetToHandlerMap getHandlers_;
TargetToHandlerMap postHandlers_;
std::optional<MessageHandler> wsHandler_;
boost::signals2::signal<void()> onStop_;
public:
ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests);
void
onGet(std::string const& target, MessageHandler handler);
void
onPost(std::string const& target, MessageHandler handler);
void
onWs(MessageHandler handler);
void
processConnection(ConnectionPtr connection, boost::asio::yield_context yield);
void
stop();
private:
/**
* @brief Handle an error.
*
* @param error The error to handle.
* @param connection The connection that caused the error.
* @return True if the connection should be gracefully closed, false otherwise.
*/
bool
handleError(Error const& error, Connection const& connection) const;
/**
* @brief The request-response loop.
*
* @param connection The connection to handle.
* @param yield The yield context.
* @return True if the connection should be gracefully closed, false otherwise.
*/
bool
sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
bool
parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
std::optional<bool>
processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield);
/**
* @brief Handle a request.
*
* @param connectionContext The connection context.
* @param request The request to handle.
* @param yield The yield context.
* @return The response to send.
*/
Response
handleRequest(ConnectionContext const& connectionContext, Request const& request, boost::asio::yield_context yield);
};
} // namespace web::ng::impl

View File

@@ -0,0 +1,219 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/Assert.hpp"
#include "util/Taggable.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/Concepts.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core/basic_stream.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/websocket.hpp>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <utility>
namespace web::ng::impl {
class UpgradableConnection : public Connection {
public:
using Connection::Connection;
virtual std::expected<bool, Error>
isUpgradeRequested(
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
) = 0;
virtual std::expected<ConnectionPtr, Error>
upgrade(
std::optional<boost::asio::ssl::context>& sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
) = 0;
};
using UpgradableConnectionPtr = std::unique_ptr<UpgradableConnection>;
template <typename StreamType>
class HttpConnection : public UpgradableConnection {
StreamType stream_;
std::optional<boost::beast::http::request<boost::beast::http::string_body>> request_;
public:
HttpConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
util::TagDecoratorFactory const& tagDecoratorFactory
)
requires IsTcpStream<StreamType>
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory), stream_{std::move(socket)}
{
}
HttpConnection(
boost::asio::ip::tcp::socket socket,
std::string ip,
boost::beast::flat_buffer buffer,
boost::asio::ssl::context& sslCtx,
util::TagDecoratorFactory const& tagDecoratorFactory
)
requires IsSslTcpStream<StreamType>
: UpgradableConnection(std::move(ip), std::move(buffer), tagDecoratorFactory)
, stream_{std::move(socket), sslCtx}
{
}
bool
wasUpgraded() const override
{
return false;
}
std::optional<Error>
send(
Response response,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
) override
{
auto const httpResponse = std::move(response).intoHttpResponse();
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
boost::beast::http::async_write(stream_, httpResponse, yield[error]);
if (error)
return error;
return std::nullopt;
}
std::expected<Request, Error>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
{
if (request_.has_value()) {
Request result{std::move(request_).value()};
request_.reset();
return result;
}
auto expectedRequest = fetch(yield, timeout);
if (expectedRequest.has_value())
return Request{std::move(expectedRequest).value()};
return std::unexpected{std::move(expectedRequest).error()};
}
void
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) override
{
[[maybe_unused]] boost::system::error_code error;
if constexpr (IsSslTcpStream<StreamType>) {
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.async_shutdown(yield[error]);
}
if constexpr (IsTcpStream<StreamType>) {
stream_.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error);
} else {
boost::beast::get_lowest_layer(stream_).socket().shutdown(
boost::asio::ip::tcp::socket::shutdown_type::shutdown_both, error
);
}
}
std::expected<bool, Error>
isUpgradeRequested(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT)
override
{
auto expectedRequest = fetch(yield, timeout);
if (not expectedRequest.has_value())
return std::unexpected{std::move(expectedRequest).error()};
request_ = std::move(expectedRequest).value();
return boost::beast::websocket::is_upgrade(request_.value());
}
std::expected<ConnectionPtr, Error>
upgrade(
[[maybe_unused]] std::optional<boost::asio::ssl::context>& sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield
) override
{
ASSERT(request_.has_value(), "Request must be present to upgrade the connection");
if constexpr (IsSslTcpStream<StreamType>) {
ASSERT(sslContext.has_value(), "SSL context must be present to upgrade the connection");
return make_SslWsConnection(
boost::beast::get_lowest_layer(stream_).release_socket(),
std::move(ip_),
std::move(buffer_),
std::move(request_).value(),
sslContext.value(),
tagDecoratorFactory,
yield
);
} else {
return make_PlainWsConnection(
stream_.release_socket(),
std::move(ip_),
std::move(buffer_),
std::move(request_).value(),
tagDecoratorFactory,
yield
);
}
}
private:
std::expected<boost::beast::http::request<boost::beast::http::string_body>, Error>
fetch(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::http::request<boost::beast::http::string_body> request{};
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
boost::beast::http::async_read(stream_, buffer_, request, yield[error]);
if (error)
return std::unexpected{error};
return request;
}
};
using PlainHttpConnection = HttpConnection<boost::beast::tcp_stream>;
using SslHttpConnection = HttpConnection<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
} // namespace web::ng::impl

View File

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

View File

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

View File

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

View File

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