diff --git a/docs_source/sphinx/http/server.md b/docs_source/sphinx/http/server.md index 37c359be..17a3284a 100644 --- a/docs_source/sphinx/http/server.md +++ b/docs_source/sphinx/http/server.md @@ -1,4 +1,4 @@ -# HTTP Server +# HTTP Server ## Tutorial @@ -28,13 +28,17 @@ int main() Roar::Server server{{.executor = pool.executor()}}; // SSL Server (you can provide your own SSL Context for more options.) + // + // auto sslContext = SslServerContext{ + // .certificate = "certificate string here", // or filesystem::path to cert + // .privateKey = "private key here", // or filesystem::path to key + // .password = "passwordHereIfApplicable", + // }; + // initializeServerSslContext(sslContext); + // // Roar::Server server{{ // .executor = pool.executor() - // .sslContext = makeSslContext(SslContextCreationParameters{ - // .certificate = std::string_view{"certificate string here"}, - // .privateKey = std::string_view{"private key here"}, - // .password = keyPassphrase, - // }) + // .sslContext = std::move(sslContext), // }}; // stop the thread_pool on scope exit to guarantee that all asynchronous tasks are finished before the server is @@ -51,7 +55,7 @@ int main() std::cin.get(); } ``` -In this example we are creating an asio thread pool for the asynchronous actions of our server, +In this example we are creating an asio thread pool for the asynchronous actions of our server, the server itself and then start the server by binding and accepting on port 8081. ```{warning} Important: The Roar::ScopeExit construct ensures that the threads are shutdown before the server or request listeners are destroyed. @@ -141,7 +145,7 @@ int main() pool.stop(); pool.join(); }}; - + server.installRequestListener(); // Start server and bind on port "port". @@ -215,7 +219,7 @@ class MyRequestListener BOOST_DESCRIBE_CLASS(MyRequestListener, (), (), (), (roar_index, roar_images)) }; ``` -Available options for routes are documented here: +Available options for routes are documented here: RouteInfo. ```{admonition} Head Requests @@ -366,7 +370,7 @@ class FileServer // Allow DELETE requests to recursively delete directories? Defaults to false. .allowDeleteOfNonEmptyDirectories = false, - // If this option is set, requests to directories do not try to serve an index.html, + // If this option is set, requests to directories do not try to serve an index.html, // but give a table with all existing files instead. Defaults to true. .allowListing = true, diff --git a/include/roar/client.hpp b/include/roar/client.hpp index dcae8243..2fdfc2ad 100644 --- a/include/roar/client.hpp +++ b/include/roar/client.hpp @@ -26,6 +26,7 @@ #include #include #include +#include namespace Roar { @@ -34,21 +35,25 @@ namespace Roar public: constexpr static std::chrono::seconds defaultTimeout{10}; - struct ConstructionArguments + struct SslOptions { - /// Required io executor for boost::asio. - boost::asio::any_io_executor executor; - /// Supply for SSL support. - std::optional sslContext; + boost::asio::ssl::context sslContext; /// SSL verify mode: - boost::asio::ssl::verify_mode sslVerifyMode = boost::asio::ssl::verify_none; + boost::asio::ssl::verify_mode sslVerifyMode = boost::asio::ssl::verify_peer; /** * @brief sslVerifyCallback, you can use boost::asio::ssl::rfc2818_verification(host) most of the time. */ - std::function sslVerifyCallback = {}; + std::function sslVerifyCallback; + }; + + struct ConstructionArguments + { + /// Required io executor for boost::asio. + boost::asio::any_io_executor executor; + std::optional sslOptions = std::nullopt; }; Client(ConstructionArguments&& args); @@ -75,6 +80,54 @@ namespace Roar Detail::PromiseTypeBindFail> read(std::chrono::seconds timeout = defaultTimeout); + /** + * @brief Attach some state to the client lifetime. + * + * @param tag A tag name, to retrieve it back with. + * @param state The state. + */ + template + void attachState(std::string const& tag, T&& state) + { + attachedState_[tag] = std::forward(state); + } + + /** + * @brief Create state in place. + * + * @tparam ConstructionArgs + * @param tag + * @param args + */ + template + void emplaceState(std::string const& tag, ConstructionArgs&&... args) + { + attachedState_[tag] = std::make_any(std::forward(args)...); + } + + /** + * @brief Retrieve attached state by tag. + * + * @tparam T Type of the attached state. + * @param tag The tag of the state. + * @return T& Returns a reference to the held state. + */ + template + T& state(std::string const& tag) + { + return std::any_cast(attachedState_.at(tag)); + } + + /** + * @brief Remove attached state. + * + * @param tag The tag of the state to remove. + */ + void removeState(std::string const& tag) + { + attachedState_.erase(tag); + } + /** * @brief Connects the client to a server and performs a request * @@ -86,22 +139,6 @@ namespace Roar request(Request&& request, std::chrono::seconds timeout = defaultTimeout) { return promise::newPromise([&, this](promise::Defer d) mutable { - if (std::holds_alternative>(socket_)) - { - auto& sslSocket = std::get>(socket_); - if (!SSL_ctrl( - sslSocket.native_handle(), - SSL_CTRL_SET_TLSEXT_HOSTNAME, - TLSEXT_NAMETYPE_host_name, - // yikes openssl you make me do this - const_cast(reinterpret_cast(request.host().c_str())))) - { - boost::beast::error_code ec{ - static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()}; - return d.reject(Error{.error = ec, .additionalInfo = "SSL_set_tlsext_host_name failed."}); - } - } - const auto host = request.host(); const auto port = request.port(); doResolve( @@ -324,6 +361,10 @@ namespace Roar { if (std::holds_alternative>(socket_)) { + auto maybeError = setupSsl(request.host()); + if (maybeError) + return d.reject(*maybeError); + auto& sslSocket = std::get>(socket_); withLowerLayerDo([&](auto& socket) { socket.expires_after(timeout); @@ -371,8 +412,12 @@ namespace Roar }); } + std::optional setupSsl(std::string const& host); + private: + std::optional sslOptions_; std::variant, boost::beast::tcp_stream> socket_; boost::asio::ip::tcp::resolver::results_type::endpoint_type endpoint_; + std::unordered_map attachedState_; }; } \ No newline at end of file diff --git a/include/roar/server.hpp b/include/roar/server.hpp index b8a65eb8..eb0d8f0b 100644 --- a/include/roar/server.hpp +++ b/include/roar/server.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -37,7 +38,7 @@ namespace Roar boost::asio::any_io_executor executor; /// Supply for SSL support. - std::optional sslContext; + std::optional sslContext; /// Called when an error occurs in an asynchronous routine. std::function onError = [](auto&&) {}; diff --git a/include/roar/session/factory.hpp b/include/roar/session/factory.hpp index e5ac993c..b4ad7c74 100644 --- a/include/roar/session/factory.hpp +++ b/include/roar/session/factory.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -23,7 +24,7 @@ namespace Roar constexpr static std::chrono::seconds sslDetectionTimeout{10}; public: - Factory(std::optional& sslContext, std::function onError); + Factory(std::optional& sslContext, std::function onError); ROAR_PIMPL_SPECIAL_FUNCTIONS(Factory); /** diff --git a/include/roar/session/session.hpp b/include/roar/session/session.hpp index d5c33f84..171f3ef4 100644 --- a/include/roar/session/session.hpp +++ b/include/roar/session/session.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -49,7 +50,7 @@ namespace Roar Session( boost::asio::basic_stream_socket&& socket, boost::beast::basic_flat_buffer>&& buffer, - std::optional& sslContext, + std::optional& sslContext, bool isSecure, std::function onError, std::weak_ptr router, @@ -743,7 +744,7 @@ namespace Roar std::shared_ptr>& parser(); boost::beast::flat_buffer& buffer(); StandardResponseProvider const& standardResponseProvider(); - void startup(); + void startup(bool immediate = true); private: struct Implementation; diff --git a/include/roar/ssl/make_ssl_context.hpp b/include/roar/ssl/make_ssl_context.hpp index 8451354f..aaed972f 100644 --- a/include/roar/ssl/make_ssl_context.hpp +++ b/include/roar/ssl/make_ssl_context.hpp @@ -1,18 +1,20 @@ +#pragma once + #include -#include +#include #include #include namespace Roar { - struct SslContextCreationParameters + struct SslServerContext { - boost::asio::ssl::context::method method = boost::asio::ssl::context::tls_server; - std::variant certificate; - std::variant privateKey; - std::string_view diffieHellmanParameters = ""; - std::string_view password = ""; + boost::asio::ssl::context ctx = boost::asio::ssl::context{boost::asio::ssl::context::tls_server}; + std::variant certificate; + std::variant privateKey; + std::string diffieHellmanParameters = ""; + std::string password = ""; }; /** @@ -22,5 +24,5 @@ namespace Roar * @param settings * @return boost::asio::ssl::context */ - boost::asio::ssl::context makeSslContext(SslContextCreationParameters settings); + void initializeServerSslContext(SslServerContext& ctx); } \ No newline at end of file diff --git a/src/roar/client.cpp b/src/roar/client.cpp index 57def14d..c81fb5b2 100644 --- a/src/roar/client.cpp +++ b/src/roar/client.cpp @@ -7,62 +7,74 @@ #include #include +#include + namespace Roar { using namespace promise; - namespace Detail - { - namespace Parser - { - namespace x3 = boost::spirit::x3; - namespace ascii = boost::spirit::x3::ascii; - - using ascii::char_; - using ascii::alnum; - - const auto key = x3::rule{"key"} = +(alnum | char_('-') | char_('_')); - const auto value = x3::rule{"value"} = +(char_ - '\n'); - const auto newLine = x3::rule{"newLine"} = char_('\n'); - const auto whitespace = x3::rule{"whitespace"} = - char_(' ') | char_('\t'); - - const auto entry = x3::rule>{"entry"} = - -(key[([](auto& ctx) { - x3::traits::move_to(x3::_attr(ctx), x3::_val(ctx).first); - })] > ':' >> - *whitespace) >> - value[([](auto& ctx) { - x3::traits::move_to(x3::_attr(ctx), x3::_val(ctx).second); - })]; - - const auto entries = - x3::rule>>{"entries"} = - entry % newLine; - } - } // ################################################################################################################## Client::Client(ConstructionArguments&& args) - : socket_{[&args]() -> decltype(socket_) { - if (args.sslContext) + : sslOptions_{std::move(args.sslOptions)} + , socket_{[this, &args]() -> decltype(socket_) { + if (args.sslOptions) return boost::beast::ssl_stream{ - boost::beast::tcp_stream{args.executor}, *args.sslContext}; + boost::beast::tcp_stream{args.executor}, sslOptions_->sslContext}; else return boost::beast::tcp_stream{args.executor}; }()} + , endpoint_{} + , attachedState_{} + {} + //------------------------------------------------------------------------------------------------------------------ + Client::~Client() + { + shutdownSync(); + } + //------------------------------------------------------------------------------------------------------------------ + std::optional Client::setupSsl(std::string const& host) { if (std::holds_alternative>(socket_)) { + if (!sslOptions_) + throw std::runtime_error{"No SSL options, but SSL socket was created?"}; + auto& sslSocket = std::get>(socket_); - sslSocket.set_verify_mode(args.sslVerifyMode); - if (args.sslVerifyCallback) - sslSocket.set_verify_callback(std::move(args.sslVerifyCallback)); + if (!SSL_ctrl( + sslSocket.native_handle(), + SSL_CTRL_SET_TLSEXT_HOSTNAME, + TLSEXT_NAMETYPE_host_name, + // yikes openssl you make me do this + const_cast(reinterpret_cast(host.c_str())))) + { + boost::beast::error_code ec{ + static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()}; + return Error{.error = ec, .additionalInfo = "SSL_set_tlsext_host_name failed."}; + } + + sslSocket.set_verify_mode(sslOptions_->sslVerifyMode); + if (!sslOptions_->sslVerifyCallback) + { + if (sslOptions_->sslVerifyMode == boost::asio::ssl::verify_none) + { + sslSocket.set_verify_callback([](bool, boost::asio::ssl::verify_context&) { + return true; + }); + } + else + { + return Error{ + .error = boost::asio::error::operation_not_supported, + .additionalInfo = "No verify callback provided when verify mode is not none."}; + } + } + else + sslSocket.set_verify_callback(sslOptions_->sslVerifyCallback); + + return std::nullopt; } - } - //------------------------------------------------------------------------------------------------------------------ - Client::~Client() - { - shutdownSync(); + else + return Error{.error = boost::asio::error::operation_not_supported, .additionalInfo = "Not an SSL socket."}; } //------------------------------------------------------------------------------------------------------------------ Detail::PromiseTypeBind, Detail::PromiseTypeBindFail> @@ -146,134 +158,5 @@ namespace Roar }); }); } - //------------------------------------------------------------------------------------------------------------------ - namespace - { - class OnChunkHandler; - struct OnChunkHeader; - struct OnChunkBody; - - class SseContext : public std::enable_shared_from_this - { - public: - SseContext( - std::function onEvent, - std::function const&)> onEndOfStream); - - void init(); - - void enterReadCycle(std::shared_ptr client, std::chrono::seconds timeout); - - void parseChunk(std::string_view rawData); - void onEndOfStream(std::optional const&); - - public: - std::function onEvent; - boost::beast::flat_buffer headerBuffer; - boost::beast::http::response_parser responseParser; - - private: - std::function const&)> onEndOfStream_; - bool streamEndReached_; - std::string messageBuffer_; - std::size_t searchCursor_; - }; - - SseContext::SseContext( - std::function onEvent, - std::function const&)> onEndOfStream) - : onEvent{std::move(onEvent)} - , headerBuffer{} - , responseParser{} - , onEndOfStream_{std::move(onEndOfStream)} - , streamEndReached_{false} - , messageBuffer_{} - , searchCursor_{0} - {} - - void SseContext::onEndOfStream(std::optional const& err) - { - bool wasEndedBefore = streamEndReached_; - streamEndReached_ = true; - if (!wasEndedBefore) - onEndOfStream_(err); - } - - void SseContext::enterReadCycle(std::shared_ptr client, std::chrono::seconds timeout) - { - client->withLowerLayerDo([timeout](auto& socket) { - socket.expires_after(timeout); - }); - - client->withStreamDo([&client, timeout, this](auto& socket) { - if (streamEndReached_) - return; - - boost::asio::async_read_until( - socket, - headerBuffer, - "\n\n", - [weakClient = client->weak_from_this(), self = shared_from_this(), timeout]( - boost::beast::error_code ec, std::size_t) mutable { - auto client = weakClient.lock(); - if (!client) - return; - - if (self->streamEndReached_) - return; - - if (ec) - return self->onEndOfStream(Error{.error = ec, .additionalInfo = "Stream read failed."}); - - self->parseChunk(std::string_view{ - boost::asio::buffer_cast(self->headerBuffer.data()), - self->headerBuffer.size()}); - - self->headerBuffer.consume(self->headerBuffer.size()); - - self->enterReadCycle(std::move(client), timeout); - }); - }); - } - - void SseContext::parseChunk(std::string_view rawData) - { - std::string data = std::string{rawData}; - using namespace boost::spirit::x3; - - std::vector> keyValuePairs; - try - { - auto const parser = Detail::Parser::entries; - auto iter = std::make_move_iterator(data.cbegin()); - const auto end = std::make_move_iterator(data.cend()); - - bool success = parse(iter, end, parser, keyValuePairs); - if (!success) - { - return onEndOfStream( - Error{.error = boost::asio::error::no_recovery, .additionalInfo = "Error parsing chunk."}); - } - - std::string event; - std::string eventData; - - for (auto const& [key, value] : keyValuePairs) - { - if (key == "event") - event = value; - else if (key == "data") - eventData = value; - } - - if (!onEvent(event, eventData)) - onEndOfStream(std::nullopt); - } - catch (expectation_failure const& e) - { - return onEndOfStream(Error{.error = boost::asio::error::no_recovery, .additionalInfo = e.what()}); - } - } - } // ################################################################################################################## } \ No newline at end of file diff --git a/src/roar/server.cpp b/src/roar/server.cpp index 258212f4..85dd83de 100644 --- a/src/roar/server.cpp +++ b/src/roar/server.cpp @@ -20,7 +20,7 @@ namespace Roar struct Server::Implementation : public std::enable_shared_from_this { boost::asio::ip::tcp::acceptor acceptor; - std::optional sslContext; + std::optional sslContext; boost::asio::ip::tcp::endpoint bindEndpoint; boost::asio::ip::tcp::endpoint resolvedEndpoint; std::shared_mutex acceptorStopGuard; @@ -32,7 +32,7 @@ namespace Roar Implementation( boost::asio::any_io_executor& executor, - std::optional sslContext, + std::optional sslContext, std::function onError, std::function onAcceptAbort, std::unique_ptr standardResponseProvider); @@ -42,7 +42,7 @@ namespace Roar //------------------------------------------------------------------------------------------------------------------ Server::Implementation::Implementation( boost::asio::any_io_executor& executor, - std::optional sslContext, + std::optional sslContext, std::function onError, std::function onAcceptAbort, std::unique_ptr standardResponseProvider) diff --git a/src/roar/session/factory.cpp b/src/roar/session/factory.cpp index 34c32786..b2193a94 100644 --- a/src/roar/session/factory.cpp +++ b/src/roar/session/factory.cpp @@ -28,16 +28,16 @@ namespace Roar // ################################################################################################################## struct Factory::Implementation { - std::optional& sslContext; + std::optional& sslContext; std::function onError; - Implementation(std::optional& sslContext, std::function onError) + Implementation(std::optional& sslContext, std::function onError) : sslContext{sslContext} , onError{std::move(onError)} {} }; // ################################################################################################################## - Factory::Factory(std::optional& sslContext, std::function onError) + Factory::Factory(std::optional& sslContext, std::function onError) : impl_{std::make_unique(sslContext, std::move(onError))} {} //------------------------------------------------------------------------------------------------------------------ diff --git a/src/roar/session/session.cpp b/src/roar/session/session.cpp index 6aa4aa5d..cfa80165 100644 --- a/src/roar/session/session.cpp +++ b/src/roar/session/session.cpp @@ -33,14 +33,14 @@ namespace Roar Implementation( boost::asio::ip::tcp::socket&& socket, boost::beast::flat_buffer&& buffer, - std::optional& sslContext, + std::optional& sslContext, bool isSecure, std::function onError, std::weak_ptr router, std::shared_ptr standardResponseProvider) : stream{[&socket, &sslContext, isSecure]() -> decltype(stream) { if (isSecure) - return boost::beast::ssl_stream{std::move(socket), *sslContext}; + return boost::beast::ssl_stream{std::move(socket), sslContext->ctx}; return Detail::StreamType{std::move(socket)}; }()} , buffer{std::move(buffer)} @@ -62,7 +62,7 @@ namespace Roar Session::Session( boost::asio::ip::tcp::socket&& socket, boost::beast::flat_buffer&& buffer, - std::optional& sslContext, + std::optional& sslContext, bool isSecure, std::function onError, std::weak_ptr router, @@ -140,22 +140,32 @@ namespace Roar }); } //------------------------------------------------------------------------------------------------------------------ - void Session::startup() + void Session::startup(bool immediate) { if (std::holds_alternative(impl_->stream)) { - boost::asio::dispatch( - std::get(impl_->stream).get_executor(), [self = this->shared_from_this()]() { - self->readHeader(); - }); + if (immediate) + readHeader(); + else + { + boost::asio::dispatch( + std::get(impl_->stream).get_executor(), [self = this->shared_from_this()]() { + self->readHeader(); + }); + } } else { - boost::asio::dispatch( - std::get>(impl_->stream).get_executor(), - [self = this->shared_from_this()]() { - self->performSslHandshake(); - }); + if (immediate) + performSslHandshake(); + else + { + boost::asio::dispatch( + std::get>(impl_->stream).get_executor(), + [self = this->shared_from_this()]() { + self->performSslHandshake(); + }); + } } } //------------------------------------------------------------------------------------------------------------------ @@ -211,7 +221,10 @@ namespace Roar std::get>(impl_->stream) .async_handshake( boost::asio::ssl::stream_base::server, - [self = this->shared_from_this()](const boost::system::error_code& ec) { + impl_->buffer.cdata(), + [self = this->shared_from_this()](const boost::system::error_code& ec, std::size_t bytesTransferred) { + self->impl_->buffer.consume(bytesTransferred); + if (ec) return self->impl_->onError({.error = ec, .additionalInfo = "Error during SSL handshake."}); self->readHeader(); diff --git a/src/roar/ssl/make_ssl_context.cpp b/src/roar/ssl/make_ssl_context.cpp index a7dc7a3c..deade578 100644 --- a/src/roar/ssl/make_ssl_context.cpp +++ b/src/roar/ssl/make_ssl_context.cpp @@ -3,47 +3,43 @@ namespace Roar { - boost::asio::ssl::context makeSslContext(SslContextCreationParameters settings) + void initializeServerSslContext(SslServerContext& ctx) { - boost::asio::ssl::context sslContext{settings.method}; - - sslContext.set_password_callback( - [password = settings.password](std::size_t, boost::asio::ssl::context_base::password_purpose) { - return std::string{password}; + ctx.ctx.set_password_callback( + [password = std::string{ctx.password}](std::size_t, boost::asio::ssl::context_base::password_purpose) { + return password; }); - sslContext.set_options( + ctx.ctx.set_options( boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use); std::visit( overloaded{ - [&](std::string_view key) { - sslContext.use_certificate_chain(boost::asio::buffer(key.data(), key.size())); + [&](std::string const& key) { + ctx.ctx.use_certificate_chain(boost::asio::buffer(key.data(), key.size())); }, [&](std::filesystem::path const& path) { - sslContext.use_certificate_chain_file(path.string()); + ctx.ctx.use_certificate_chain_file(path.string()); }, }, - settings.certificate); + ctx.certificate); std::visit( overloaded{ - [&](std::string_view key) { - sslContext.use_private_key( + [&](std::string const& key) { + ctx.ctx.use_private_key( boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context::file_format::pem); }, [&](std::filesystem::path const& path) { - sslContext.use_private_key_file(path.string(), boost::asio::ssl::context::file_format::pem); + ctx.ctx.use_private_key_file(path.string(), boost::asio::ssl::context::file_format::pem); }}, - settings.privateKey); + ctx.privateKey); - if (!settings.diffieHellmanParameters.empty()) + if (!ctx.diffieHellmanParameters.empty()) { - sslContext.use_tmp_dh( - boost::asio::buffer(settings.diffieHellmanParameters.data(), settings.diffieHellmanParameters.size())); + ctx.ctx.use_tmp_dh( + boost::asio::buffer(ctx.diffieHellmanParameters.data(), ctx.diffieHellmanParameters.size())); } - - return sslContext; } } \ No newline at end of file diff --git a/test/resources/keys.hpp b/test/resources/keys.hpp index 800ffa2e..5ffe3629 100644 --- a/test/resources/keys.hpp +++ b/test/resources/keys.hpp @@ -1,99 +1,99 @@ +#pragma once - +#include namespace Roar::Tests { - char const* certificateForTests = + static constexpr std::string_view certificateForTests = "-----BEGIN CERTIFICATE-----\r\n" - "MIIF/zCCA+egAwIBAgIUP+fnN+9ZTdBIPqH17yfgbhcc7towDQYJKoZIhvcNAQEL\r\n" - "BQAwgY4xCzAJBgNVBAYTAkRFMRUwEwYDVQQIDAxMb3dlci1TYXhvbnkxDjAMBgNV\r\n" - "BAcMBUNlbGxlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEzAR\r\n" - "BgNVBAMMClRpbSBFYmJla2UxIDAeBgkqhkiG9w0BCQEWEXRpbTA2dHJAZ21haWwu\r\n" - "Y29tMB4XDTIyMDUwMTExMzgzMVoXDTIzMDUwMTExMzgzMVowgY4xCzAJBgNVBAYT\r\n" - "AkRFMRUwEwYDVQQIDAxMb3dlci1TYXhvbnkxDjAMBgNVBAcMBUNlbGxlMSEwHwYD\r\n" - "VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEzARBgNVBAMMClRpbSBFYmJl\r\n" - "a2UxIDAeBgkqhkiG9w0BCQEWEXRpbTA2dHJAZ21haWwuY29tMIICIjANBgkqhkiG\r\n" - "9w0BAQEFAAOCAg8AMIICCgKCAgEArpqfJncfaCGSXxB8gWT4PreU5Ov1mMUMuR9E\r\n" - "egnY65GR2a3u31VRvo3D20auqpDIpT0eFf1FK7O4iuYg8Q27Cba/WIUoJ19OHezw\r\n" - "DMWc6/ZFUZLvMy80O7UwTAd/iV25/TXVdpnEbJ3X9+bABI1Y2zB68mJWnzx6CGV1\r\n" - "e5kOYmPupqgR00/efB3vw2AIVm0rEIi7fklc06hlKqyeTPimVt1ENu5vTBG0JkQY\r\n" - "UvK6Are/qWkZ+Oz5O6Pmy7SywOjt6cijjtOcFJXM9fXeNRfBeBDhrGR4yweU/Ayh\r\n" - "PWUql2vaVYCf3UyZKlwSMXPElC7RAO0oaAmltcrQYC6mvms5eyjCEORGdnOaZycO\r\n" - "DnvH1G25t4DWNDgteYdw+aJjmOTYrIbJFdL35BArgABXND8IFaXrBjnwrVn4XNe/\r\n" - "zN03GNiQrPUOQV+HCawyNwhnyruJhXrBIwuGfiVRU9HyZM6ZEjCyzKplYGF+ULWX\r\n" - "5jw7v8vHDEpu5EBaibpQGl+Wvop38F7tazVkcfJqKCl9kejSTo8EyqriLICmLOfT\r\n" - "O5bCYyg5rE4W8tWuoQudQ1taZ+F5mkYNECm+Rq6QxaoiD+qXvYZWsS/IffYC214K\r\n" - "D9mDbH5lSRQzXOo0ssV4kH24QuOS03eCZDVe+SfawxHQUZdu5syBo0QCuzOrXb/p\r\n" - "+mNKxCkCAwEAAaNTMFEwHQYDVR0OBBYEFEamM5EGh7ZzToOLpsiyjfHXfb2NMB8G\r\n" - "A1UdIwQYMBaAFEamM5EGh7ZzToOLpsiyjfHXfb2NMA8GA1UdEwEB/wQFMAMBAf8w\r\n" - "DQYJKoZIhvcNAQELBQADggIBAHo7h0nYmckkHmn1i7VpeehajqovQDBKHY7G/udH\r\n" - "e/k+p7fl/15M7lEu/Evj5CGyT7l0E1zK7cbmmZdAZ69SBzRXTvmw/gfiYwd4F7/4\r\n" - "VdfHdjBrhtg3PGIaT7Ejt/aHe+lc8kWENSbKidJx0A2jaIj6RTX3Ar+8K8Fb4srS\r\n" - "4qLsvbtIW1Eh7+ina9uquYYWvWKzNDPxSG7jv+y9rIpWZsrMLXuP+NldBHWu/DlM\r\n" - "3w3ofXbcutNL1ZN9r8Noj1dVInfbOtJLRkcf/2FBJm0+DjSGSK4JQdClaQh8soY2\r\n" - "7gYqy7NqXpwi96XwuNBLe6vQMyPCxidsqQrgLjRB7UnTA4xrSQafdj1qBMFYrUpm\r\n" - "jZT+E2TLHsc7sDCg7CuRo9DXVKioiS1wpM3wT1dqq7Xgr91t/0LWihOEDcILON98\r\n" - "dTGsbwaCP911Vu3JdenRLaSlrUFBbrUaNW6NcBZ9agyBTo1/v3OydjYERgkuxfOT\r\n" - "TdG7znbuU5Sj/q5ujYdtSs8ThEkLkGJnnUF38u6tTKLX3IosXdo5QijuTGSWOVr8\r\n" - "CTEtoxhIPmPWXnHrckZFMiFezUGkhON0VC4yfbjG1rahQh0ylMx42PRa8FM4PDaS\r\n" - "oe2X0rPpbUW6/iODvvbENO/MW70Kr7BFv6CUNB4jezVHyVFHjljZ3+VVuzKBW9ay\r\n" - "EI/7\r\n" + "MIIF1TCCA72gAwIBAgIUP95Hy5yNAdIYCrGVAPG6fmk8E5swDQYJKoZIhvcNAQEL\r\n" + "BQAwejELMAkGA1UEBhMCREUxFjAUBgNVBAgMDU5pZWRlcnNhY2hzZW4xDjAMBgNV\r\n" + "BAcMBUNlbGxlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxIDAe\r\n" + "BgkqhkiG9w0BCQEWEXRpbTA2dHJAZ21haWwuY29tMB4XDTIzMTAyMzA5MTEzNVoX\r\n" + "DTMzMTAyMDA5MTEzNVowejELMAkGA1UEBhMCREUxFjAUBgNVBAgMDU5pZWRlcnNh\r\n" + "Y2hzZW4xDjAMBgNVBAcMBUNlbGxlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz\r\n" + "IFB0eSBMdGQxIDAeBgkqhkiG9w0BCQEWEXRpbTA2dHJAZ21haWwuY29tMIICIjAN\r\n" + "BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvXUNE2afeqck9wo/J0TP/yoGw31S\r\n" + "q/zXj7PG80NYN8kCjjVNDlG//vt9q0EfHR5GGUNUSr6iO9wFJP1SxY8AHr/2SRA3\r\n" + "1zKjp2mBE6eUxHTVe/ZlnyZvFF//ZWIlbzutbePFTGTqHhuAhCusgLKq9xjfkprl\r\n" + "R3Ky2keaoPbkbrzEc30bFTu4bE5ZyuPJsX1rPszrNGhdmnHIebnORrV2XjarKwMi\r\n" + "6My1zMqgC374wp65zLS9o+pmbiBr5anKjQtg335ZiMAgDlSRO144wA6zgIJHdyMp\r\n" + "fU8YXvT1oE3jUx3/WlzwyDzc2mohZkj0w4/y+T7Ctnhw1BcUqHS85myE/uQAgMeW\r\n" + "DvkIQ1ySmC9Yeb/H+SC7TCFDgJrcK7Z4gY5/0cMd3hXfAFs5ewelNB61Q2K0KKuf\r\n" + "TMDnfoEfwagYNgNVr1rWgV/xnQ5CqraLI1B3xLIG5Kq32t7iDt8gfsHeet0NA/rm\r\n" + "Umq2FPvYazk1BCz24G9ABWbrdPm/6Wsuv7j60bV7CuP9S30cXBDL9Bf6Rpe9HXW2\r\n" + "l6/PoFPAi7bp/mAfMP8BoV1YEZEeP2hI5mK50elrc/tXj9NO6l6VUV/SzJUYxgBc\r\n" + "Jp0FXAmjEMY5/g3FE+60WbV2rPnjhD1jbKYPSnMR3c7tzPf+zSqC4nkxuWx+Pt9f\r\n" + "bYiiYcE0FQxAA90CAwEAAaNTMFEwHQYDVR0OBBYEFBniKLPSRuX2y5rxzXF+6tcu\r\n" + "qiilMB8GA1UdIwQYMBaAFBniKLPSRuX2y5rxzXF+6tcuqiilMA8GA1UdEwEB/wQF\r\n" + "MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAHFZbpCA6Ui5Gcfa/eDEATTN6KlI31Z4\r\n" + "jDZJrlHzcwcZCUc+KEme/RCV8JNzUX7sgRpmy3NowpeUVQxGvUyqtdvrdUn0xha1\r\n" + "0TeaK+4PJUdo7VBMPcrCMhIeb1rvGQG+iasBVs+CNefJIy6d3cuP9XL17x3uZR7R\r\n" + "cMNjSaLGX6yZIw5+FfHgyGeCs8uU66BAX9eU3ZlHC6v7MVV7+n0z7wvnZ+1kIyoS\r\n" + "i0LCDPteFAJOr8CRLTjrJUtY59ytZOnJkWfCudCM1UUpfFyWQEn4PCya37oGQiQv\r\n" + "zNSQo/uCNr1bKepIYxxfKCETZ3JZTlCk45D9zgRxqKtDVOLGbNzAuRSFzUh8zkQF\r\n" + "uahEY1aCSyS3DHIsQSkW6nCokQHx5cu9aKAl6J5yzi+s1X1k+38ceSbHxaDC7kig\r\n" + "gmarUlWFlvZDsPlUYkH4nLcxK0kZMjSj49lo9G4BeyhUp/z+3RD7chG6L0o0pLqJ\r\n" + "SNlSCA7lSURJwqccJOu16g2HCaGd+v35FTNj5a9Xpaa35nHrXcRTQ3oQfkxG6ixz\r\n" + "k/JsaNQ2NueSgSutTDkN8nRAlCgfvnkk49wkJSDkuPVQECxQ9lEbhMX4TZI/CXLD\r\n" + "nR6MQUJyP4Fh/OGDgyNVJ6xtPqw/sRMiFP7pVdVDj0HrRb+g+5gfNvL+plkg03VT\r\n" + "1EysP049Estw\r\n" "-----END CERTIFICATE-----\r\n"; - char const* keyForTests = + static constexpr std::string_view keyForTests = "-----BEGIN ENCRYPTED PRIVATE KEY-----\r\n" - "MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIrf/77b8sNwUCAggA\r\n" - "MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECNHCR2GvchYZBIIJSHIIG5ap59IE\r\n" - "qt7jOf9xg1NjOoiQeE/a4EsYA2XsntezYIXh/MnHZBO6sqX8+fBlGwMTsMOWyySH\r\n" - "sPUGLdlgwMwpKJMIaHxD3GG49DtxuwBdK47HALe5eEpV5fMCV94k3IxWFAP8mXVl\r\n" - "Y8iNwkMUqma8G8S7HJvjWCPdqLzKIsAYKD4nLsCXCVVsaJgYDjLP7ALcc/fBMPcv\r\n" - "gIJWmhfkiwfQGeyNBz4uFgSEBNEM+0y2+W3lf71GRaQIjFDRtShrADmvrGDLV9wu\r\n" - "YxQmaHejmKIv9oJfLptjXuKgDfo8DNDOf21Ailgnnyv7L50o8D0F1vNWaUzzNCRy\r\n" - "doElGGlu594bryED/i4UaWpizy/ePn0CeTwSl8l51157IYWeqSmadoP08fhyB1j/\r\n" - "mBLO7G/WgpNnSQYh0ee2lgUnGpOggc2PmVuHkk3NpXcbnvDiXADlkAzR5X9qboXJ\r\n" - "0Fuo+ORLwzZZdJO92AAEG6r2yCijD0VyU7XIXQAX1th7Px36MHBmnb0UfldG+dJH\r\n" - "mkrYQbJXg02CIzuSpnmxBpA/y6hcgADEZaT3OmiDXwGEVZ+MPsEZwrQKKo97x3lg\r\n" - "mxs5MFfT2T0BQ7DmfBjz3dlFfLsBXpSbjioJ9z5PjrVnDdUL87f1GkUF214OxZeh\r\n" - "N8+E32sYt/ryuIoHrXRggz7xWCQYQqq+j8UX2CR2Nd+D08WLp/z74ywe5JSIo3Vj\r\n" - "gAZvPMGYmfRWBPDaYcbUryZXJ4KOj/+LIRJ2w4NJHv4P1f1BIpZV5cGj38ANpTzs\r\n" - "3+ZZRMZ9v3YiLLq2FQZi8p5hSZGxaB3dXm9mv6sTzCr9n5erVw6L4u87H4UFXcT9\r\n" - "EFLv9zXzr9n1Ikb+avsJoqid94umLhmJpjYZ9NMQH9MYLmKIlEm5bTU8FvO9lz20\r\n" - "tPhgXnOFSkcKka4U9i3jD+u+XDbunPyr6PfVglCZ1Ny7eoOZBKFLHiM3bGikDFCC\r\n" - "2F3c489wyNneTSP4GFbH8pd0jXmy6Nki01xx01CQ4BWsjkPreJpg5W8ELmUukpg/\r\n" - "xa9jGYJ2II7WRzAoUgd25RDeElkuNN39YbO2zBww1MQFloAFxXHZ4TI5dzVCP0jT\r\n" - "h+0h8kRD9XFihzMqKuvwtVTglYDCt4uMIglCj7YZDiDHqEI/PAwby+Releqc5I++\r\n" - "7P1JWCbtOp2Sb72ibsVddFhf1Epfbt5Fc0BmtQAjZsT7xbdS6MrCXKXXxrAoSc3+\r\n" - "UAQlIz9eNzcqh4IsRAUoghEFGodEGiMFGJ4f9sbasq6HvFL2MCzd+dXqTNnDKkkA\r\n" - "wK5uKW/xayLHaYyPK6aAfPL4ig8VAgcYx/AklaSB9jEc4qjXhXIcIdMu49NLdNst\r\n" - "8KE0gfIefOuBUQeS6x4Q36QSfNEtw3mS4GGLH4r+X457Dpng2YmsDrUMeRBGv23P\r\n" - "4f5aLsjZWlMXN8D6RhS3Jym7c3uuGO5XokdqvubIMZVM5cQYLfot2eZdK4nPsU7H\r\n" - "ouqYEpix3l/qkDOmELRqitb/ZMSsbYUvFOIS90KJg4wrLJwWPlkLNIJTmP7Z22s4\r\n" - "w2b3JuO8vzEZ8H7WzDntrgyVkmdXaC2haKEs2oZURfoTdKc5D1D6p40jLp6sMOdu\r\n" - "6Z4li+lG2Y4DmafEH5ev5a5+qABAug1MQVS3+FXhh6+4Fr54WnT8BR1Me1KYUVW1\r\n" - "l1N+Zjkb25TCtS6xcQ9Mt/1EeL9h+7T03Y8At0xW3S+/lsH2XbA1hnXcnoskDM9h\r\n" - "EPP93b/K0aTPy6ac5BtDF/4n+We7C+llrEifwd81yu3f3vTDp2BMPQjAgix55JFK\r\n" - "Tga3VjGebcDGLlh2k1j5hZp+BWA+D1U7d2yUdkeCNNKqoo8qEBOShdFKs/N7XYGJ\r\n" - "Zam5b6evJ91mbfrxmAuFmaHDfIZw4TNl4mIk9I2ZvHI5PjHAohc2XH8ng5C+OgMk\r\n" - "1VL4QiznJd2tXPmLmx3lIR9Eym1PJ4zkdYR9nyoQCdBlc3mKQC8wacGMxctcHeOz\r\n" - "44tKdESEt+xzx5U5T+hrGIVdEoI/H6355x9vJi55G+JYUYOY6Bb6VZE2329QqqV9\r\n" - "F7/9uYC8aJMzNFin0SamdHDFmAZ5aRCNKxEJG9i60YYsL74crZuCAtKBwuTIcaD9\r\n" - "pHWvZIw1ub5wyjEDqJ91aId0SvEfNj9meyKQeGvPRevHpp/Iz/Yj4nnjFhA9gPub\r\n" - "pxe0Ec2kBqD2OTuJyaiRF3SWRw1p9lqqiTbpJTOZvZ1xlnjeMgmYrLGbPYhurXLy\r\n" - "/KoregjfOQZDXscCGB5DI3B9qiLZ8JmDPIT0RBz42QvJa+fwtNlmQnUQeBISLLHM\r\n" - "vIfe5iXA4sjHuMoxRA/6pbSX6rka9NPr/GzvAiENcRxy+KAjkyVVHd4NiXnPt3O8\r\n" - "l39SBVd8xDqPB21nYeQsqQMy90jlrH0/RXhUVNPk3ahubcKdiZExIn070H7XZYdv\r\n" - "I34g1CnkdLs6RcOvG0kEii3L/qgqVFsuc1oshTgBqkFTjC66soU5ABqMVIYSzlfX\r\n" - "eEvSNE1yabj1o5gX2wiIQF74THiCf3cjPLVxj8AaKfjbMHkC5DJb2LGFnxdIehx7\r\n" - "Pq2tIkxzaBAyMff7LEpHr8DogIxmZy8Fr6jYZrL9i11hyQePzi21gYdJvBaMqATl\r\n" - "ZpgP43ufma71qY7noHZlZO8goAICJWtaQVSGk9Hi7OQsC8KNoa2SmK8WFithWGcu\r\n" - "XFmiLBaX2aK9sZwcgSAvV0vuaEij5kQl6UPGVXsvy2jTl4m0cBCZH2x9OPPdu3rf\r\n" - "RidT2UwiaeMPAPnwkcR6GxTwN6gffKmfeKlA377cVP6qEDfR5xZaxRmGquncRaoR\r\n" - "2nxnWshEPE2cko8fXqZ9ZlTDZ9yBrJVIDEJOKmXj0Q7OHfYeaCyXG9/BNT81ilEc\r\n" - "h7cJFpi/CqmDO1L0X0Ufw4BwgM/ZoXwLrW/otjrfczS4DuGfSiPvxyttQsMv8pZP\r\n" - "Xgm9AZ5bs0WUAIRlHPW7kNeuVqqnJmHkxWKuHqjRA1K+cz47eZnkhD2F1iDbfM2R\r\n" - "HvKLw/tp/peZLjSv+HVnV517+dNFVOV85hBjXOWr7HNTmxPfUPZyqjS4focwsY5M\r\n" - "4yIdeB2/VdiV/ROhbsYIqEbSwmkRdtXkOmR5xahgsaHtLgtims4zUmNpI/4BRidH\r\n" - "W78JU09QBBw1qYEFER5zAw==\r\n" + "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\r\n" + "BwHGSpPNwyMk31jwz0nKaA==\r\n" "-----END ENCRYPTED PRIVATE KEY-----\r\n"; - char const* keyPassphrase = "asdf"; + static constexpr std::string_view keyPassphrase = "asdf"; } \ No newline at end of file diff --git a/test/test_secure_async_client.hpp b/test/test_secure_async_client.hpp new file mode 100644 index 00000000..941e32d5 --- /dev/null +++ b/test/test_secure_async_client.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +#include "util/common_server_setup.hpp" +#include "util/common_listeners.hpp" +#include "util/test_sources.hpp" +#include "util/temporary_directory.hpp" + +#include +#include + +#include + +namespace Roar::Tests +{ + class SecureAsyncClientTests + : public CommonServerSetup + , public ::testing::Test + { + protected: + void SetUp() override + { + makeDefaultServer(); + makeSecureServer(); + listener_ = server_->installRequestListener(); + secureServer_->installRequestListener(); + } + + protected: + std::shared_ptr listener_; + }; + + TEST_F(SecureAsyncClientTests, ConnectingSecurelyToUnsecureServerFails) + { + auto client = makeClient("https"); + + auto req = Roar::Request{}; + req.method(boost::beast::http::verb::get); + req.host("::1"); + req.port(server_->getLocalEndpoint().port()); + req.target("/index.txt"); + req.version(11); + + std::promise awaitCompletion; + client->request(std::move(req)) + .then([client, &awaitCompletion]() { + awaitCompletion.set_value(true); + }) + .fail([&awaitCompletion](auto e) { + awaitCompletion.set_value(false); + }); + + EXPECT_FALSE(awaitCompletion.get_future().get()); + } + + TEST_F(SecureAsyncClientTests, CanSendRequestToSecureServerAndCompleteViaThen) + { + auto client = makeClient("https"); + + auto req = Roar::Request{}; + req.method(boost::beast::http::verb::get); + req.version(11); + + req.host("::1"); + req.port(secureServer_->getLocalEndpoint().port()); + req.target("/index.txt"); + + std::promise awaitCompletion; + client->request(std::move(req)) + .then([client, &awaitCompletion]() { + awaitCompletion.set_value(true); + }) + .fail([&awaitCompletion](auto e) { + std::cerr << e << std::endl; + awaitCompletion.set_value(false); + }); + + EXPECT_TRUE(awaitCompletion.get_future().get()); + } +} \ No newline at end of file diff --git a/test/test_unsecure_async_client.hpp b/test/test_unsecure_async_client.hpp index 20132096..8b59d307 100644 --- a/test/test_unsecure_async_client.hpp +++ b/test/test_unsecure_async_client.hpp @@ -29,21 +29,6 @@ namespace Roar::Tests secureServer_->installRequestListener(); } - std::shared_ptr makeClient(std::string const& scheme = "http") - { - if (scheme == "http") - return std::make_shared(Client::ConstructionArguments{.executor = executor_}); - else if (scheme == "https") - { - return std::make_shared(Client::ConstructionArguments{ - .executor = executor_, - .sslContext = boost::asio::ssl::context{boost::asio::ssl::context::tlsv12_client}, - }); - } - else - throw std::runtime_error{"Unknown scheme: " + scheme}; - } - protected: std::shared_ptr listener_; }; @@ -241,4 +226,31 @@ namespace Roar::Tests ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, "Hello"); } + + TEST_F(AsyncClientTests, CanAttachAndRetrieveState) + { + auto client = makeClient(); + + client->attachState("name", std::string{"Hello"}); + auto& str = client->state("name"); + EXPECT_EQ(str, "Hello"); + } + + TEST_F(AsyncClientTests, CanEmplaceState) + { + auto client = makeClient(); + + client->emplaceState("name", "Hello"); + auto& str = client->state("name"); + EXPECT_EQ(str, "Hello"); + } + + TEST_F(AsyncClientTests, CanRemoveState) + { + auto client = makeClient(); + + client->emplaceState("name", "Hello"); + client->removeState("name"); + EXPECT_THROW(client->state("name"), std::out_of_range); + } } \ No newline at end of file diff --git a/test/tests.cpp b/test/tests.cpp index 7b984c97..4bf18546 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -5,6 +5,7 @@ #include "test_web_socket.hpp" #include "test_serve.hpp" #include "test_url.hpp" +#include "test_secure_async_client.hpp" #include "test_unsecure_async_client.hpp" #include diff --git a/test/util/common_server_setup.hpp b/test/util/common_server_setup.hpp index 8fe151c4..01e403d6 100644 --- a/test/util/common_server_setup.hpp +++ b/test/util/common_server_setup.hpp @@ -1,8 +1,10 @@ #pragma once #include "../resources/keys.hpp" +#include "temporary_directory.hpp" #include +#include #include #include @@ -22,10 +24,11 @@ namespace Roar::Tests { public: CommonServerSetup() - : pool_{2} + : pool_{4} , executor_{pool_.executor()} , errors_{} , server_{} + , tmpDir_{std::filesystem::current_path(), true} {} void makeDefaultServer() @@ -40,15 +43,44 @@ namespace Roar::Tests server_->start(); } - void makeSecureServer() + std::pair generateCertAndKey() const { + std::filesystem::path certFile = tmpDir_.path() / "example.com.crt"; + std::filesystem::path keyFile = tmpDir_.path() / "example.com.key"; + std::stringstream cmd; + cmd << "openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout " << keyFile << " -out " + << certFile + << " -subj \"/CN=example.com\" -addext " + "\"subjectAltName=DNS:example.com,DNS:*.example.com,IP:127.0.0.1\""; + system(cmd.str().c_str()); + return {certFile, keyFile}; + } + + void makeSecureServer(bool freshKeys = false) + { + auto ctx = [this, freshKeys]() { + if (freshKeys) + { + const auto [certFile, keyFile] = generateCertAndKey(); + return SslServerContext{ + .certificate = certFile, + .privateKey = keyFile, + }; + } + else + { + return SslServerContext{ + .certificate = std::string{certificateForTests}, + .privateKey = std::string{keyForTests}, + .password = std::string{keyPassphrase}, + }; + } + }(); + initializeServerSslContext(ctx); + secureServer_ = std::make_unique(Roar::Server::ConstructionArguments{ .executor = executor_, - .sslContext = makeSslContext(SslContextCreationParameters{ - .certificate = std::string_view{certificateForTests}, - .privateKey = std::string_view{keyForTests}, - .password = keyPassphrase, - }), + .sslContext = std::move(ctx), .onError = [this](Roar::Error&& err) { errors_.push_back(std::move(err)); @@ -57,6 +89,30 @@ namespace Roar::Tests secureServer_->start(); } + std::shared_ptr + makeClient(std::string const& scheme = "http", std::optional options = std::nullopt) + { + if (scheme == "http") + return std::make_shared(Client::ConstructionArguments{.executor = executor_}); + else if (scheme == "https") + { + if (!options) + { + options = Client::SslOptions{ + .sslContext = boost::asio::ssl::context{boost::asio::ssl::context::tlsv12_client}, + .sslVerifyMode = boost::asio::ssl::verify_none, + }; + } + + return std::make_shared(Client::ConstructionArguments{ + .executor = executor_, + .sslOptions = std::move(options), + }); + } + else + throw std::runtime_error{"Unknown scheme: " + scheme}; + } + std::string urlImpl(Roar::Server& server, std::string const& path, UrlParams params = {}) const { const auto url = @@ -95,5 +151,6 @@ namespace Roar::Tests std::vector errors_; std::unique_ptr server_; std::unique_ptr secureServer_; + TemporaryDirectory tmpDir_; }; } \ No newline at end of file