From cabc1caead6e1d3c5e107db7f48642a83e6b8961 Mon Sep 17 00:00:00 2001 From: Christopher Blauvelt Date: Tue, 8 Feb 2022 21:48:24 -0500 Subject: [PATCH] Add support for Auth Fixes #3 --- .gitignore | 3 +- .vscode/tasks.json | 6 +++ conanfile.py | 2 +- include/redis_client_config.hpp | 29 ++++++++++++ src/redis_client.cpp | 35 ++++++++++++++- test/redis_client_test.cpp | 72 +++++++++++++++++++++++++++--- test/redis_sub_connection_test.cpp | 8 ++-- test/redis_sub_test.cpp | 8 ++-- 8 files changed, 147 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 3b5cbd9..5e70e21 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ build/** *.app # Testing -Testing/ \ No newline at end of file +Testing/ +.env \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1b6f145..5726225 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -43,5 +43,11 @@ "command": "rm -rf build/*", "problemMatcher": [] }, + { + "label": "Conan Create", + "type": "shell", + "command": "conan create .", + "problemMatcher": [] + }, ] } \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 884bb1a..4fb958e 100644 --- a/conanfile.py +++ b/conanfile.py @@ -20,7 +20,7 @@ class RedisClientConan(ConanFile): exports_sources = ["CMakeLists.txt", "conan.cmake", "conanfile.py", "include/*", "src/*", "test/*"] generators = "cmake" settings = "os", "arch", "compiler", "build_type" - requires = "cpool/0.9.4", "boost/1.78.0", "openssl/1.1.1m", "fmt/8.1.1" + requires = "cpool/0.9.5", "boost/1.78.0", "openssl/1.1.1m", "fmt/8.1.1" build_requires = "gtest/cci.20210126" options = {"cxx_standard": [20], "build_testing": [True, False]} default_options = {"cxx_standard": 20, "build_testing": True} diff --git a/include/redis_client_config.hpp b/include/redis_client_config.hpp index 82bc165..8bd6bb8 100644 --- a/include/redis_client_config.hpp +++ b/include/redis_client_config.hpp @@ -26,6 +26,13 @@ struct redis_client_config { /// disconnect from the server will be attempted again bool rety_failed_commands; + /// username Used for authentication with the redis server. If password is + /// defined and username is blank, it is set to "default". + std::string username; + + /// password Used for authentication with the redis server + std::string password; + /// Creates a configuration with default parameters redis_client_config() : host("127.0.0.1") @@ -55,6 +62,28 @@ struct redis_client_config { return *this; } + /** + * @brief Sets the username of the server. + * @param username The username to authenticate with the redis server. + * @returns The configuration object so subsequent commands to set methods + * can be chained. + */ + redis_client_config set_username(std::string username) { + this->username = username; + return *this; + } + + /** + * @brief Sets the password of the server. + * @param password The password to authenticate with the redis server. + * @returns The configuration object so subsequent commands to set methods + * can be chained. + */ + redis_client_config set_password(std::string password) { + this->password = password; + return *this; + } + /** * @brief Sets the maximum connections in the connection pool. * @param num_connections The max number of connections. diff --git a/src/redis_client.cpp b/src/redis_client.cpp index 733f616..4f9f330 100644 --- a/src/redis_client.cpp +++ b/src/redis_client.cpp @@ -13,8 +13,39 @@ redis_client::redis_client(cpool::net::any_io_executor exec, , state_(state::not_running) { auto conn_creator = [&]() -> std::unique_ptr { - return std::make_unique(exec_, config_.host, - config_.port); + auto conn = std::make_unique(exec_, config_.host, + config_.port); + if (!config_.password.empty()) { + if (config_.username.empty()) { + config_.username = "default"; + } + + // login when a connection is created + conn->set_state_change_handler( + [&](cpool::tcp_connection* conn, + const cpool::client_connection_state state) + -> awaitable { + if (state == cpool::client_connection_state::connected) { + auto username = this->config().username; + auto password = this->config().password; + auto loginCmd = redis_command(std::vector{ + "AUTH", username, password}); + + this->log_message(redis::log_level::trace, + "AUTH password"); + auto reply = co_await this->send(conn, loginCmd); + if (reply.error()) { + this->log_message(redis::log_level::error, + reply.error().message()); + } + co_return reply.error(); + } + + co_return cpool::error(); + }); + } + + return conn; }; con_pool_ = std::make_unique>( diff --git a/test/redis_client_test.cpp b/test/redis_client_test.cpp index 15e8a73..21dba14 100644 --- a/test/redis_client_test.cpp +++ b/test/redis_client_test.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "commands-json.hpp" #include "commands.hpp" #include "redis_client.hpp" @@ -15,9 +17,12 @@ namespace { using namespace redis; -std::string get_env_var(std::string const& key) { +const std::string DEFAULT_REDIS_HOST = "host.docker.internal"; +const std::string DEFAULT_REDIS_PORT = "6379"; + +std::optional get_env_var(std::string const& key) { char* val = getenv(key.c_str()); - return val == NULL ? std::string("redis") : std::string(val); + return (val == NULL) ? std::nullopt : std::optional(std::string(val)); } void testForError(std::string cmd, const redis::redis_reply& reply) { @@ -128,7 +133,7 @@ awaitable test_basic(redis_client& client, int c, awaitable run_tests(asio::io_context& ctx) { std::atomic barrier; auto exec = co_await cpool::net::this_coro::executor; - auto host = get_env_var("REDIS_HOST"); + auto host = get_env_var("REDIS_HOST").value_or(DEFAULT_REDIS_HOST); logMessage(redis::log_level::info, host); redis_client client(exec, host, 6379); @@ -147,7 +152,46 @@ awaitable run_tests(asio::io_context& ctx) { while (barrier != 0) { cpool::timer timer(exec); - co_await timer.async_wait(std::chrono::milliseconds(100)); + co_await timer.async_wait(100ms); + } + + ctx.stop(); + co_return; +} + +awaitable run_password_tests(asio::io_context& ctx) { + std::atomic barrier; + auto exec = co_await cpool::net::this_coro::executor; + auto host = get_env_var("REDIS_PASSWORD_HOST").value_or(DEFAULT_REDIS_HOST); + auto portString = + get_env_var("REDIS_PASSWORD_PORT").value_or(DEFAULT_REDIS_PORT); + int port = std::stoi(portString); + auto password = get_env_var("REDIS_PASSWORD").value_or(""); + auto config = + redis_client_config{}.set_host(host).set_port(port).set_password( + password); + + logMessage(redis::log_level::info, + fmt::format("Logging into {}:{} with password {}", config.host, + config.port, config.password)); + + redis_client client(exec, config); + client.set_logging_handler( + std::bind(logMessage, std::placeholders::_1, std::placeholders::_2)); + + auto reply = co_await client.ping(); + testForString("PING", reply, "PONG"); + EXPECT_TRUE(client.running()); + + barrier = 2; + int num_runners = barrier; + for (int i = 0; i < num_runners; i++) { + cpool::co_spawn(ctx, test_basic(client, i, barrier), cpool::detached); + } + + while (barrier != 0) { + cpool::timer timer(exec); + co_await timer.async_wait(100ms); } ctx.stop(); @@ -276,7 +320,7 @@ awaitable test_json(redis_client& client, int c, awaitable run_json_tests(asio::io_context& ctx) { std::atomic barrier; auto exec = co_await cpool::net::this_coro::executor; - auto host = get_env_var("REDIS_HOST"); + auto host = get_env_var("REDIS_HOST").value_or(DEFAULT_REDIS_HOST); redis_client client(exec, host, 6379); client.set_logging_handler( std::bind(logMessage, std::placeholders::_1, std::placeholders::_2)); @@ -289,7 +333,7 @@ awaitable run_json_tests(asio::io_context& ctx) { while (barrier != 0) { cpool::timer timer(exec); - co_await timer.async_wait(std::chrono::milliseconds(100)); + co_await timer.async_wait(100ms); } auto reply = co_await client.send(flush_all()); @@ -307,6 +351,22 @@ TEST(Redis, ClientTest) { ctx.run(); } +TEST(Redis, MultiClientTest) { + asio::io_context ctx(8); + + cpool::co_spawn(ctx, run_tests(std::ref(ctx)), cpool::detached); + + ctx.run(); +} + +TEST(Redis, PasswordClientTest) { + asio::io_context ctx(1); + + cpool::co_spawn(ctx, run_password_tests(std::ref(ctx)), cpool::detached); + + ctx.run(); +} + TEST(Redis, JsonTest) { asio::io_context ctx(1); diff --git a/test/redis_sub_connection_test.cpp b/test/redis_sub_connection_test.cpp index f3d5691..4d7834d 100644 --- a/test/redis_sub_connection_test.cpp +++ b/test/redis_sub_connection_test.cpp @@ -14,9 +14,11 @@ namespace { using string = std::string; using namespace redis; -std::string get_env_var(std::string const& key) { +const std::string DEFAULT_REDIS_HOST = "host.docker.internal"; + +std::optional get_env_var(std::string const& key) { char* val = getenv(key.c_str()); - return val == NULL ? std::string("redis") : std::string(val); + return (val == NULL) ? std::nullopt : std::optional(std::string(val)); } awaitable logMessage(log_level target, log_level level, @@ -62,7 +64,7 @@ awaitable connect_and_hold(redis_subscriber_connection& connection, awaitable run_tests(asio::io_context& ctx) { auto exec = co_await cpool::net::this_coro::executor; - auto host = get_env_var("REDIS_HOST"); + auto host = get_env_var("REDIS_HOST").value_or(DEFAULT_REDIS_HOST); std::atomic_int barrier(1); redis_subscriber_connection connection(exec, host, 6379); diff --git a/test/redis_sub_test.cpp b/test/redis_sub_test.cpp index 3b20c60..fb665ae 100644 --- a/test/redis_sub_test.cpp +++ b/test/redis_sub_test.cpp @@ -16,9 +16,11 @@ namespace { using string = std::string; using namespace redis; -std::string get_env_var(std::string const& key) { +const std::string DEFAULT_REDIS_HOST = "host.docker.internal"; + +std::optional get_env_var(std::string const& key) { char* val = getenv(key.c_str()); - return val == NULL ? std::string("redis") : std::string(val); + return (val == NULL) ? std::nullopt : std::optional(std::string(val)); } void testForError(std::string cmd, const redis::redis_reply& reply) { @@ -144,7 +146,7 @@ awaitable publish_messages(redis_client& client, std::string channel, awaitable run_tests(asio::io_context& ctx) { std::atomic barrier; auto exec = co_await cpool::net::this_coro::executor; - auto host = get_env_var("REDIS_HOST"); + auto host = get_env_var("REDIS_HOST").value_or(DEFAULT_REDIS_HOST); redis_client client(exec, host, 6379); redis_subscriber subscriber(exec, host, 6379);