From 0818b6ce5bbcfec726fefc2030ce9b176c350aca Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Tue, 3 Oct 2023 17:22:37 +0100 Subject: [PATCH] Add admin password check (#847) Fixes #846 --- CMakeLists.txt | 3 +- README.md | 13 ++ example-config.json | 3 +- src/rpc/Factories.cpp | 8 +- src/rpc/Factories.h | 4 +- src/rpc/RPCEngine.h | 16 +-- src/web/Context.h | 6 +- src/web/HttpSession.h | 13 +- src/web/PlainWsSession.h | 13 +- src/web/RPCServerHandler.h | 3 +- src/web/Server.h | 34 +++++- src/web/SslHttpSession.h | 13 +- src/web/SslWsSession.h | 15 ++- .../impl/AdminVerificationStrategy.cpp} | 54 ++++++--- src/web/impl/AdminVerificationStrategy.h | 82 +++++++++++++ src/web/impl/HttpBase.h | 7 ++ src/web/interface/ConnectionBase.h | 12 ++ unittests/rpc/AdminVerificationTests.cpp | 40 ------- unittests/rpc/ForwardingProxyTests.cpp | 36 +++--- unittests/util/TestHttpSyncClient.h | 34 +++++- unittests/web/AdminVerificationTests.cpp | 113 ++++++++++++++++++ unittests/web/ServerTests.cpp | 73 +++++++++++ 22 files changed, 478 insertions(+), 117 deletions(-) rename src/{rpc/common/impl/AdminVerificationStrategy.h => web/impl/AdminVerificationStrategy.cpp} (52%) create mode 100644 src/web/impl/AdminVerificationStrategy.h delete mode 100644 unittests/rpc/AdminVerificationTests.cpp create mode 100644 unittests/web/AdminVerificationTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b041c96a..225d56e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ target_sources (clio PRIVATE ## Feed src/feed/SubscriptionManager.cpp ## Web + src/web/impl/AdminVerificationStrategy.cpp src/web/IntervalSweepHandler.cpp ## RPC src/rpc/Errors.cpp @@ -173,7 +174,6 @@ if (tests) unittests/rpc/BaseTests.cpp unittests/rpc/RPCHelpersTests.cpp unittests/rpc/CountersTests.cpp - unittests/rpc/AdminVerificationTests.cpp unittests/rpc/APIVersionTests.cpp unittests/rpc/ForwardingProxyTests.cpp unittests/rpc/WorkQueueTests.cpp @@ -220,6 +220,7 @@ if (tests) unittests/data/cassandra/ExecutionStrategyTests.cpp unittests/data/cassandra/AsyncExecutorTests.cpp # Webserver + unittests/web/AdminVerificationTests.cpp unittests/web/ServerTests.cpp unittests/web/RPCServerHandlerTests.cpp unittests/web/WhitelistHandlerTests.cpp diff --git a/README.md b/README.md index 413816fc..3eda19b6 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,19 @@ Clio will fallback to hardcoded defaults when not specified in the config file o of the minimum and maximum supported versions hardcoded in `src/rpc/common/APIVersion.h`. > **Note:** See `example-config.json` for more details. +## Admin rights for requests + +By default clio checks admin privileges by IP address from request (only `127.0.0.1` is considered to be an admin). +It is not very secure because the IP could be spoofed. +For a better security `admin_password` could be provided in the `server` section of clio's config: +```json +"server": { + "admin_password": "secret" +} +``` +If the password is presented in the config, clio will check the Authorization header (if any) in each request for the password. +Exactly equal password gains admin rights for the request or a websocket connection. + ## Using clang-tidy for static analysis Minimum clang-tidy version required is 16.0. diff --git a/example-config.json b/example-config.json index 558c5bc6..151f62bb 100644 --- a/example-config.json +++ b/example-config.json @@ -64,7 +64,8 @@ "port": 51233, // Max number of requests to queue up before rejecting further requests. // Defaults to 0, which disables the limit. - "max_queue_size": 500 + "max_queue_size": 500, + "admin_password": "xrp" }, // Overrides log level on a per logging channel. // Defaults to global "log_level" for each unspecified channel. diff --git a/src/rpc/Factories.cpp b/src/rpc/Factories.cpp index df46370b..06c58a9c 100644 --- a/src/rpc/Factories.cpp +++ b/src/rpc/Factories.cpp @@ -53,7 +53,7 @@ make_WsContext( return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}}; string const command = commandValue.as_string().c_str(); - return web::Context(yc, command, *apiVersion, request, session, tagFactory, range, clientIp); + return web::Context(yc, command, *apiVersion, request, session, tagFactory, range, clientIp, session->isAdmin()); } Expected @@ -63,7 +63,8 @@ make_HttpContext( TagDecoratorFactory const& tagFactory, data::LedgerRange const& range, string const& clientIp, - std::reference_wrapper apiVersionParser) + std::reference_wrapper apiVersionParser, + bool const isAdmin) { if (!request.contains("method")) return Error{{ClioError::rpcCOMMAND_IS_MISSING}}; @@ -91,7 +92,8 @@ make_HttpContext( if (!apiVersion) return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}}; - return web::Context(yc, command, *apiVersion, array.at(0).as_object(), nullptr, tagFactory, range, clientIp); + return web::Context( + yc, command, *apiVersion, array.at(0).as_object(), nullptr, tagFactory, range, clientIp, isAdmin); } } // namespace rpc diff --git a/src/rpc/Factories.h b/src/rpc/Factories.h index a7591233..20e21846 100644 --- a/src/rpc/Factories.h +++ b/src/rpc/Factories.h @@ -71,6 +71,7 @@ make_WsContext( * @param range The ledger range that is available at request time * @param clientIp The IP address of the connected client * @param apiVersionParser A parser that is used to parse out the "api_version" field + * @param isAdmin Whether the connection has admin privileges */ util::Expected make_HttpContext( @@ -79,6 +80,7 @@ make_HttpContext( util::TagDecoratorFactory const& tagFactory, data::LedgerRange const& range, std::string const& clientIp, - std::reference_wrapper apiVersionParser); + std::reference_wrapper apiVersionParser, + bool isAdmin); } // namespace rpc diff --git a/src/rpc/RPCEngine.h b/src/rpc/RPCEngine.h index 1bdbd747..30b9c161 100644 --- a/src/rpc/RPCEngine.h +++ b/src/rpc/RPCEngine.h @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -60,8 +59,7 @@ namespace rpc { /** * @brief The RPC engine that ties all RPC-related functionality together. */ -template -class RPCEngineBase +class RPCEngine { util::Logger perfLog_{"Performance"}; util::Logger log_{"RPC"}; @@ -76,10 +74,9 @@ class RPCEngineBase std::shared_ptr handlerProvider_; detail::ForwardingProxy forwardingProxy_; - AdminVerificationStrategyType adminVerifier_; public: - RPCEngineBase( + RPCEngine( std::shared_ptr const& backend, std::shared_ptr const& subscriptions, std::shared_ptr const& balancer, @@ -98,7 +95,7 @@ public: { } - static std::shared_ptr + static std::shared_ptr make_RPCEngine( std::shared_ptr const& backend, std::shared_ptr const& subscriptions, @@ -108,7 +105,7 @@ public: Counters& counters, std::shared_ptr const& handlerProvider) { - return std::make_shared( + return std::make_shared( backend, subscriptions, balancer, dosGuard, workQueue, counters, handlerProvider); } @@ -142,8 +139,7 @@ public: { LOG(perfLog_.debug()) << ctx.tag() << " start executing rpc `" << ctx.method << '`'; - auto const isAdmin = adminVerifier_.isAdmin(ctx.clientIp); - auto const context = Context{ctx.yield, ctx.session, isAdmin, ctx.clientIp, ctx.apiVersion}; + auto const context = Context{ctx.yield, ctx.session, ctx.isAdmin, ctx.clientIp, ctx.apiVersion}; auto const v = (*method).process(ctx.params, context); LOG(perfLog_.debug()) << ctx.tag() << " finish executing rpc `" << ctx.method << '`'; @@ -282,6 +278,4 @@ private: } }; -using RPCEngine = RPCEngineBase; - } // namespace rpc diff --git a/src/web/Context.h b/src/web/Context.h index c8b8b7a9..773d4ce8 100644 --- a/src/web/Context.h +++ b/src/web/Context.h @@ -45,6 +45,7 @@ struct Context : util::Taggable std::shared_ptr session; data::LedgerRange range; std::string clientIp; + bool isAdmin; /** * @brief Create a new Context instance. @@ -57,6 +58,7 @@ struct Context : util::Taggable * @param tagFactory A factory that is used to generate tags to track requests and connections * @param range The ledger range that is available at the time of the request * @param clientIp IP of the peer + * @param isAdmin Whether the peer has admin privileges */ Context( boost::asio::yield_context yield, @@ -66,7 +68,8 @@ struct Context : util::Taggable std::shared_ptr const& session, util::TagDecoratorFactory const& tagFactory, data::LedgerRange const& range, - std::string clientIp) + std::string clientIp, + bool isAdmin) : Taggable(tagFactory) , yield(std::move(yield)) , method(std::move(command)) @@ -75,6 +78,7 @@ struct Context : util::Taggable , session(session) , range(range) , clientIp(std::move(clientIp)) + , isAdmin(isAdmin) { static util::Logger const perfLog{"Performance"}; LOG(perfLog.debug()) << tag() << "new Context created"; diff --git a/src/web/HttpSession.h b/src/web/HttpSession.h index 3b5ea36e..33210a9d 100644 --- a/src/web/HttpSession.h +++ b/src/web/HttpSession.h @@ -47,6 +47,7 @@ public: * * @param socket The socket. Ownership is transferred to HttpSession * @param ip Client's IP address + * @param adminPassword The optional password to verify admin role in requests * @param tagFactory A factory that is used to generate tags to track requests and sessions * @param dosGuard The denial of service guard to use * @param handler The server handler to use @@ -55,11 +56,18 @@ public: explicit HttpSession( tcp::socket&& socket, std::string const& ip, + std::optional adminPassword, std::reference_wrapper tagFactory, std::reference_wrapper dosGuard, std::shared_ptr const& handler, boost::beast::flat_buffer buffer) - : detail::HttpBase(ip, tagFactory, dosGuard, handler, std::move(buffer)) + : detail::HttpBase( + ip, + tagFactory, + std::move(adminPassword), + dosGuard, + handler, + std::move(buffer)) , stream_(std::move(socket)) , tagFactory_(tagFactory) { @@ -103,7 +111,8 @@ public: this->dosGuard_, this->handler_, std::move(this->buffer_), - std::move(this->req_)) + std::move(this->req_), + ConnectionBase::isAdmin()) ->run(); } }; diff --git a/src/web/PlainWsSession.h b/src/web/PlainWsSession.h index b5fd96ec..3dc791cd 100644 --- a/src/web/PlainWsSession.h +++ b/src/web/PlainWsSession.h @@ -46,6 +46,7 @@ public: * @param dosGuard The denial of service guard to use * @param handler The server handler to use * @param buffer Buffer with initial data received from the peer + * @param isAdmin Whether the connection has admin privileges */ explicit PlainWsSession( boost::asio::ip::tcp::socket&& socket, @@ -53,10 +54,12 @@ public: std::reference_wrapper tagFactory, std::reference_wrapper dosGuard, std::shared_ptr const& handler, - boost::beast::flat_buffer&& buffer) + boost::beast::flat_buffer&& buffer, + bool isAdmin) : detail::WsBase(ip, tagFactory, dosGuard, handler, std::move(buffer)) , ws_(std::move(socket)) { + ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer) } ~PlainWsSession() override = default; @@ -87,6 +90,7 @@ class WsUpgrader : public std::enable_shared_from_this> http::request req_; std::string ip_; std::shared_ptr const handler_; + bool isAdmin_; public: /** @@ -99,6 +103,7 @@ public: * @param handler The server handler to use * @param buffer Buffer with initial data received from the peer. Ownership is transferred * @param request The request. Ownership is transferred + * @param isAdmin Whether the connection has admin privileges */ WsUpgrader( boost::beast::tcp_stream&& stream, @@ -107,7 +112,8 @@ public: std::reference_wrapper dosGuard, std::shared_ptr const& handler, boost::beast::flat_buffer&& buffer, - http::request request) + http::request request, + bool isAdmin) : http_(std::move(stream)) , buffer_(std::move(buffer)) , tagFactory_(tagFactory) @@ -115,6 +121,7 @@ public: , req_(std::move(request)) , ip_(std::move(ip)) , handler_(handler) + , isAdmin_(isAdmin) { } @@ -150,7 +157,7 @@ private: boost::beast::get_lowest_layer(http_).expires_never(); std::make_shared>( - http_.release_socket(), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_)) + http_.release_socket(), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_), isAdmin_) ->run(std::move(req_)); } }; diff --git a/src/web/RPCServerHandler.h b/src/web/RPCServerHandler.h index 051fe9c4..1e550f0c 100644 --- a/src/web/RPCServerHandler.h +++ b/src/web/RPCServerHandler.h @@ -176,7 +176,8 @@ private: tagFactory_.with(connection->tag()), *range, connection->clientIp, - std::cref(apiVersionParser_)); + std::cref(apiVersionParser_), + connection->isAdmin()); }(); if (!context) diff --git a/src/web/Server.h b/src/web/Server.h index 33d4e7dd..45132d92 100644 --- a/src/web/Server.h +++ b/src/web/Server.h @@ -58,6 +58,7 @@ class Detector : public std::enable_shared_from_this const dosGuard_; std::shared_ptr const handler_; boost::beast::flat_buffer buffer_; + std::optional adminPassword_; public: /** @@ -68,18 +69,21 @@ public: * @param tagFactory A factory that is used to generate tags to track requests and sessions * @param dosGuard The denial of service guard to use * @param handler The server handler to use + * @param adminPassword The optional password to verify admin role in requests */ Detector( tcp::socket&& socket, std::optional> ctx, std::reference_wrapper tagFactory, std::reference_wrapper dosGuard, - std::shared_ptr const& handler) + std::shared_ptr const& handler, + std::optional adminPassword) : stream_(std::move(socket)) , ctx_(ctx) , tagFactory_(std::cref(tagFactory)) , dosGuard_(dosGuard) , handler_(handler) + , adminPassword_(std::move(adminPassword)) { } @@ -134,13 +138,20 @@ public: return fail(ec, "SSL is not supported by this server"); std::make_shared>( - stream_.release_socket(), ip, *ctx_, tagFactory_, dosGuard_, handler_, std::move(buffer_)) + stream_.release_socket(), + ip, + adminPassword_, + *ctx_, + tagFactory_, + dosGuard_, + handler_, + std::move(buffer_)) ->run(); return; } std::make_shared>( - stream_.release_socket(), ip, tagFactory_, dosGuard_, handler_, std::move(buffer_)) + stream_.release_socket(), ip, adminPassword_, tagFactory_, dosGuard_, handler_, std::move(buffer_)) ->run(); } }; @@ -166,6 +177,7 @@ class Server : public std::enable_shared_from_this dosGuard_; std::shared_ptr handler_; tcp::acceptor acceptor_; + std::optional adminPassword_; public: /** @@ -177,6 +189,7 @@ public: * @param tagFactory A factory that is used to generate tags to track requests and sessions * @param dosGuard The denial of service guard to use * @param handler The server handler to use + * @param adminPassword The optional password to verify admin role in requests */ Server( boost::asio::io_context& ioc, @@ -184,13 +197,15 @@ public: tcp::endpoint endpoint, util::TagDecoratorFactory tagFactory, web::DOSGuard& dosGuard, - std::shared_ptr const& handler) + std::shared_ptr const& handler, + std::optional adminPassword) : ioc_(std::ref(ioc)) , ctx_(ctx) , tagFactory_(tagFactory) , dosGuard_(std::ref(dosGuard)) , handler_(handler) , acceptor_(boost::asio::make_strand(ioc)) + , adminPassword_(std::move(adminPassword)) { boost::beast::error_code ec; @@ -244,7 +259,7 @@ private: ctx_ ? std::optional>{ctx_.value()} : std::nullopt; std::make_shared>( - std::move(socket), ctxRef, std::cref(tagFactory_), dosGuard_, handler_) + std::move(socket), ctxRef, std::cref(tagFactory_), dosGuard_, handler_, adminPassword_) ->run(); } @@ -282,9 +297,16 @@ make_HttpServer( auto const serverConfig = config.section("server"); auto const address = boost::asio::ip::make_address(serverConfig.value("ip")); auto const port = serverConfig.value("port"); + auto adminPassword = serverConfig.maybeValue("admin_password"); auto server = std::make_shared>( - ioc, ctx, boost::asio::ip::tcp::endpoint{address, port}, util::TagDecoratorFactory(config), dosGuard, handler); + ioc, + ctx, + boost::asio::ip::tcp::endpoint{address, port}, + util::TagDecoratorFactory(config), + dosGuard, + handler, + std::move(adminPassword)); server->run(); return server; diff --git a/src/web/SslHttpSession.h b/src/web/SslHttpSession.h index 444416f7..99d90e5d 100644 --- a/src/web/SslHttpSession.h +++ b/src/web/SslHttpSession.h @@ -47,6 +47,7 @@ public: * * @param socket The socket. Ownership is transferred to HttpSession * @param ip Client's IP address + * @param adminPassword The optional password to verify admin role in requests * @param ctx The SSL context * @param tagFactory A factory that is used to generate tags to track requests and sessions * @param dosGuard The denial of service guard to use @@ -56,12 +57,19 @@ public: explicit SslHttpSession( tcp::socket&& socket, std::string const& ip, + std::optional adminPassword, boost::asio::ssl::context& ctx, std::reference_wrapper tagFactory, std::reference_wrapper dosGuard, std::shared_ptr const& handler, boost::beast::flat_buffer buffer) - : detail::HttpBase(ip, tagFactory, dosGuard, handler, std::move(buffer)) + : detail::HttpBase( + ip, + tagFactory, + std::move(adminPassword), + dosGuard, + handler, + std::move(buffer)) , stream_(std::move(socket), ctx) , tagFactory_(tagFactory) { @@ -142,7 +150,8 @@ public: this->dosGuard_, this->handler_, std::move(this->buffer_), - std::move(this->req_)) + std::move(this->req_), + ConnectionBase::isAdmin()) ->run(); } }; diff --git a/src/web/SslWsSession.h b/src/web/SslWsSession.h index e4e7f96a..3e1d2a9b 100644 --- a/src/web/SslWsSession.h +++ b/src/web/SslWsSession.h @@ -21,6 +21,8 @@ #include +#include + #include namespace web { @@ -46,6 +48,7 @@ public: * @param dosGuard The denial of service guard to use * @param handler The server handler to use * @param buffer Buffer with initial data received from the peer + * @param isAdmin Whether the connection has admin privileges */ explicit SslWsSession( boost::beast::ssl_stream&& stream, @@ -53,10 +56,12 @@ public: std::reference_wrapper tagFactory, std::reference_wrapper dosGuard, std::shared_ptr const& handler, - boost::beast::flat_buffer&& buffer) + boost::beast::flat_buffer&& buffer, + bool isAdmin) : detail::WsBase(ip, tagFactory, dosGuard, handler, std::move(buffer)) , ws_(std::move(stream)) { + ConnectionBase::isAdmin_ = isAdmin; // NOLINT(cppcoreguidelines-prefer-member-initializer) } /** @return The secure websocket stream. */ @@ -85,6 +90,7 @@ class SslWsUpgrader : public std::enable_shared_from_this dosGuard_; std::shared_ptr const handler_; http::request req_; + bool isAdmin_; public: /** @@ -97,6 +103,7 @@ public: * @param handler The server handler to use * @param buffer Buffer with initial data received from the peer. Ownership is transferred * @param request The request. Ownership is transferred + * @param isAdmin Whether the connection has admin privileges */ SslWsUpgrader( boost::beast::ssl_stream stream, @@ -105,7 +112,8 @@ public: std::reference_wrapper dosGuard, std::shared_ptr const& handler, boost::beast::flat_buffer&& buffer, - http::request request) + http::request request, + bool isAdmiin) : https_(std::move(stream)) , buffer_(std::move(buffer)) , ip_(std::move(ip)) @@ -113,6 +121,7 @@ public: , dosGuard_(dosGuard) , handler_(handler) , req_(std::move(request)) + , isAdmin_(isAdmiin) { } @@ -153,7 +162,7 @@ private: boost::beast::get_lowest_layer(https_).expires_never(); std::make_shared>( - std::move(https_), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_)) + std::move(https_), ip_, tagFactory_, dosGuard_, handler_, std::move(buffer_), isAdmin_) ->run(std::move(req_)); } }; diff --git a/src/rpc/common/impl/AdminVerificationStrategy.h b/src/web/impl/AdminVerificationStrategy.cpp similarity index 52% rename from src/rpc/common/impl/AdminVerificationStrategy.h rename to src/web/impl/AdminVerificationStrategy.cpp index ed0aacab..e8c965d7 100644 --- a/src/rpc/common/impl/AdminVerificationStrategy.h +++ b/src/web/impl/AdminVerificationStrategy.cpp @@ -17,26 +17,42 @@ */ //============================================================================== -#pragma once +#include -#include +namespace web::detail { -namespace rpc::detail { - -class IPAdminVerificationStrategy final +bool +IPAdminVerificationStrategy::isAdmin(RequestType const&, std::string_view ip) const { -public: - /** - * @brief Checks whether request is from a host that is considered authorized as admin. - * - * @param ip The ip addr of the client - * @return true if authorized; false otherwise - */ - static bool - isAdmin(std::string_view ip) - { - return ip == "127.0.0.1"; - } -}; + return ip == "127.0.0.1"; +} -} // namespace rpc::detail +PasswordAdminVerificationStrategy::PasswordAdminVerificationStrategy(std::string password) + : password_(std::move(password)) +{ +} + +bool +PasswordAdminVerificationStrategy::isAdmin(RequestType const& request, std::string_view) const +{ + auto it = request.find(boost::beast::http::field::authorization); + if (it == request.end()) + { + // No Authorization header + return false; + } + + return it->value() == password_; +} + +std::unique_ptr +make_AdminVerificationStrategy(std::optional password) +{ + if (password.has_value()) + { + return std::make_unique(std::move(*password)); + } + return std::make_unique(); +} + +} // namespace web::detail diff --git a/src/web/impl/AdminVerificationStrategy.h b/src/web/impl/AdminVerificationStrategy.h new file mode 100644 index 00000000..67b23fb9 --- /dev/null +++ b/src/web/impl/AdminVerificationStrategy.h @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + 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 + +#include +#include + +namespace web::detail { + +class AdminVerificationStrategy +{ +public: + using RequestType = boost::beast::http::request; + virtual ~AdminVerificationStrategy() = default; + + /** + * @brief Checks whether request is from a host that is considered authorized as admin. + * + * @param request The http request from the client + * @param ip The ip addr of the client + * @return true if authorized; false otherwise + */ + virtual bool + isAdmin(RequestType const& request, std::string_view ip) const = 0; +}; + +class IPAdminVerificationStrategy : public AdminVerificationStrategy +{ +public: + /** + * @brief Checks whether request is from a host that is considered authorized as admin + * by checking the ip address. + * + * @param ip The ip addr of the client + * @return true if authorized; false otherwise + */ + bool + isAdmin(RequestType const&, std::string_view ip) const override; +}; + +class PasswordAdminVerificationStrategy : public AdminVerificationStrategy +{ +private: + std::string password_; + +public: + PasswordAdminVerificationStrategy(std::string password); + + /** + * @brief Checks whether request is from a host that is considered authorized as admin using + * the password (if any) from the request. + * + * @param request The request from a host + * @return true if the password from request matches admin password from config + */ + bool + isAdmin(RequestType const& request, std::string_view) const override; +}; + +std::unique_ptr +make_AdminVerificationStrategy(std::optional password); + +} // namespace web::detail diff --git a/src/web/impl/HttpBase.h b/src/web/impl/HttpBase.h index 4ce6dda5..a6f80ff9 100644 --- a/src/web/impl/HttpBase.h +++ b/src/web/impl/HttpBase.h @@ -22,6 +22,7 @@ #include
#include #include +#include #include #include @@ -86,6 +87,7 @@ class HttpBase : public ConnectionBase std::shared_ptr res_; SendLambda sender_; + std::unique_ptr adminVerification_; protected: boost::beast::flat_buffer buffer_; @@ -130,11 +132,13 @@ public: HttpBase( std::string const& ip, std::reference_wrapper tagFactory, + std::optional adminPassword, std::reference_wrapper dosGuard, std::shared_ptr const& handler, boost::beast::flat_buffer buffer) : ConnectionBase(tagFactory, ip) , sender_(*this) + , adminVerification_(make_AdminVerificationStrategy(std::move(adminPassword))) , buffer_(std::move(buffer)) , dosGuard_(dosGuard) , handler_(handler) @@ -178,6 +182,9 @@ public: if (ec) return httpFail(ec, "read"); + // Update isAdmin property of the connection + ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp); + if (boost::beast::websocket::is_upgrade(req_)) { upgraded = true; diff --git a/src/web/interface/ConnectionBase.h b/src/web/interface/ConnectionBase.h index 60211b4a..fe11e43f 100644 --- a/src/web/interface/ConnectionBase.h +++ b/src/web/interface/ConnectionBase.h @@ -41,6 +41,7 @@ protected: public: std::string const clientIp; bool upgraded = false; + bool isAdmin_ = false; /** * @brief Create a new connection base. @@ -85,5 +86,16 @@ public: { return ec_ != boost::system::error_code{}; } + + /** + * @brief Indicates whether the connection has admin privileges + * + * @return true if the connection is from admin user + */ + [[nodiscard]] bool + isAdmin() const + { + return isAdmin_; + } }; } // namespace web diff --git a/unittests/rpc/AdminVerificationTests.cpp b/unittests/rpc/AdminVerificationTests.cpp deleted file mode 100644 index 513b6ba9..00000000 --- a/unittests/rpc/AdminVerificationTests.cpp +++ /dev/null @@ -1,40 +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. -*/ -//============================================================================== - -#include - -#include - -#include -#include - -class RPCAdminVerificationTest : public NoLoggerFixture -{ -protected: - rpc::detail::IPAdminVerificationStrategy strat_; -}; - -TEST_F(RPCAdminVerificationTest, IsAdminOnlyForIP_127_0_0_1) -{ - EXPECT_TRUE(strat_.isAdmin("127.0.0.1")); - EXPECT_FALSE(strat_.isAdmin("127.0.0.2")); - EXPECT_FALSE(strat_.isAdmin("127")); - EXPECT_FALSE(strat_.isAdmin("")); - EXPECT_FALSE(strat_.isAdmin("localhost")); -} diff --git a/unittests/rpc/ForwardingProxyTests.cpp b/unittests/rpc/ForwardingProxyTests.cpp index effb8e8d..f50be415 100644 --- a/unittests/rpc/ForwardingProxyTests.cpp +++ b/unittests/rpc/ForwardingProxyTests.cpp @@ -63,7 +63,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsFalseIfClioOnly) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -83,7 +83,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfProxied) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -103,7 +103,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfCurrentLedgerSpecified) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -123,7 +123,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfClosedLedgerSpecified) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -143,7 +143,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfAccountInfoWithQueueSpe runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -163,7 +163,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfLedgerWithQueueSpecifie runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -183,7 +183,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfLedgerWithFullSpecified runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -203,7 +203,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsTrueIfLedgerWithAccountsSpeci runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_TRUE(res); @@ -223,7 +223,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsFalseIfAccountInfoQueueIsFals runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -243,7 +243,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsFalseIfLedgerQueueIsFalse) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -263,7 +263,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsFalseIfLedgerFullIsFalse) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -283,7 +283,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsFalseIfLedgerAccountsIsFalse) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -303,7 +303,7 @@ TEST_F(RPCForwardingProxyTest, ShouldNotForwardReturnsTrueIfAPIVersionIsV1) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -323,7 +323,7 @@ TEST_F(RPCForwardingProxyTest, ShouldForwardReturnsFalseIfAPIVersionIsV2) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -339,7 +339,7 @@ TEST_F(RPCForwardingProxyTest, ShouldNeverForwardSubscribe) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -355,7 +355,7 @@ TEST_F(RPCForwardingProxyTest, ShouldNeverForwardUnsubscribe) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.shouldForward(ctx); ASSERT_FALSE(res); @@ -383,7 +383,7 @@ TEST_F(RPCForwardingProxyTest, ForwardCallsBalancerWithCorrectParams) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.forward(ctx); @@ -413,7 +413,7 @@ TEST_F(RPCForwardingProxyTest, ForwardingFailYieldsErrorStatus) runSpawn([&](auto yield) { auto const range = mockBackendPtr->fetchLedgerRange(); auto const ctx = - web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP); + web::Context(yield, method, apiVersion, params.as_object(), nullptr, tagFactory, *range, CLIENT_IP, true); auto const res = proxy.forward(ctx); diff --git a/unittests/util/TestHttpSyncClient.h b/unittests/util/TestHttpSyncClient.h index 63be86d8..4f90a253 100644 --- a/unittests/util/TestHttpSyncClient.h +++ b/unittests/util/TestHttpSyncClient.h @@ -31,10 +31,23 @@ 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) + syncPost( + std::string const& host, + std::string const& port, + std::string const& body, + std::vector additionalHeaders = {}) { boost::asio::io_context ioc; @@ -47,6 +60,12 @@ struct HttpSyncClient http::request req{http::verb::post, "/", 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, std::move(header.value)); + } + req.body() = std::string(body); req.prepare_payload(); http::write(stream, req); @@ -70,7 +89,7 @@ class WebSocketSyncClient public: void - connect(std::string const& host, std::string const& port) + connect(std::string const& host, std::string const& port, std::vector additionalHeaders = {}) { auto const results = resolver_.resolve(host, port); auto const ep = net::connect(ws_.next_layer(), results); @@ -80,9 +99,14 @@ public: // 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([](boost::beast::websocket::request_type& req) { - req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); - })); + 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& header : additionalHeaders) + { + req.set(header.name, std::move(header.value)); + } + })); ws_.handshake(hostPort, "/"); } diff --git a/unittests/web/AdminVerificationTests.cpp b/unittests/web/AdminVerificationTests.cpp new file mode 100644 index 00000000..53e8cc6b --- /dev/null +++ b/unittests/web/AdminVerificationTests.cpp @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#include + +#include + +#include +#include + +namespace http = boost::beast::http; + +class IPAdminVerificationStrategyTest : public NoLoggerFixture +{ +protected: + web::detail::IPAdminVerificationStrategy strat_; + http::request request_ = {}; +}; + +TEST_F(IPAdminVerificationStrategyTest, IsAdminOnlyForIP_127_0_0_1) +{ + EXPECT_TRUE(strat_.isAdmin(request_, "127.0.0.1")); + EXPECT_FALSE(strat_.isAdmin(request_, "127.0.0.2")); + EXPECT_FALSE(strat_.isAdmin(request_, "127")); + EXPECT_FALSE(strat_.isAdmin(request_, "")); + EXPECT_FALSE(strat_.isAdmin(request_, "localhost")); +} + +class PasswordAdminVerificationStrategyTest : public NoLoggerFixture +{ +protected: + const std::string password_ = "secret"; + web::detail::PasswordAdminVerificationStrategy strat_{password_}; + + static http::request + makeRequest(std::string const& password, http::field const field = http::field::authorization) + { + http::request request = {}; + request.set(field, password); + return request; + } +}; + +TEST_F(PasswordAdminVerificationStrategyTest, IsAdminReturnsTrueOnlyForValidPasswordInAuthHeader) +{ + EXPECT_TRUE(strat_.isAdmin(makeRequest(password_), "")); + EXPECT_TRUE(strat_.isAdmin(makeRequest(password_), "123")); + + // Wrong password + EXPECT_FALSE(strat_.isAdmin(makeRequest("SECRET"), "")); + EXPECT_FALSE(strat_.isAdmin(makeRequest("SECRET"), "127.0.0.1")); + EXPECT_FALSE(strat_.isAdmin(makeRequest("S"), "127.0.0.1")); + EXPECT_FALSE(strat_.isAdmin(makeRequest("SeCret"), "127.0.0.1")); + EXPECT_FALSE(strat_.isAdmin(makeRequest("secre"), "127.0.0.1")); + EXPECT_FALSE(strat_.isAdmin(makeRequest("s"), "127.0.0.1")); + EXPECT_FALSE(strat_.isAdmin(makeRequest("a"), "127.0.0.1")); + + // Wrong header + EXPECT_FALSE(strat_.isAdmin(makeRequest(password_, http::field::authentication_info), "")); +} + +struct MakeAdminVerificationStrategyTestParams +{ + MakeAdminVerificationStrategyTestParams( + std::optional passwordOpt, + bool expectIpStrategy, + bool expectPasswordStrategy) + : passwordOpt(std::move(passwordOpt)) + , expectIpStrategy(expectIpStrategy) + , expectPasswordStrategy(expectPasswordStrategy) + { + } + std::optional passwordOpt; + bool expectIpStrategy; + bool expectPasswordStrategy; +}; + +class MakeAdminVerificationStrategyTest : public testing::TestWithParam +{ +}; + +TEST_P(MakeAdminVerificationStrategyTest, ChoosesStrategyCorrectly) +{ + auto strat = web::detail::make_AdminVerificationStrategy(GetParam().passwordOpt); + auto ipStrat = dynamic_cast(strat.get()); + EXPECT_EQ(ipStrat != nullptr, GetParam().expectIpStrategy); + auto passwordStrat = dynamic_cast(strat.get()); + EXPECT_EQ(passwordStrat != nullptr, GetParam().expectPasswordStrategy); +} + +INSTANTIATE_TEST_CASE_P( + MakeAdminVerificationStrategyTest, + MakeAdminVerificationStrategyTest, + testing::Values( + MakeAdminVerificationStrategyTestParams(std::nullopt, true, false), + MakeAdminVerificationStrategyTestParams("p", false, true), + MakeAdminVerificationStrategyTestParams("", false, true))); diff --git a/unittests/web/ServerTests.cpp b/unittests/web/ServerTests.cpp index 0eed3f43..7f169b76 100644 --- a/unittests/web/ServerTests.cpp +++ b/unittests/web/ServerTests.cpp @@ -360,3 +360,76 @@ TEST_F(WebServerTest, WsPayloadOverload) res, R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})"); } + +static auto constexpr JSONServerConfigWithAdminPassword = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888, + "admin_password": "secret" + } + } +)JSON"; + +class AdminCheckExecutor +{ +public: + void + operator()(std::string const& reqStr, std::shared_ptr const& ws) + { + auto response = fmt::format("{} {}", reqStr, ws->isAdmin() ? "admin" : "user"); + ws->send(std::move(response), http::status::ok); + } + + void + operator()(boost::beast::error_code /* ec */, std::shared_ptr const& /* ws */) + { + } +}; + +struct WebServerAdminTestParams +{ + std::vector headers; + std::string expectedResponse; +}; + +class WebServerAdminTest : public WebServerTest, public ::testing::WithParamInterface +{ +}; + +TEST_P(WebServerAdminTest, WsAdminCheck) +{ + auto e = std::make_shared(); + Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword)}; + auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuardOverload, e); + WebSocketSyncClient wsClient; + wsClient.connect("localhost", "8888", GetParam().headers); + std::string const request = "Why hello"; + auto const res = wsClient.syncPost(request); + wsClient.disconnect(); + EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse)); +} + +TEST_P(WebServerAdminTest, HttpAdminCheck) +{ + auto e = std::make_shared(); + Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword)}; + auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuardOverload, e); + std::string const request = "Why hello"; + auto const res = HttpSyncClient::syncPost("localhost", "8888", request, GetParam().headers); + EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse)); +} + +INSTANTIATE_TEST_CASE_P( + WebServerAdminTestsSuit, + WebServerAdminTest, + ::testing::Values( + WebServerAdminTestParams{.headers = {}, .expectedResponse = "user"}, + WebServerAdminTestParams{.headers = {WebHeader(http::field::authorization, "")}, .expectedResponse = "user"}, + WebServerAdminTestParams{.headers = {WebHeader(http::field::authorization, "s")}, .expectedResponse = "user"}, + WebServerAdminTestParams{ + .headers = {WebHeader(http::field::authorization, "secret")}, + .expectedResponse = "admin"}, + WebServerAdminTestParams{ + .headers = {WebHeader(http::field::authentication_info, "secret")}, + .expectedResponse = "user"}));