Improve redis-cli non-binary safe string handling. (#8566)

* The `redis-cli --scan` output should honor output mode (set explicitly or implicitly), and quote key names when not in raw mode.
  * Technically this is a breaking change, but it should be very minor since raw mode is by default on for non-tty output.
  * It should only affect  TTY output (human users) or non-tty output if `--no-raw` is specified.

* Added `--quoted-input` option to treat all arguments as potentially quoted strings.
* Added `--quoted-pattern` option to accept a potentially quoted pattern.

Unquoting is applied to potentially quoted input only if single or double quotes are used. 

Fixes #8561, #8563
This commit is contained in:
Yossi Gottlieb 2021-03-04 15:03:49 +02:00 committed by GitHub
parent f07b7393a0
commit 3c7d6a1853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 113 additions and 44 deletions

View File

@ -227,7 +227,7 @@ static struct config {
int scan_mode;
int intrinsic_latency_mode;
int intrinsic_latency_duration;
char *pattern;
sds pattern;
char *rdb_filename;
int bigkeys;
int memkeys;
@ -237,6 +237,7 @@ static struct config {
char *auth;
int askpass;
char *user;
int quoted_input; /* Force input args to be treated as quoted strings */
int output; /* output mode, see OUTPUT_* defines */
int push_output; /* Should we display spontaneous PUSH replies */
sds mb_delim;
@ -763,6 +764,23 @@ static void freeHintsCallback(void *ptr) {
* Networking / parsing
*--------------------------------------------------------------------------- */
/* Unquote a null-terminated string and return it as a binary-safe sds. */
static sds unquoteCString(char *str) {
int count;
sds *unquoted = sdssplitargs(str, &count);
sds res = NULL;
if (unquoted && count == 1) {
res = unquoted[0];
unquoted[0] = NULL;
}
if (unquoted)
sdsfreesplitres(unquoted, count);
return res;
}
/* Send AUTH command to the server */
static int cliAuth(redisContext *ctx, char *user, char *auth) {
redisReply *reply;
@ -1533,6 +1551,8 @@ static int parseOptions(int argc, char **argv) {
config.output = OUTPUT_RAW;
} else if (!strcmp(argv[i],"--no-raw")) {
config.output = OUTPUT_STANDARD;
} else if (!strcmp(argv[i],"--quoted-input")) {
config.quoted_input = 1;
} else if (!strcmp(argv[i],"--csv")) {
config.output = OUTPUT_CSV;
} else if (!strcmp(argv[i],"--latency")) {
@ -1557,7 +1577,15 @@ static int parseOptions(int argc, char **argv) {
} else if (!strcmp(argv[i],"--scan")) {
config.scan_mode = 1;
} else if (!strcmp(argv[i],"--pattern") && !lastarg) {
config.pattern = argv[++i];
sdsfree(config.pattern);
config.pattern = sdsnew(argv[++i]);
} else if (!strcmp(argv[i],"--quoted-pattern") && !lastarg) {
sdsfree(config.pattern);
config.pattern = unquoteCString(argv[++i]);
if (!config.pattern) {
fprintf(stderr,"Invalid quoted string specified for --quoted-pattern.\n");
exit(1);
}
} else if (!strcmp(argv[i],"--intrinsic-latency") && !lastarg) {
config.intrinsic_latency_mode = 1;
config.intrinsic_latency_duration = atoi(argv[++i]);
@ -1841,6 +1869,7 @@ static void usage(void) {
" --raw Use raw formatting for replies (default when STDOUT is\n"
" not a tty).\n"
" --no-raw Force formatted output even when STDOUT is not a tty.\n"
" --quoted-input Force input to be handled as quoted strings.\n"
" --csv Output in CSV format.\n"
" --show-pushes <yn> Whether to print RESP3 PUSH messages. Enabled by default when\n"
" STDOUT is a tty but can be overriden with --show-pushes no.\n"
@ -1876,6 +1905,8 @@ static void usage(void) {
" --scan List all keys using the SCAN command.\n"
" --pattern <pat> Keys pattern when using the --scan, --bigkeys or --hotkeys\n"
" options (default: *).\n"
" --quoted-pattern <pat> Same as --pattern, but the specified string can be\n"
" quoted, in order to pass an otherwise non binary-safe string.\n"
" --intrinsic-latency <sec> Run a test to measure intrinsic system latency.\n"
" The test will run for the specified amount of seconds.\n"
" --eval <file> Send an EVAL command using the Lua script at <file>.\n"
@ -1901,6 +1932,7 @@ static void usage(void) {
" redis-cli get mypasswd\n"
" redis-cli -r 100 lpush mylist x\n"
" redis-cli -r 100 -i 1 info | grep used_memory_human:\n"
" redis-cli --quoted-input set '\"null-\\x00-separated\"' value\n"
" redis-cli --eval myscript.lua key1 key2 , arg1 arg2 arg3\n"
" redis-cli --scan --pattern '*:12345*'\n"
"\n"
@ -1930,22 +1962,28 @@ static int confirmWithYes(char *msg, int ignore_force) {
return (nread != 0 && !strcmp("yes", buf));
}
/* Turn the plain C strings into Sds strings */
static char **convertToSds(int count, char** args) {
int j;
char **sds = zmalloc(sizeof(char*)*count);
/* Create an sds array from argv, either as-is or by dequoting every
* element. When quoted is non-zero, may return a NULL to indicate an
* invalid quoted string.
*/
static sds *getSdsArrayFromArgv(int argc, char **argv, int quoted) {
sds *res = sds_malloc(sizeof(sds) * argc);
for(j = 0; j < count; j++)
sds[j] = sdsnew(args[j]);
for (int j = 0; j < argc; j++) {
if (quoted) {
sds unquoted = unquoteCString(argv[j]);
if (!unquoted) {
while (--j >= 0) sdsfree(res[j]);
sds_free(res);
return NULL;
}
res[j] = unquoted;
} else {
res[j] = sdsnew(argv[j]);
}
}
return sds;
}
static void freeConvertedSds(int count, char **sds) {
int j;
for (j = 0; j < count; j++)
sdsfree(sds[j]);
zfree(sds);
return res;
}
static int issueCommandRepeat(int argc, char **argv, long repeat) {
@ -2178,17 +2216,19 @@ static void repl(void) {
static int noninteractive(int argc, char **argv) {
int retval = 0;
argv = convertToSds(argc, argv);
if (config.stdinarg) {
argv = zrealloc(argv, (argc+1)*sizeof(char*));
argv[argc] = readArgFromStdin();
retval = issueCommand(argc+1, argv);
sdsfree(argv[argc]);
} else {
retval = issueCommand(argc, argv);
sds *sds_args = getSdsArrayFromArgv(argc, argv, config.quoted_input);
if (!sds_args) {
printf("Invalid quoted string\n");
return 1;
}
freeConvertedSds(argc, argv);
if (config.stdinarg) {
sds_args = sds_realloc(sds_args, (argc + 1) * sizeof(sds));
sds_args[argc] = readArgFromStdin();
argc++;
}
retval = issueCommand(argc, sds_args);
sdsfreesplitres(sds_args, argc);
return retval;
}
@ -7332,8 +7372,8 @@ static redisReply *sendScan(unsigned long long *it) {
redisReply *reply;
if (config.pattern)
reply = redisCommand(context,"SCAN %llu MATCH %s",
*it,config.pattern);
reply = redisCommand(context, "SCAN %llu MATCH %b",
*it, config.pattern, sdslen(config.pattern));
else
reply = redisCommand(context,"SCAN %llu",*it);
@ -7945,23 +7985,16 @@ static void scanMode(void) {
unsigned long long cur = 0;
do {
if (config.pattern)
reply = redisCommand(context,"SCAN %llu MATCH %s",
cur,config.pattern);
else
reply = redisCommand(context,"SCAN %llu",cur);
if (reply == NULL) {
printf("I/O error\n");
exit(1);
} else if (reply->type == REDIS_REPLY_ERROR) {
printf("ERROR: %s\n", reply->str);
exit(1);
} else {
unsigned int j;
cur = strtoull(reply->element[0]->str,NULL,10);
for (j = 0; j < reply->element[1]->elements; j++)
reply = sendScan(&cur);
for (unsigned int j = 0; j < reply->element[1]->elements; j++) {
if (config.output == OUTPUT_STANDARD) {
sds out = sdscatrepr(sdsempty(), reply->element[1]->element[j]->str,
reply->element[1]->element[j]->len);
printf("%s\n", out);
sdsfree(out);
} else {
printf("%s\n", reply->element[1]->element[j]->str);
}
}
freeReplyObject(reply);
} while(cur != 0);

View File

@ -207,6 +207,28 @@ start_server {tags {"cli"}} {
assert_equal "foo\nbar" [run_cli lrange list 0 -1]
}
test_nontty_cli "Quoted input arguments" {
r set "\x00\x00" "value"
assert_equal "value" [run_cli --quoted-input get {"\x00\x00"}]
}
test_nontty_cli "No accidental unquoting of input arguments" {
run_cli --quoted-input set {"\x41\x41"} quoted-val
run_cli set {"\x41\x41"} unquoted-val
assert_equal "quoted-val" [r get AA]
assert_equal "unquoted-val" [r get {"\x41\x41"}]
}
test_nontty_cli "Invalid quoted input arguments" {
catch {run_cli --quoted-input set {"Unterminated}} err
assert_match {*exited abnormally*} $err
# A single arg that unquotes to two arguments is also not expected
catch {run_cli --quoted-input set {"arg1" "arg2"}} err
assert_match {*exited abnormally*} $err
}
test_nontty_cli "Read last argument from pipe" {
assert_equal "OK" [run_cli_with_input_pipe "echo foo" set key]
assert_equal "foo\n" [r get key]
@ -247,6 +269,20 @@ start_server {tags {"cli"}} {
test_redis_cli_rdb_dump
}
test "Scan mode" {
r flushdb
populate 1000 key: 1
# basic use
assert_equal 1000 [llength [split [run_cli --scan]]]
# pattern
assert_equal {key:2} [run_cli --scan --pattern "*:2"]
# pattern matching with a quoted string
assert_equal {key:2} [run_cli --scan --quoted-pattern {"*:\x32"}]
}
test "Connecting as a replica" {
set fd [open_cli "--replica"]
wait_for_condition 500 500 {