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

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

View File

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

View File

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

View File

@@ -0,0 +1,250 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/TestHttpClient.hpp"
#include "util/Assert.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/stream_traits.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
namespace {
std::string
syncRequest(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders,
http::verb method,
std::string target = "/"
)
{
boost::asio::io_context ioc;
net::ip::tcp::resolver resolver(ioc);
boost::beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{method, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
for (auto& header : additionalHeaders) {
req.set(header.name, header.value);
}
req.target(target);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
}
} // namespace
WebHeader::WebHeader(http::field name, std::string value) : name(name), value(std::move(value))
{
}
std::string
HttpSyncClient::post(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
}
std::string
HttpSyncClient::get(
std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target);
}
bool
HttpsSyncClient::verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */)
{
return true;
}
std::string
HttpsSyncClient::syncPost(std::string const& host, std::string const& port, std::string const& body)
{
net::io_context ioc;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
// We can't fix this so have to ignore
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
#pragma GCC diagnostic pop
{
boost::beast::error_code const ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
throw boost::beast::system_error{ec};
}
auto const results = resolver.resolve(host, port);
boost::beast::get_lowest_layer(stream).connect(results);
stream.handshake(ssl::stream_base::client);
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.shutdown(ec);
return res.body();
}
HttpAsyncClient::HttpAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext}
{
}
std::optional<boost::system::error_code>
HttpAsyncClient::connect(
std::string_view host,
std::string_view port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
)
{
boost::system::error_code error;
boost::asio::ip::tcp::resolver resolver{stream_.get_executor()};
auto const resolverResults = resolver.resolve(host, port, error);
if (error)
return error;
ASSERT(resolverResults.size() > 0, "No results from resolver");
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.async_connect(resolverResults.begin()->endpoint(), yield[error]);
if (error)
return error;
return std::nullopt;
}
std::optional<boost::system::error_code>
HttpAsyncClient::send(
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
)
{
request.prepare_payload();
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
http::async_write(stream_, request, yield[error]);
if (error)
return error;
return std::nullopt;
}
std::expected<boost::beast::http::response<boost::beast::http::string_body>, boost::system::error_code>
HttpAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
http::response<http::string_body> response;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
http::async_read(stream_, buffer_, response, yield[error]);
if (error)
return std::unexpected{error};
return response;
}
void
HttpAsyncClient::gracefulShutdown()
{
boost::system::error_code error;
stream_.socket().shutdown(tcp::socket::shutdown_both, error);
}
void
HttpAsyncClient::disconnect()
{
boost::system::error_code error;
stream_.socket().close(error);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,225 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/TestWebSocketClient.hpp"
#include "util/Assert.hpp"
#include "util/TestHttpClient.hpp"
#include "util/WithTimeout.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/stream_traits.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <fmt/core.h>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
void
WebSocketSyncClient::connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders)
{
auto const results = resolver_.resolve(host, port);
auto const ep = net::connect(ws_.next_layer(), results);
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
auto const hostPort = host + ':' + std::to_string(ep.port());
ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
ws_.handshake(hostPort, "/");
}
void
WebSocketSyncClient::disconnect()
{
ws_.close(boost::beast::websocket::close_code::normal);
}
std::string
WebSocketSyncClient::syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_.write(net::buffer(std::string(body)));
ws_.read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
void
WebServerSslSyncClient::connect(std::string const& host, std::string const& port)
{
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver{ioc_};
ws_.emplace(ioc_, ctx);
auto const results = resolver.resolve(host, port);
net::connect(ws_->next_layer().next_layer(), results.begin(), results.end());
ws_->next_layer().handshake(ssl::stream_base::client);
ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws_->handshake(host, "/");
}
void
WebServerSslSyncClient::disconnect()
{
ws_->close(boost::beast::websocket::close_code::normal);
}
std::string
WebServerSslSyncClient::syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_->write(net::buffer(std::string(body)));
ws_->read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
WebSocketAsyncClient::WebSocketAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext}
{
}
std::optional<boost::system::error_code>
WebSocketAsyncClient::connect(
std::string const& host,
std::string const& port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout,
std::vector<WebHeader> additionalHeaders
)
{
auto const results = boost::asio::ip::tcp::resolver{yield.get_executor()}.resolve(host, port);
ASSERT(not results.empty(), "Could not resolve {}:{}", host, port);
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.next_layer().async_connect(results, yield[error]);
if (error)
return error;
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
boost::beast::get_lowest_layer(stream_).expires_never();
stream_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
stream_.async_handshake(fmt::format("{}:{}", host, port), "/", yield[error]);
if (error)
return error;
return std::nullopt;
}
std::optional<boost::system::error_code>
WebSocketAsyncClient::send(
boost::asio::yield_context yield,
std::string_view message,
std::chrono::steady_clock::duration timeout
)
{
auto const error = util::withTimeout(
[this, &message](auto&& cyield) { stream_.async_write(net::buffer(message), cyield); }, yield, timeout
);
if (error)
return error;
return std::nullopt;
}
std::expected<std::string, boost::system::error_code>
WebSocketAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::flat_buffer buffer{};
auto error =
util::withTimeout([this, &buffer](auto&& cyield) { stream_.async_read(buffer, cyield); }, yield, timeout);
if (error)
return std::unexpected{error};
return boost::beast::buffers_to_string(buffer.data());
}
void
WebSocketAsyncClient::gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
stream_.async_close(boost::beast::websocket::close_code::normal, yield);
}
void
WebSocketAsyncClient::close()
{
boost::beast::get_lowest_layer(stream_).close();
}

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
struct MockConnectionImpl : web::ng::Connection {
using web::ng::Connection::Connection;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(
SendReturnType,
send,
(web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
MOCK_METHOD(
ReceiveReturnType,
receive,
(boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration));
};
using MockConnection = testing::NiceMock<MockConnectionImpl>;
using MockConnectionPtr = std::unique_ptr<testing::NiceMock<MockConnectionImpl>>;
using StrictMockConnection = testing::StrictMock<MockConnectionImpl>;
using StrictMockConnectionPtr = std::unique_ptr<testing::StrictMock<MockConnectionImpl>>;