From a808e33ab367c5fd6f5abb37db617c61303bf84c Mon Sep 17 00:00:00 2001 From: Yossi Gottlieb Date: Thu, 5 Nov 2020 10:51:26 +0200 Subject: [PATCH] Add RESET command. (#7982) Perform full reset of all client connection states, is if the client was disconnected and re-connected. This affects: * MULTI state * Watched keys * MONITOR mode * Pub/Sub subscription * ACL/Authenticated state * Client tracking state * Cluster read-only/asking state * RESP version (reset to 2) * Selected database * CLIENT REPLY state The response is +RESET to make it easily distinguishable from other responses. Co-authored-by: Oran Agra Co-authored-by: Itamar Haber --- src/networking.c | 60 ++++++++++++++++++++++++++++++++++++++++---- src/server.c | 17 +++++++++---- src/server.h | 1 + tests/unit/other.tcl | 51 +++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 10 deletions(-) diff --git a/src/networking.c b/src/networking.c index 4f0e378da..8aca33e85 100644 --- a/src/networking.c +++ b/src/networking.c @@ -97,6 +97,16 @@ void linkClient(client *c) { raxInsert(server.clients_index,(unsigned char*)&id,sizeof(id),c,NULL); } +/* Initialize client authentication state. + */ +static void clientSetDefaultAuth(client *c) { + /* If the default user does not require authentication, the user is + * directly authenticated. */ + c->user = DefaultUser; + c->authenticated = (c->user->flags & USER_FLAG_NOPASS) && + !(c->user->flags & USER_FLAG_DISABLED); +} + client *createClient(connection *conn) { client *c = zmalloc(sizeof(client)); @@ -130,16 +140,12 @@ client *createClient(connection *conn) { c->argv = NULL; c->argv_len_sum = 0; c->cmd = c->lastcmd = NULL; - c->user = DefaultUser; c->multibulklen = 0; c->bulklen = -1; c->sentlen = 0; c->flags = 0; c->ctime = c->lastinteraction = server.unixtime; - /* If the default user does not require authentication, the user is - * directly authenticated. */ - c->authenticated = (c->user->flags & USER_FLAG_NOPASS) && - !(c->user->flags & USER_FLAG_DISABLED); + clientSetDefaultAuth(c); c->replstate = REPL_STATE_NONE; c->repl_put_online_on_ack = 0; c->reploff = 0; @@ -2257,6 +2263,50 @@ int clientSetNameOrReply(client *c, robj *name) { return C_OK; } +/* Reset the client state to resemble a newly connected client. + */ +void resetCommand(client *c) { + listNode *ln; + + /* MONITOR clients are also marked with CLIENT_SLAVE, we need to + * distinguish between the two. + */ + if (c->flags & CLIENT_MONITOR) { + ln = listSearchKey(server.monitors,c); + serverAssert(ln != NULL); + listDelNode(server.monitors,ln); + + c->flags &= ~(CLIENT_MONITOR|CLIENT_SLAVE); + } + + if (c->flags & (CLIENT_SLAVE|CLIENT_MASTER|CLIENT_MODULE)) { + addReplyError(c,"can only reset normal client connections"); + return; + } + + if (c->flags & CLIENT_TRACKING) disableTracking(c); + selectDb(c,0); + c->resp = 2; + + clientSetDefaultAuth(c); + moduleNotifyUserChanged(c); + discardTransaction(c); + + pubsubUnsubscribeAllChannels(c,0); + pubsubUnsubscribeAllPatterns(c,0); + + if (c->name) { + decrRefCount(c->name); + c->name = NULL; + } + + /* Selectively clear state flags not covered above */ + c->flags &= ~(CLIENT_ASKING|CLIENT_READONLY|CLIENT_PUBSUB| + CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP_NEXT); + + addReplyStatus(c,"RESET"); +} + void clientCommand(client *c) { listNode *ln; listIter li; diff --git a/src/server.c b/src/server.c index a5aa64c77..9aab1f39b 100644 --- a/src/server.c +++ b/src/server.c @@ -1036,7 +1036,11 @@ struct redisCommand redisCommandTable[] = { {"stralgo",stralgoCommand,-2, "read-only @string", - 0,lcsGetKeys,0,0,0,0,0,0} + 0,lcsGetKeys,0,0,0,0,0,0}, + + {"reset",resetCommand,-1, + "no-script ok-stale ok-loading fast @connection", + 0,NULL,0,0,0,0,0,0} }; /*============================ Utility functions ============================ */ @@ -3740,7 +3744,8 @@ int processCommand(client *c) { * set. */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && - c->cmd->proc != discardCommand) { + c->cmd->proc != discardCommand && + c->cmd->proc != resetCommand) { reject_cmd_on_oom = 1; } @@ -3806,10 +3811,11 @@ int processCommand(client *c) { c->cmd->proc != subscribeCommand && c->cmd->proc != unsubscribeCommand && c->cmd->proc != psubscribeCommand && - c->cmd->proc != punsubscribeCommand) { + c->cmd->proc != punsubscribeCommand && + c->cmd->proc != resetCommand) { rejectCommandFormat(c, "Can't execute '%s': only (P)SUBSCRIBE / " - "(P)UNSUBSCRIBE / PING / QUIT are allowed in this context", + "(P)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context", c->cmd->name); return C_OK; } @@ -3860,7 +3866,8 @@ int processCommand(client *c) { /* Exec the command */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && c->cmd->proc != discardCommand && - c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) + c->cmd->proc != multiCommand && c->cmd->proc != watchCommand && + c->cmd->proc != resetCommand) { queueMultiCommand(c); addReply(c,shared.queued); diff --git a/src/server.h b/src/server.h index b1e645b0c..7431c7a1e 100644 --- a/src/server.h +++ b/src/server.h @@ -2493,6 +2493,7 @@ void xtrimCommand(client *c); void lolwutCommand(client *c); void aclCommand(client *c); void stralgoCommand(client *c); +void resetCommand(client *c); #if defined(__GNUC__) void *calloc(size_t count, size_t size) __attribute__ ((deprecated)); diff --git a/tests/unit/other.tcl b/tests/unit/other.tcl index 3ca70a77a..96e9ee3da 100644 --- a/tests/unit/other.tcl +++ b/tests/unit/other.tcl @@ -249,6 +249,57 @@ start_server {tags {"other"}} { waitForBgsave r r save } {OK} + + test {RESET clears client state} { + r client setname test-client + r client tracking on + + assert_equal [r reset] "RESET" + set client [r client list] + assert_match {*name= *} $client + assert_match {*flags=N *} $client + } + + test {RESET clears MONITOR state} { + set rd [redis_deferring_client] + $rd monitor + assert_equal [$rd read] "OK" + + $rd reset + + # skip reset ouptut + $rd read + assert_equal [$rd read] "RESET" + + assert_no_match {*flags=O*} [r client list] + } + + test {RESET clears and discards MULTI state} { + r multi + r set key-a a + + r reset + catch {r exec} err + assert_match {*EXEC without MULTI*} $err + } + + test {RESET clears Pub/Sub state} { + r subscribe channel-1 + r reset + + # confirm we're not subscribed by executing another command + r set key val + } + + test {RESET clears authenticated state} { + r acl setuser user1 on >secret +@all + r auth user1 secret + assert_equal [r acl whoami] user1 + + r reset + + assert_equal [r acl whoami] default + } } start_server {tags {"other"}} {