diff --git a/example-config.json b/example-config.json index 65f9018f..ee83837c 100644 --- a/example-config.json +++ b/example-config.json @@ -66,7 +66,12 @@ // Max number of requests to queue up before rejecting further requests. // Defaults to 0, which disables the limit. "max_queue_size": 500, - "admin_password": "xrp" + // If request contains header with authorization, Clio will check if it matches this value's hash + // If matches, the request will be considered as admin request + "admin_password": "xrp", + // If local_admin is true, Clio will consider requests come from 127.0.0.1 as admin requests + // It's true by default unless admin_password is set,'local_admin' : true and 'admin_password' can not be set at the same time + "local_amdin": false }, // Overrides log level on a per logging channel. // Defaults to global "log_level" for each unspecified channel. diff --git a/src/web/Server.h b/src/web/Server.h index a7af5cb6..282e8540 100644 --- a/src/web/Server.h +++ b/src/web/Server.h @@ -298,6 +298,20 @@ make_HttpServer( auto const address = boost::asio::ip::make_address(serverConfig.value("ip")); auto const port = serverConfig.value("port"); auto adminPassword = serverConfig.maybeValue("admin_password"); + auto const localAdmin = serverConfig.maybeValue("local_admin"); + + // Throw config error when localAdmin is true and admin_password is also set + if (localAdmin && localAdmin.value() && adminPassword) { + LOG(log.error()) << "local_admin is true but admin_password is also set, please specify only one method " + "to authorize admin"; + throw std::logic_error("Admin config error, local_admin and admin_password can not be set together."); + } + // Throw config error when localAdmin is false but admin_password is not set + if (localAdmin && !localAdmin.value() && !adminPassword) { + LOG(log.error()) << "local_admin is false but admin_password is not set, please specify one method " + "to authorize admin"; + throw std::logic_error("Admin config error, one method must be specified to authorize admin."); + } auto server = std::make_shared>( ioc, diff --git a/src/web/impl/AdminVerificationStrategy.cpp b/src/web/impl/AdminVerificationStrategy.cpp index 34642831..b26e6786 100644 --- a/src/web/impl/AdminVerificationStrategy.cpp +++ b/src/web/impl/AdminVerificationStrategy.cpp @@ -19,6 +19,8 @@ #include +#include + namespace web::detail { bool @@ -27,9 +29,16 @@ IPAdminVerificationStrategy::isAdmin(RequestType const&, std::string_view ip) co return ip == "127.0.0.1"; } -PasswordAdminVerificationStrategy::PasswordAdminVerificationStrategy(std::string password) - : password_(std::move(password)) +PasswordAdminVerificationStrategy::PasswordAdminVerificationStrategy(std::string const& password) { + ripple::sha256_hasher hasher; + hasher(password.data(), password.size()); + auto const d = static_cast(hasher); + ripple::uint256 sha256; + std::memcpy(sha256.data(), d.data(), d.size()); + passwordSha256_ = ripple::to_string(sha256); + // make sure it's uppercase + std::transform(passwordSha256_.begin(), passwordSha256_.end(), passwordSha256_.begin(), ::toupper); } bool @@ -40,8 +49,9 @@ PasswordAdminVerificationStrategy::isAdmin(RequestType const& request, std::stri // No Authorization header return false; } - - return it->value() == password_; + std::string userAuth(it->value()); + std::transform(userAuth.begin(), userAuth.end(), userAuth.begin(), ::toupper); + return passwordSha256_ == userAuth; } std::unique_ptr diff --git a/src/web/impl/AdminVerificationStrategy.h b/src/web/impl/AdminVerificationStrategy.h index 3815a321..156e6103 100644 --- a/src/web/impl/AdminVerificationStrategy.h +++ b/src/web/impl/AdminVerificationStrategy.h @@ -57,10 +57,10 @@ public: class PasswordAdminVerificationStrategy : public AdminVerificationStrategy { private: - std::string password_; + std::string passwordSha256_; public: - PasswordAdminVerificationStrategy(std::string password); + PasswordAdminVerificationStrategy(std::string const& password); /** * @brief Checks whether request is from a host that is considered authorized as admin using diff --git a/unittests/web/AdminVerificationTests.cpp b/unittests/web/AdminVerificationTests.cpp index 67fd92cc..86560f37 100644 --- a/unittests/web/AdminVerificationTests.cpp +++ b/unittests/web/AdminVerificationTests.cpp @@ -44,6 +44,8 @@ TEST_F(IPAdminVerificationStrategyTest, IsAdminOnlyForIP_127_0_0_1) class PasswordAdminVerificationStrategyTest : public NoLoggerFixture { protected: const std::string password_ = "secret"; + const std::string passwordHash_ = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b"; + web::detail::PasswordAdminVerificationStrategy strat_{password_}; static http::request @@ -57,8 +59,8 @@ protected: TEST_F(PasswordAdminVerificationStrategyTest, IsAdminReturnsTrueOnlyForValidPasswordInAuthHeader) { - EXPECT_TRUE(strat_.isAdmin(makeRequest(password_), "")); - EXPECT_TRUE(strat_.isAdmin(makeRequest(password_), "123")); + EXPECT_TRUE(strat_.isAdmin(makeRequest(passwordHash_), "")); + EXPECT_TRUE(strat_.isAdmin(makeRequest(passwordHash_), "123")); // Wrong password EXPECT_FALSE(strat_.isAdmin(makeRequest("SECRET"), "")); @@ -70,7 +72,7 @@ TEST_F(PasswordAdminVerificationStrategyTest, IsAdminReturnsTrueOnlyForValidPass EXPECT_FALSE(strat_.isAdmin(makeRequest("a"), "127.0.0.1")); // Wrong header - EXPECT_FALSE(strat_.isAdmin(makeRequest(password_, http::field::authentication_info), "")); + EXPECT_FALSE(strat_.isAdmin(makeRequest(passwordHash_, http::field::authentication_info), "")); } struct MakeAdminVerificationStrategyTestParams { diff --git a/unittests/web/ServerTests.cpp b/unittests/web/ServerTests.cpp index 34ab712d..5da4e41a 100644 --- a/unittests/web/ServerTests.cpp +++ b/unittests/web/ServerTests.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -398,6 +399,49 @@ static auto constexpr JSONServerConfigWithAdminPassword = R"JSON( } )JSON"; +static auto constexpr JSONServerConfigWithAdminPasswordWithFalseLocalAdmin = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888, + "admin_password": "secret" + } + } +)JSON"; + +static auto constexpr JSONServerConfigWithLocalAdmin = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888, + "local_admin": true + } + } +)JSON"; + +static auto constexpr JSONServerConfigWithBothAdminPasswordAndLocalAdminFalse = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888, + "admin_password": "secret", + "local_admin": false + } + } +)JSON"; + +static auto constexpr JSONServerConfigWithNoSpecifiedAdmin = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888 + } + } +)JSON"; + +// get this value from online sha256 generator +static auto constexpr SecertSha256 = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b"; + class AdminCheckExecutor { public: void @@ -414,6 +458,7 @@ public: }; struct WebServerAdminTestParams { + std::string config; std::vector headers; std::string expectedResponse; }; @@ -423,7 +468,7 @@ class WebServerAdminTest : public WebServerTest, public ::testing::WithParamInte TEST_P(WebServerAdminTest, WsAdminCheck) { auto e = std::make_shared(); - Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword)}; + Config const serverConfig{boost::json::parse(GetParam().config)}; auto server = makeServerSync(serverConfig, ctx, std::nullopt, dosGuardOverload, e); WebSocketSyncClient wsClient; wsClient.connect("localhost", "8888", GetParam().headers); @@ -436,7 +481,7 @@ TEST_P(WebServerAdminTest, WsAdminCheck) TEST_P(WebServerAdminTest, HttpAdminCheck) { auto e = std::make_shared(); - Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword)}; + Config const serverConfig{boost::json::parse(GetParam().config)}; 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); @@ -447,14 +492,74 @@ 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")}, + .config = JSONServerConfigWithAdminPassword, + .headers = {}, + .expectedResponse = "user"}, + WebServerAdminTestParams{ + .config = JSONServerConfigWithAdminPassword, + .headers = {WebHeader(http::field::authorization, "")}, + .expectedResponse = "user"}, + WebServerAdminTestParams{ + .config = JSONServerConfigWithAdminPassword, + .headers = {WebHeader(http::field::authorization, "s")}, + .expectedResponse = "user"}, + WebServerAdminTestParams{ + .config = JSONServerConfigWithAdminPassword, + .headers = {WebHeader(http::field::authorization, SecertSha256)}, .expectedResponse = "admin"}, WebServerAdminTestParams{ - .headers = {WebHeader(http::field::authentication_info, "secret")}, - .expectedResponse = "user"} + .config = JSONServerConfigWithAdminPasswordWithFalseLocalAdmin, + .headers = {WebHeader(http::field::authorization, SecertSha256)}, + .expectedResponse = "admin"}, + WebServerAdminTestParams{ + .config = JSONServerConfigWithBothAdminPasswordAndLocalAdminFalse, + .headers = {WebHeader(http::field::authorization, SecertSha256)}, + .expectedResponse = "admin"}, + WebServerAdminTestParams{ + .config = JSONServerConfigWithAdminPassword, + .headers = {WebHeader(http::field::authentication_info, SecertSha256)}, + .expectedResponse = "user"}, + WebServerAdminTestParams{.config = JSONServerConfigWithLocalAdmin, .headers = {}, .expectedResponse = "admin"}, + WebServerAdminTestParams{ + .config = JSONServerConfigWithNoSpecifiedAdmin, + .headers = {}, + .expectedResponse = "admin"} + ) ); + +TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminSet) +{ + static auto constexpr JSONServerConfigWithBothAdminPasswordAndLocalAdmin = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888, + "admin_password": "secret", + "local_admin": true + } + } + )JSON"; + + auto e = std::make_shared(); + Config const serverConfig{boost::json::parse(JSONServerConfigWithBothAdminPasswordAndLocalAdmin)}; + EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, std::nullopt, dosGuardOverload, e), std::logic_error); +} + +TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse) +{ + static auto constexpr JSONServerConfigWithNoAdminPasswordAndLocalAdminFalse = R"JSON( + { + "server":{ + "ip": "0.0.0.0", + "port": 8888, + "local_admin": false + } + } + )JSON"; + + auto e = std::make_shared(); + Config const serverConfig{boost::json::parse(JSONServerConfigWithNoAdminPasswordAndLocalAdminFalse)}; + EXPECT_THROW(web::make_HttpServer(serverConfig, ctx, std::nullopt, dosGuardOverload, e), std::logic_error); +}