Allow clients to report name and version (#11758)

This PR allows clients to send information about the client library to redis
to be displayed in CLIENT LIST and CLIENT INFO.

Currently supports:
`CLIENT [lib-name | lib-ver] <value>`
Client libraries are expected to pipeline these right after AUTH, and ignore
the failure in case they're talking to an older version of redis.

These will be shown in CLIENT LIST and CLIENT INFO as:
* `lib-name` - meant to hold the client library name.
* `lib-ver` - meant to hold the client library version.

The values cannot contain spaces, newlines and any wild ASCII characters,
but all other normal chars are accepted, e.g `.`, `=` etc (same as CLIENT NAME).

The RESET command does NOT clear these, but they can be cleared to the
default by sending a command with a blank string.

Co-authored-by: Oran Agra <oran@redislabs.com>
This commit is contained in:
Igor Malinovskiy 2023-03-22 07:17:20 +01:00 committed by GitHub
parent 6948dacaf6
commit c3b9f2fbd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 11 deletions

View File

@ -958,6 +958,27 @@ struct redisCommandArg CLIENT_REPLY_Args[] = {
{0} {0}
}; };
/********** CLIENT SETINFO ********************/
/* CLIENT SETINFO history */
#define CLIENT_SETINFO_History NULL
/* CLIENT SETINFO tips */
#define CLIENT_SETINFO_tips NULL
/* CLIENT SETINFO attr argument table */
struct redisCommandArg CLIENT_SETINFO_attr_Subargs[] = {
{"libname",ARG_TYPE_STRING,-1,"LIB-NAME",NULL,NULL,CMD_ARG_NONE},
{"libver",ARG_TYPE_STRING,-1,"LIB-VER",NULL,NULL,CMD_ARG_NONE},
{0}
};
/* CLIENT SETINFO argument table */
struct redisCommandArg CLIENT_SETINFO_Args[] = {
{"attr",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,.subargs=CLIENT_SETINFO_attr_Subargs},
{0}
};
/********** CLIENT SETNAME ********************/ /********** CLIENT SETNAME ********************/
/* CLIENT SETNAME history */ /* CLIENT SETNAME history */
@ -1051,6 +1072,7 @@ struct redisCommand CLIENT_Subcommands[] = {
{"no-touch","Controls whether commands sent by the client will alter the LRU/LFU of the keys they access.","O(1)","7.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_NO_TOUCH_History,CLIENT_NO_TOUCH_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,.args=CLIENT_NO_TOUCH_Args}, {"no-touch","Controls whether commands sent by the client will alter the LRU/LFU of the keys they access.","O(1)","7.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_NO_TOUCH_History,CLIENT_NO_TOUCH_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,.args=CLIENT_NO_TOUCH_Args},
{"pause","Stop processing commands from clients for some time","O(1)","3.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_PAUSE_History,CLIENT_PAUSE_tips,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_PAUSE_Args}, {"pause","Stop processing commands from clients for some time","O(1)","3.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_PAUSE_History,CLIENT_PAUSE_tips,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_PAUSE_Args},
{"reply","Instruct the server whether to reply to commands","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_REPLY_History,CLIENT_REPLY_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_REPLY_Args}, {"reply","Instruct the server whether to reply to commands","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_REPLY_History,CLIENT_REPLY_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_REPLY_Args},
{"setinfo","Set client or connection specific info","O(1)","7.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_SETINFO_History,CLIENT_SETINFO_tips,clientSetinfoCommand,4,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_SETINFO_Args},
{"setname","Set the current connection name","O(1)","2.6.9",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_SETNAME_History,CLIENT_SETNAME_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_SETNAME_Args}, {"setname","Set the current connection name","O(1)","2.6.9",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_SETNAME_History,CLIENT_SETNAME_tips,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_SETNAME_Args},
{"tracking","Enable or disable server assisted client side caching support","O(1). Some options may introduce additional complexity.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_TRACKING_History,CLIENT_TRACKING_tips,clientCommand,-3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_TRACKING_Args}, {"tracking","Enable or disable server assisted client side caching support","O(1). Some options may introduce additional complexity.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_TRACKING_History,CLIENT_TRACKING_tips,clientCommand,-3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,.args=CLIENT_TRACKING_Args},
{"trackinginfo","Return information about server assisted client side caching for the current connection","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_TRACKINGINFO_History,CLIENT_TRACKINGINFO_tips,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION}, {"trackinginfo","Return information about server assisted client side caching for the current connection","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_CONNECTION,CLIENT_TRACKINGINFO_History,CLIENT_TRACKINGINFO_tips,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION},

View File

@ -0,0 +1,41 @@
{
"SETINFO": {
"summary": "Set client or connection specific info",
"complexity": "O(1)",
"group": "connection",
"since": "7.2.0",
"arity": 4,
"container": "CLIENT",
"function": "clientSetinfoCommand",
"command_flags": [
"NOSCRIPT",
"LOADING",
"STALE",
"SENTINEL"
],
"acl_categories": [
"CONNECTION"
],
"reply_schema": {
"const": "OK"
},
"arguments": [
{
"name": "attr",
"type": "oneof",
"arguments": [
{
"token": "lib-name",
"name": "libname",
"type": "string"
},
{
"token": "lib-ver",
"name": "libver",
"type": "string"
}
]
}
]
}
}

View File

@ -147,6 +147,8 @@ client *createClient(connection *conn) {
#endif #endif
c->conn = conn; c->conn = conn;
c->name = NULL; c->name = NULL;
c->lib_name = NULL;
c->lib_ver = NULL;
c->bufpos = 0; c->bufpos = 0;
c->buf_usable_size = zmalloc_usable_size(c->buf); c->buf_usable_size = zmalloc_usable_size(c->buf);
c->buf_peak = c->buf_usable_size; c->buf_peak = c->buf_usable_size;
@ -1511,6 +1513,9 @@ void clearClientConnectionState(client *c) {
c->name = NULL; c->name = NULL;
} }
/* Note: lib_name and lib_ver are not reset since they still
* represent the client library behind the connection. */
/* Selectively clear state flags not covered above */ /* Selectively clear state flags not covered above */
c->flags &= ~(CLIENT_ASKING|CLIENT_READONLY|CLIENT_PUBSUB|CLIENT_REPLY_OFF| c->flags &= ~(CLIENT_ASKING|CLIENT_READONLY|CLIENT_PUBSUB|CLIENT_REPLY_OFF|
CLIENT_REPLY_SKIP_NEXT|CLIENT_NO_TOUCH|CLIENT_NO_EVICT); CLIENT_REPLY_SKIP_NEXT|CLIENT_NO_TOUCH|CLIENT_NO_EVICT);
@ -1662,6 +1667,8 @@ void freeClient(client *c) {
/* Release other dynamically allocated client structure fields, /* Release other dynamically allocated client structure fields,
* and finally release the client structure itself. */ * and finally release the client structure itself. */
if (c->name) decrRefCount(c->name); if (c->name) decrRefCount(c->name);
if (c->lib_name) decrRefCount(c->lib_name);
if (c->lib_ver) decrRefCount(c->lib_ver);
freeClientMultiState(c); freeClientMultiState(c);
sdsfree(c->peerid); sdsfree(c->peerid);
sdsfree(c->sockname); sdsfree(c->sockname);
@ -2775,7 +2782,7 @@ sds catClientInfoString(sds s, client *client) {
} }
sds ret = sdscatfmt(s, sds ret = sdscatfmt(s,
"id=%U addr=%s laddr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i ssub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U rbs=%U rbp=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i", "id=%U addr=%s laddr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i ssub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U rbs=%U rbp=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i lib-name=%s lib-ver=%s",
(unsigned long long) client->id, (unsigned long long) client->id,
getClientPeerId(client), getClientPeerId(client),
getClientSockname(client), getClientSockname(client),
@ -2803,7 +2810,10 @@ sds catClientInfoString(sds s, client *client) {
client->lastcmd ? client->lastcmd->fullname : "NULL", client->lastcmd ? client->lastcmd->fullname : "NULL",
client->user ? client->user->name : "(superuser)", client->user ? client->user->name : "(superuser)",
(client->flags & CLIENT_TRACKING) ? (long long) client->client_tracking_redirection : -1, (client->flags & CLIENT_TRACKING) ? (long long) client->client_tracking_redirection : -1,
client->resp); client->resp,
client->lib_name ? (char*)client->lib_name->ptr : "",
client->lib_ver ? (char*)client->lib_ver->ptr : ""
);
return ret; return ret;
} }
@ -2823,6 +2833,20 @@ sds getAllClientsInfoString(int type) {
return o; return o;
} }
/* Check validity of an attribute that's gonna be shown in CLIENT LIST. */
int validateClientAttr(const char *val) {
/* Check if the charset is ok. We need to do this otherwise
* CLIENT LIST format will break. You should always be able to
* split by space to get the different fields. */
while (*val) {
if (*val < '!' || *val > '~') { /* ASCII is assumed. */
return C_ERR;
}
val++;
}
return C_OK;
}
/* Returns C_OK if the name is valid. Returns C_ERR & sets `err` (when provided) otherwise. */ /* Returns C_OK if the name is valid. Returns C_ERR & sets `err` (when provided) otherwise. */
int validateClientName(robj *name, const char **err) { int validateClientName(robj *name, const char **err) {
const char *err_msg = "Client names cannot contain spaces, newlines or special characters."; const char *err_msg = "Client names cannot contain spaces, newlines or special characters.";
@ -2830,15 +2854,9 @@ int validateClientName(robj *name, const char **err) {
/* We allow setting the client name to an empty string. */ /* We allow setting the client name to an empty string. */
if (len == 0) if (len == 0)
return C_OK; return C_OK;
/* Otherwise check if the charset is ok. We need to do this otherwise if (validateClientAttr(name->ptr) == C_ERR) {
* CLIENT LIST format will break. You should always be able to if (err) *err = err_msg;
* split by space to get the different fields. */ return C_ERR;
char *p = name->ptr;
for (int j = 0; j < len; j++) {
if (p[j] < '!' || p[j] > '~') { /* ASCII is assumed. */
if (err) *err = err_msg;
return C_ERR;
}
} }
return C_OK; return C_OK;
} }
@ -2880,6 +2898,35 @@ int clientSetNameOrReply(client *c, robj *name) {
return result; return result;
} }
/* Set client or connection related info */
void clientSetinfoCommand(client *c) {
sds attr = c->argv[2]->ptr;
robj *valob = c->argv[3];
sds val = valob->ptr;
robj **destvar = NULL;
if (!strcasecmp(attr,"lib-name")) {
destvar = &c->lib_name;
} else if (!strcasecmp(attr,"lib-ver")) {
destvar = &c->lib_ver;
} else {
addReplyStatusFormat(c,"Unrecognized option '%s'", attr);
return;
}
if (validateClientAttr(val)==C_ERR) {
addReplyStatusFormat(c,
"%s cannot contain spaces, newlines or special characters.", attr);
return;
}
if (*destvar) decrRefCount(*destvar);
if (sdslen(val)) {
*destvar = valob;
incrRefCount(valob);
} else
*destvar = NULL;
addReply(c,shared.ok);
}
/* Reset the client state to resemble a newly connected client. /* Reset the client state to resemble a newly connected client.
*/ */
void resetCommand(client *c) { void resetCommand(client *c) {

View File

@ -1151,6 +1151,8 @@ typedef struct client {
int resp; /* RESP protocol version. Can be 2 or 3. */ int resp; /* RESP protocol version. Can be 2 or 3. */
redisDb *db; /* Pointer to currently SELECTed DB. */ redisDb *db; /* Pointer to currently SELECTed DB. */
robj *name; /* As set by CLIENT SETNAME. */ robj *name; /* As set by CLIENT SETNAME. */
robj *lib_name; /* The client library name as set by CLIENT SETINFO. */
robj *lib_ver; /* The client library version as set by CLIENT SETINFO. */
sds querybuf; /* Buffer we use to accumulate client queries. */ sds querybuf; /* Buffer we use to accumulate client queries. */
size_t qb_pos; /* The position we have read in querybuf. */ size_t qb_pos; /* The position we have read in querybuf. */
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size. */ size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size. */
@ -3639,6 +3641,7 @@ void objectCommand(client *c);
void memoryCommand(client *c); void memoryCommand(client *c);
void clientCommand(client *c); void clientCommand(client *c);
void helloCommand(client *c); void helloCommand(client *c);
void clientSetinfoCommand(client *c);
void evalCommand(client *c); void evalCommand(client *c);
void evalRoCommand(client *c); void evalRoCommand(client *c);
void evalShaCommand(client *c); void evalShaCommand(client *c);

View File

@ -325,6 +325,31 @@ start_server {tags {"introspection"}} {
} }
} }
test {CLIENT SETINFO can set a library name to this connection} {
r CLIENT SETINFO lib-name redis.py
r CLIENT SETINFO lib-ver 1.2.3
r client info
} {*lib-name=redis.py lib-ver=1.2.3*}
test {CLIENT SETINFO invalid args} {
assert_error {*wrong number of arguments*} {r CLIENT SETINFO lib-name}
assert_match {*cannot contain spaces*} [r CLIENT SETINFO lib-name "redis py"]
assert_match {*newlines*} [r CLIENT SETINFO lib-name "redis.py\n"]
assert_match {*Unrecognized*} [r CLIENT SETINFO badger hamster]
# test that all of these didn't affect the previously set values
r client info
} {*lib-name=redis.py lib-ver=1.2.3*}
test {RESET doesn NOT clean library name} {
r reset
r client info
} {*lib-name=redis.py*}
test {CLIENT SETINFO can clear library name} {
r CLIENT SETINFO lib-name ""
r client info
} {*lib-name= *}
test {CONFIG save params special case handled properly} { test {CONFIG save params special case handled properly} {
# No "save" keyword - defaults should apply # No "save" keyword - defaults should apply
start_server {config "minimal.conf"} { start_server {config "minimal.conf"} {