Skip to content

Commit

Permalink
Add support of password authentication (AUTH) (#3)
Browse files Browse the repository at this point in the history
When a password is configured it will be sent right
after a connection to a Redis instance is setup,
via the AUTH command.

Added API:
    redisClusterSetOptionPassword(cc, "password");
  • Loading branch information
bjosv authored Nov 13, 2020
1 parent db21dbe commit 53f9667
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 1 deletion.
87 changes: 86 additions & 1 deletion hircluster.c
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,42 @@ static void cluster_open_slot_destroy(copen_slot *oslot) {
hi_free(oslot);
}

/**
* Handle password authentication in the synchronous API
*/
static int authenticate(redisClusterContext *cc, redisContext *c) {
if (cc == NULL || c == NULL) {
return REDIS_ERR;
}

// Skip if no password configured
if (cc->password[0] == '\0') {
return REDIS_OK;
}

redisReply *reply = redisCommand(c, "AUTH %s", cc->password);
if (reply == NULL) {
__redisClusterSetError(cc, REDIS_ERR_OTHER,
"Command AUTH reply error (NULL)");
goto error;
}

if (reply->type == REDIS_REPLY_ERROR) {
__redisClusterSetError(cc, REDIS_ERR_OTHER, reply->str);
goto error;
}

freeReplyObject(reply);
return REDIS_OK;

error:
if (reply != NULL) {
freeReplyObject(reply);
reply = NULL;
}
return REDIS_ERR;
}

/**
* Return a new node with the "cluster slots" command reply.
*/
Expand Down Expand Up @@ -1262,6 +1298,11 @@ static int cluster_update_route_by_addr(redisClusterContext *cc, const char *ip,
}
}
#endif

if (authenticate(cc, c) != REDIS_OK) {
goto error;
}

if (cc->flags & HIRCLUSTER_FLAG_ROUTE_USE_SLOTS) {
reply = redisCommand(c, REDIS_COMMAND_CLUSTER_SLOTS);
if (reply == NULL) {
Expand Down Expand Up @@ -1583,6 +1624,8 @@ redisClusterContext *redisClusterContextInit(void) {
#ifdef SSL_SUPPORT
cc->ssl = NULL;
#endif
cc->password[0] = '\0';

return cc;
}

Expand Down Expand Up @@ -1848,6 +1891,27 @@ int redisClusterSetOptionConnectNonBlock(redisClusterContext *cc) {
return REDIS_OK;
}

/**
* Configure a password used when connecting to password-protected
* Redis instances. (See Redis AUTH command)
*/
int redisClusterSetOptionPassword(redisClusterContext *cc,
const char *password) {

if (cc == NULL || password == NULL) {
return REDIS_ERR;
}

if (strlen(password) > CONFIG_AUTHPASS_MAX_LEN) {
return REDIS_ERR;
}

strncpy(cc->password, password, sizeof(cc->password) - 1);
cc->password[sizeof(cc->password) - 1] = '\0';

return REDIS_OK;
}

int redisClusterSetOptionParseSlaves(redisClusterContext *cc) {

if (cc == NULL) {
Expand Down Expand Up @@ -2001,6 +2065,7 @@ redisContext *ctx_get_by_node(redisClusterContext *cc, cluster_node *node) {
}
}
#endif
authenticate(cc, c); // err and errstr handled in function

if (cc->timeout && c->err == 0) {
redisSetTimeout(c, *cc->timeout);
Expand Down Expand Up @@ -2035,6 +2100,11 @@ redisContext *ctx_get_by_node(redisClusterContext *cc, cluster_node *node) {
}
#endif

if (authenticate(cc, c) != REDIS_OK) {
redisFree(c);
return NULL;
}

node->con = c;

return c;
Expand Down Expand Up @@ -3593,6 +3663,7 @@ static void unlinkAsyncContextAndNode(void *data) {
redisAsyncContext *actx_get_by_node(redisClusterAsyncContext *acc,
cluster_node *node) {
redisAsyncContext *ac;
int ret;

if (node == NULL) {
return NULL;
Expand All @@ -3607,6 +3678,8 @@ redisAsyncContext *actx_get_by_node(redisClusterAsyncContext *acc,
}
}

// No async context exists, perform a connect

if (node->host == NULL || node->port <= 0) {
__redisClusterAsyncSetError(acc, REDIS_ERR_OTHER,
"node host or port is error");
Expand All @@ -3622,13 +3695,25 @@ redisAsyncContext *actx_get_by_node(redisClusterAsyncContext *acc,

#ifdef SSL_SUPPORT
if (acc->cc->ssl) {
if (redisInitiateSSLWithContext(&ac->c, acc->cc->ssl) != REDIS_OK) {
ret = redisInitiateSSLWithContext(&ac->c, acc->cc->ssl);
if (ret != REDIS_OK) {
__redisClusterAsyncSetError(acc, ac->c.err, ac->c.errstr);
redisAsyncFree(ac);
return NULL;
}
}
#endif

// Authenticate when needed
if (acc->cc->password[0] != '\0') {
ret = redisAsyncCommand(ac, NULL, NULL, "AUTH %s", acc->cc->password);
if (ret != REDIS_OK) {
__redisClusterAsyncSetError(acc, ac->c.err, ac->c.errstr);
redisAsyncFree(ac);
return NULL;
}
}

if (acc->adapter) {
acc->attach_fn(ac, acc->adapter);
}
Expand Down
6 changes: 6 additions & 0 deletions hircluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#define REDIS_ROLE_MASTER 1
#define REDIS_ROLE_SLAVE 2

#define CONFIG_AUTHPASS_MAX_LEN 512 // Defined in Redis as max characters

#define HIRCLUSTER_FLAG_NULL 0x0
/* The flag to decide whether add slave node in
* redisClusterContext->nodes. This is set in the
Expand Down Expand Up @@ -99,6 +101,8 @@ typedef struct redisClusterContext {
int need_update_route;
int64_t update_route_time;

char password[CONFIG_AUTHPASS_MAX_LEN + 1]; // Include a null terminator

#ifdef SSL_SUPPORT
redisSSLContext *ssl;
#endif
Expand All @@ -118,6 +122,8 @@ int redisClusterSetOptionAddNode(redisClusterContext *cc, const char *addr);
int redisClusterSetOptionAddNodes(redisClusterContext *cc, const char *addrs);
int redisClusterSetOptionConnectBlock(redisClusterContext *cc);
int redisClusterSetOptionConnectNonBlock(redisClusterContext *cc);
int redisClusterSetOptionPassword(redisClusterContext *cc,
const char *password);
int redisClusterSetOptionParseSlaves(redisClusterContext *cc);
int redisClusterSetOptionParseOpenSlots(redisClusterContext *cc);
int redisClusterSetOptionRouteUseSlots(redisClusterContext *cc);
Expand Down
9 changes: 9 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ else()
add_compile_options(-Wall -Wextra -pedantic -Werror)
endif()

# Debug mode for tests
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "" FORCE)

# Executable: IPv4
add_executable(example_ipv4 main.c)
target_link_libraries(example_ipv4 hiredis_cluster hiredis ${SSL_LIBRARY})
Expand All @@ -43,6 +46,12 @@ add_test(NAME example_async COMMAND "$<TARGET_FILE:example_async>")
add_executable(ct_commands ct_commands.c)
target_link_libraries(ct_commands hiredis_cluster hiredis ${SSL_LIBRARY})
add_test(NAME ct_commands COMMAND "$<TARGET_FILE:ct_commands>")
set_tests_properties(ct_commands PROPERTIES LABELS "CT")

add_executable(ct_connection ct_connection.c)
target_link_libraries(ct_connection hiredis_cluster hiredis ${SSL_LIBRARY} ${EVENT_LIBRARY})
add_test(NAME ct_connection COMMAND "$<TARGET_FILE:ct_connection>")
set_tests_properties(ct_connection PROPERTIES LABELS "CT")

if(ENABLE_SSL)
# Executable: tls
Expand Down
176 changes: 176 additions & 0 deletions tests/ct_connection.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#include "adapters/libevent.h"
#include "hircluster.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CLUSTER_NODE_WITH_PASSWORD "127.0.0.1:30001"
#define CLUSTER_PASSWORD "secretword"

// Connecting to a password protected cluster and
// providing a correct password.
void test_password_ok() {
redisClusterContext *cc = redisClusterContextInit();
assert(cc);
redisClusterSetOptionAddNodes(cc, CLUSTER_NODE_WITH_PASSWORD);
redisClusterSetOptionPassword(cc, CLUSTER_PASSWORD);
redisClusterConnect2(cc);

assert(cc->err == 0);

// Test connection
redisReply *reply;
reply = (redisReply *)redisClusterCommand(cc, "SET key1 Hello");
assert(reply);
assert(strcmp(reply->str, "OK") == 0);
freeReplyObject(reply);

redisClusterFree(cc);
}

// Connecting to a password protected cluster and
// providing wrong password.
void test_password_wrong() {
redisClusterContext *cc = redisClusterContextInit();
assert(cc);
redisClusterSetOptionAddNodes(cc, CLUSTER_NODE_WITH_PASSWORD);
redisClusterSetOptionPassword(cc, "wrongpass");
redisClusterConnect2(cc);

assert(cc->err == REDIS_ERR_OTHER);
assert(strncmp(cc->errstr, "WRONGPASS", 9) == 0);

redisClusterFree(cc);
}

// Connecting to a password protected cluster and
// not providing any password.
void test_password_missing() {
redisClusterContext *cc = redisClusterContextInit();
assert(cc);
redisClusterSetOptionAddNodes(cc, CLUSTER_NODE_WITH_PASSWORD);
// A password is not configured..
redisClusterConnect2(cc);

assert(cc->err == REDIS_ERR_OTHER);
assert(strncmp(cc->errstr, "NOAUTH", 6) == 0);

redisClusterFree(cc);
}

//------------------------------------------------------------------------------
// Async API
//------------------------------------------------------------------------------

void callbackExpectOk(const redisAsyncContext *ac, int status) {
UNUSED(ac);
assert(status == REDIS_OK);
}

void commandCallback(redisClusterAsyncContext *cc, void *r, void *privdata) {
UNUSED(r);
UNUSED(privdata);
redisReply *reply = (redisReply *)r;
assert(reply != NULL);
assert(strcmp(reply->str, "OK") == 0);
redisClusterAsyncDisconnect(cc);
}

// Connecting to a password protected cluster using
// the async API, providing correct password.
void test_async_password_ok() {
redisClusterAsyncContext *acc = redisClusterAsyncContextInit();
assert(acc);
redisClusterAsyncSetConnectCallback(acc, callbackExpectOk);
redisClusterAsyncSetDisconnectCallback(acc, callbackExpectOk);
redisClusterSetOptionAddNodes(acc->cc, CLUSTER_NODE_WITH_PASSWORD);
redisClusterSetOptionPassword(acc->cc, CLUSTER_PASSWORD);
redisClusterConnect2(acc->cc);

assert(acc->err == 0);

struct event_base *base = event_base_new();
redisClusterLibeventAttach(acc, base);

// Test connection
int status = redisClusterAsyncCommand(acc, commandCallback,
(char *)"THE_ID", "SET key1 Hello");
assert(status == REDIS_OK);

event_base_dispatch(base);

redisClusterAsyncFree(acc);
event_base_free(base);
}

// Connecting to a password protected cluster using
// the async API, providing wrong password.
void test_async_password_wrong() {
redisClusterAsyncContext *acc = redisClusterAsyncContextInit();
assert(acc);
redisClusterAsyncSetConnectCallback(acc, callbackExpectOk);
redisClusterAsyncSetDisconnectCallback(acc, callbackExpectOk);
redisClusterSetOptionAddNodes(acc->cc, CLUSTER_NODE_WITH_PASSWORD);
redisClusterSetOptionPassword(acc->cc, "wrongpass");
redisClusterConnect2(acc->cc);

assert(acc->err == 0);

struct event_base *base = event_base_new();
redisClusterLibeventAttach(acc, base);

// Test connection
int status = redisClusterAsyncCommand(acc, commandCallback,
(char *)"THE_ID", "SET key1 Hello");
assert(status == REDIS_ERR);
assert(acc->err == REDIS_ERR_OTHER);
assert(strcmp(acc->errstr, "node get by table error") == 0);

event_base_dispatch(base);

redisClusterAsyncFree(acc);
event_base_free(base);
}

// Connecting to a password protected cluster using
// the async API, not providing a password.
void test_async_password_missing() {
redisClusterAsyncContext *acc = redisClusterAsyncContextInit();
assert(acc);
redisClusterAsyncSetConnectCallback(acc, callbackExpectOk);
redisClusterAsyncSetDisconnectCallback(acc, callbackExpectOk);
redisClusterSetOptionAddNodes(acc->cc, CLUSTER_NODE_WITH_PASSWORD);
// Password not configured
redisClusterConnect2(acc->cc);

assert(acc->err == 0);

struct event_base *base = event_base_new();
redisClusterLibeventAttach(acc, base);

// Test connection
int status = redisClusterAsyncCommand(acc, commandCallback,
(char *)"THE_ID", "SET key1 Hello");
assert(status == REDIS_ERR);
assert(acc->err == REDIS_ERR_OTHER);
assert(strcmp(acc->errstr, "node get by table error") == 0);

event_base_dispatch(base);

redisClusterAsyncFree(acc);
event_base_free(base);
}

int main() {

test_password_ok();
test_password_wrong();
test_password_missing();

test_async_password_ok();
test_async_password_wrong();
test_async_password_missing();

return 0;
}

0 comments on commit 53f9667

Please sign in to comment.