summaryrefslogtreecommitdiff
path: root/src/redis-cli.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/redis-cli.c')
-rw-r--r--src/redis-cli.c233
1 files changed, 218 insertions, 15 deletions
diff --git a/src/redis-cli.c b/src/redis-cli.c
index db53fc7d8..c67663c23 100644
--- a/src/redis-cli.c
+++ b/src/redis-cli.c
@@ -47,6 +47,10 @@
#include <math.h>
#include <hiredis.h>
+#ifdef USE_OPENSSL
+#include <openssl/ssl.h>
+#include <hiredis_ssl.h>
+#endif
#include <sds.h> /* use sds.h from hiredis, so that only one set of sds functions will be present in the binary */
#include "dict.h"
#include "adlist.h"
@@ -188,6 +192,12 @@ static struct config {
char *hostip;
int hostport;
char *hostsocket;
+ int tls;
+ char *sni;
+ char *cacert;
+ char *cacertdir;
+ char *cert;
+ char *key;
long repeat;
long interval;
int dbnum;
@@ -218,6 +228,7 @@ static struct config {
int hotkeys;
int stdinarg; /* get last arg from stdin. (-x option) */
char *auth;
+ char *user;
int output; /* output mode, see OUTPUT_* defines */
sds mb_delim;
char prompt[128];
@@ -230,6 +241,7 @@ static struct config {
int verbose;
clusterManagerCommand cluster_manager_command;
int no_auth_warning;
+ int resp3;
} config;
/* User preferences. */
@@ -358,7 +370,7 @@ static sds percentDecode(const char *pe, size_t len) {
* URI scheme is based on the the provisional specification[1] excluding support
* for query parameters. Valid URIs are:
* scheme: "redis://"
- * authority: [<username> ":"] <password> "@"] [<hostname> [":" <port>]]
+ * authority: [[<username> ":"] <password> "@"] [<hostname> [":" <port>]]
* path: ["/" [<db>]]
*
* [1]: https://www.iana.org/assignments/uri-schemes/prov/redis */
@@ -728,8 +740,13 @@ static int cliAuth(void) {
redisReply *reply;
if (config.auth == NULL) return REDIS_OK;
- reply = redisCommand(context,"AUTH %s",config.auth);
+ if (config.user == NULL)
+ reply = redisCommand(context,"AUTH %s",config.auth);
+ else
+ reply = redisCommand(context,"AUTH %s %s",config.user,config.auth);
if (reply != NULL) {
+ if (reply->type == REDIS_REPLY_ERROR)
+ fprintf(stderr,"Warning: AUTH failed\n");
freeReplyObject(reply);
return REDIS_OK;
}
@@ -751,6 +768,86 @@ static int cliSelect(void) {
return REDIS_ERR;
}
+/* Wrapper around redisSecureConnection to avoid hiredis_ssl dependencies if
+ * not building with TLS support.
+ */
+static int cliSecureConnection(redisContext *c, const char **err) {
+#ifdef USE_OPENSSL
+ static SSL_CTX *ssl_ctx = NULL;
+
+ if (!ssl_ctx) {
+ ssl_ctx = SSL_CTX_new(SSLv23_client_method());
+ if (!ssl_ctx) {
+ *err = "Failed to create SSL_CTX";
+ goto error;
+ }
+
+ SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+ SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL);
+
+ if (config.cacert || config.cacertdir) {
+ if (!SSL_CTX_load_verify_locations(ssl_ctx, config.cacert, config.cacertdir)) {
+ *err = "Invalid CA Certificate File/Directory";
+ goto error;
+ }
+ } else {
+ if (!SSL_CTX_set_default_verify_paths(ssl_ctx)) {
+ *err = "Failed to use default CA paths";
+ goto error;
+ }
+ }
+
+ if (config.cert && !SSL_CTX_use_certificate_chain_file(ssl_ctx, config.cert)) {
+ *err = "Invalid client certificate";
+ goto error;
+ }
+
+ if (config.key && !SSL_CTX_use_PrivateKey_file(ssl_ctx, config.key, SSL_FILETYPE_PEM)) {
+ *err = "Invalid private key";
+ goto error;
+ }
+ }
+
+ SSL *ssl = SSL_new(ssl_ctx);
+ if (!ssl) {
+ *err = "Failed to create SSL object";
+ return REDIS_ERR;
+ }
+
+ if (config.sni && !SSL_set_tlsext_host_name(ssl, config.sni)) {
+ *err = "Failed to configure SNI";
+ SSL_free(ssl);
+ return REDIS_ERR;
+ }
+
+ return redisInitiateSSL(c, ssl);
+
+error:
+ SSL_CTX_free(ssl_ctx);
+ ssl_ctx = NULL;
+ return REDIS_ERR;
+#else
+ (void) c;
+ (void) err;
+ return REDIS_OK;
+#endif
+}
+
+/* Select RESP3 mode if redis-cli was started with the -3 option. */
+static int cliSwitchProto(void) {
+ redisReply *reply;
+ if (config.resp3 == 0) return REDIS_OK;
+
+ reply = redisCommand(context,"HELLO 3");
+ if (reply != NULL) {
+ int result = REDIS_OK;
+ if (reply->type == REDIS_REPLY_ERROR) result = REDIS_ERR;
+ freeReplyObject(reply);
+ return result;
+ }
+ return REDIS_ERR;
+}
+
/* Connect to the server. It is possible to pass certain flags to the function:
* CC_FORCE: The connection is performed even if there is already
* a connected socket.
@@ -767,6 +864,16 @@ static int cliConnect(int flags) {
context = redisConnectUnix(config.hostsocket);
}
+ if (!context->err && config.tls) {
+ const char *err = NULL;
+ if (cliSecureConnection(context, &err) == REDIS_ERR && err) {
+ fprintf(stderr, "Could not negotiate a TLS connection: %s\n", err);
+ context = NULL;
+ redisFree(context);
+ return REDIS_ERR;
+ }
+ }
+
if (context->err) {
if (!(flags & CC_QUIET)) {
fprintf(stderr,"Could not connect to Redis at ");
@@ -782,17 +889,20 @@ static int cliConnect(int flags) {
return REDIS_ERR;
}
+
/* Set aggressive KEEP_ALIVE socket option in the Redis context socket
* in order to prevent timeouts caused by the execution of long
* commands. At the same time this improves the detection of real
* errors. */
anetKeepAlive(NULL, context->fd, REDIS_CLI_KEEPALIVE_INTERVAL);
- /* Do AUTH and select the right DB. */
+ /* Do AUTH, select the right DB, switch to RESP3 if needed. */
if (cliAuth() != REDIS_OK)
return REDIS_ERR;
if (cliSelect() != REDIS_OK)
return REDIS_ERR;
+ if (cliSwitchProto() != REDIS_OK)
+ return REDIS_ERR;
}
return REDIS_OK;
}
@@ -819,10 +929,17 @@ static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
out = sdscatprintf(out,"(double) %s\n",r->str);
break;
case REDIS_REPLY_STRING:
+ case REDIS_REPLY_VERB:
/* If you are producing output for the standard output we want
- * a more interesting output with quoted characters and so forth */
- out = sdscatrepr(out,r->str,r->len);
- out = sdscat(out,"\n");
+ * a more interesting output with quoted characters and so forth,
+ * unless it's a verbatim string type. */
+ if (r->type == REDIS_REPLY_STRING) {
+ out = sdscatrepr(out,r->str,r->len);
+ out = sdscat(out,"\n");
+ } else {
+ out = sdscatlen(out,r->str,r->len);
+ out = sdscat(out,"\n");
+ }
break;
case REDIS_REPLY_NIL:
out = sdscat(out,"(nil)\n");
@@ -961,6 +1078,7 @@ static sds cliFormatReplyRaw(redisReply *r) {
break;
case REDIS_REPLY_STATUS:
case REDIS_REPLY_STRING:
+ case REDIS_REPLY_VERB:
if (r->type == REDIS_REPLY_STATUS && config.eval_ldb) {
/* The Lua debugger replies with arrays of simple (status)
* strings. We colorize the output for more fun if this
@@ -980,9 +1098,15 @@ static sds cliFormatReplyRaw(redisReply *r) {
out = sdscatlen(out,r->str,r->len);
}
break;
+ case REDIS_REPLY_BOOL:
+ out = sdscat(out,r->integer ? "(true)" : "(false)");
+ break;
case REDIS_REPLY_INTEGER:
out = sdscatprintf(out,"%lld",r->integer);
break;
+ case REDIS_REPLY_DOUBLE:
+ out = sdscatprintf(out,"%s",r->str);
+ break;
case REDIS_REPLY_ARRAY:
for (i = 0; i < r->elements; i++) {
if (i > 0) out = sdscat(out,config.mb_delim);
@@ -991,6 +1115,19 @@ static sds cliFormatReplyRaw(redisReply *r) {
sdsfree(tmp);
}
break;
+ case REDIS_REPLY_MAP:
+ for (i = 0; i < r->elements; i += 2) {
+ if (i > 0) out = sdscat(out,config.mb_delim);
+ tmp = cliFormatReplyRaw(r->element[i]);
+ out = sdscatlen(out,tmp,sdslen(tmp));
+ sdsfree(tmp);
+
+ out = sdscatlen(out," ",1);
+ tmp = cliFormatReplyRaw(r->element[i+1]);
+ out = sdscatlen(out,tmp,sdslen(tmp));
+ sdsfree(tmp);
+ }
+ break;
default:
fprintf(stderr,"Unknown reply type: %d\n", r->type);
exit(1);
@@ -1013,13 +1150,21 @@ static sds cliFormatReplyCSV(redisReply *r) {
case REDIS_REPLY_INTEGER:
out = sdscatprintf(out,"%lld",r->integer);
break;
+ case REDIS_REPLY_DOUBLE:
+ out = sdscatprintf(out,"%s",r->str);
+ break;
case REDIS_REPLY_STRING:
+ case REDIS_REPLY_VERB:
out = sdscatrepr(out,r->str,r->len);
break;
case REDIS_REPLY_NIL:
- out = sdscat(out,"NIL");
+ out = sdscat(out,"NULL");
+ break;
+ case REDIS_REPLY_BOOL:
+ out = sdscat(out,r->integer ? "true" : "false");
break;
case REDIS_REPLY_ARRAY:
+ case REDIS_REPLY_MAP: /* CSV has no map type, just output flat list. */
for (i = 0; i < r->elements; i++) {
sds tmp = cliFormatReplyCSV(r->element[i]);
out = sdscatlen(out,tmp,sdslen(tmp));
@@ -1213,7 +1358,8 @@ static int cliSendCommand(int argc, char **argv, long repeat) {
if (!strcasecmp(command,"select") && argc == 2 && config.last_cmd_type != REDIS_REPLY_ERROR) {
config.dbnum = atoi(argv[1]);
cliRefreshPrompt();
- } else if (!strcasecmp(command,"auth") && argc == 2) {
+ } else if (!strcasecmp(command,"auth") && (argc == 2 || argc == 3))
+ {
cliSelect();
}
}
@@ -1245,6 +1391,13 @@ static redisReply *reconnectingRedisCommand(redisContext *c, const char *fmt, ..
redisFree(c);
c = redisConnect(config.hostip,config.hostport);
+ if (!c->err && config.tls) {
+ const char *err = NULL;
+ if (cliSecureConnection(c, &err) == REDIS_ERR && err) {
+ fprintf(stderr, "TLS Error: %s\n", err);
+ exit(1);
+ }
+ }
usleep(1000000);
}
@@ -1296,8 +1449,12 @@ static int parseOptions(int argc, char **argv) {
config.dbnum = atoi(argv[++i]);
} else if (!strcmp(argv[i], "--no-auth-warning")) {
config.no_auth_warning = 1;
- } else if (!strcmp(argv[i],"-a") && !lastarg) {
+ } else if ((!strcmp(argv[i],"-a") || !strcmp(argv[i],"--pass"))
+ && !lastarg)
+ {
config.auth = argv[++i];
+ } else if (!strcmp(argv[i],"--user") && !lastarg) {
+ config.user = argv[++i];
} else if (!strcmp(argv[i],"-u") && !lastarg) {
parseRedisUri(argv[++i]);
} else if (!strcmp(argv[i],"--raw")) {
@@ -1434,11 +1591,27 @@ static int parseOptions(int argc, char **argv) {
} else if (!strcmp(argv[i],"--cluster-search-multiple-owners")) {
config.cluster_manager_command.flags |=
CLUSTER_MANAGER_CMD_FLAG_CHECK_OWNERS;
+#ifdef USE_OPENSSL
+ } else if (!strcmp(argv[i],"--tls")) {
+ config.tls = 1;
+ } else if (!strcmp(argv[i],"--sni")) {
+ config.sni = argv[++i];
+ } else if (!strcmp(argv[i],"--cacertdir")) {
+ config.cacertdir = argv[++i];
+ } else if (!strcmp(argv[i],"--cacert")) {
+ config.cacert = argv[++i];
+ } else if (!strcmp(argv[i],"--cert")) {
+ config.cert = argv[++i];
+ } else if (!strcmp(argv[i],"--key")) {
+ config.key = argv[++i];
+#endif
} else if (!strcmp(argv[i],"-v") || !strcmp(argv[i], "--version")) {
sds version = cliVersion();
printf("redis-cli %s\n", version);
sdsfree(version);
exit(0);
+ } else if (!strcmp(argv[i],"-3")) {
+ config.resp3 = 1;
} else if (CLUSTER_MANAGER_MODE() && argv[i][0] != '-') {
if (config.cluster_manager_command.argc == 0) {
int j = i + 1;
@@ -1514,14 +1687,26 @@ static void usage(void) {
" You can also use the " REDIS_CLI_AUTH_ENV " environment\n"
" variable to pass this password more safely\n"
" (if both are used, this argument takes predecence).\n"
+" -user <username> Used to send ACL style 'AUTH username pass'. Needs -a.\n"
+" -pass <password> Alias of -a for consistency with the new --user option.\n"
" -u <uri> Server URI.\n"
" -r <repeat> Execute specified command N times.\n"
" -i <interval> When -r is used, waits <interval> seconds per command.\n"
" It is possible to specify sub-second times like -i 0.1.\n"
" -n <db> Database number.\n"
+" -3 Start session in RESP3 protocol mode.\n"
" -x Read last argument from STDIN.\n"
" -d <delimiter> Multi-bulk delimiter in for raw formatting (default: \\n).\n"
" -c Enable cluster mode (follow -ASK and -MOVED redirections).\n"
+#ifdef USE_OPENSSL
+" --tls Establish a secure TLS connection.\n"
+" --cacert CA Certificate file to verify with.\n"
+" --cacertdir Directory where trusted CA certificates are stored.\n"
+" If neither cacert nor cacertdir are specified, the default\n"
+" system-wide trusted root certs configuration will apply.\n"
+" --cert Client certificate to authenticate with.\n"
+" --key Private key file to authenticate with.\n"
+#endif
" --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"
@@ -1533,7 +1718,9 @@ static void usage(void) {
" --csv is specified, or if you redirect the output to a non\n"
" TTY, it samples the latency for 1 second (you can use\n"
" -i to change the interval), then produces a single output\n"
-" and exits.\n"
+" and exits.\n",version);
+
+ fprintf(stderr,
" --latency-history Like --latency but tracking latency changes over time.\n"
" Default time interval is 15 sec. Change it using -i.\n"
" --latency-dist Shows latency as a spectrum, requires xterm 256 colors.\n"
@@ -1544,7 +1731,9 @@ static void usage(void) {
" --pipe Transfer raw Redis protocol from stdin to server.\n"
" --pipe-timeout <n> In --pipe mode, abort with error if after sending all data.\n"
" no reply is received within <n> seconds.\n"
-" Default timeout: %d. Use 0 to wait forever.\n"
+" Default timeout: %d. Use 0 to wait forever.\n",
+ REDIS_CLI_DEFAULT_PIPE_TIMEOUT);
+ fprintf(stderr,
" --bigkeys Sample Redis keys looking for keys with many elements (complexity).\n"
" --memkeys Sample Redis keys looking for keys consuming a lot of memory.\n"
" --memkeys-samples <n> Sample Redis keys looking for keys consuming a lot of memory.\n"
@@ -1567,8 +1756,7 @@ static void usage(void) {
" line interface.\n"
" --help Output this help and exit.\n"
" --version Output version and exit.\n"
-"\n",
- version, REDIS_CLI_DEFAULT_PIPE_TIMEOUT);
+"\n");
/* Using another fprintf call to avoid -Woverlength-strings compile warning */
fprintf(stderr,
"Cluster Manager Commands:\n"
@@ -2336,6 +2524,15 @@ cleanup:
static int clusterManagerNodeConnect(clusterManagerNode *node) {
if (node->context) redisFree(node->context);
node->context = redisConnect(node->ip, node->port);
+ if (!node->context->err && config.tls) {
+ const char *err = NULL;
+ if (cliSecureConnection(node->context, &err) == REDIS_ERR && err) {
+ fprintf(stderr,"TLS Error: %s\n", err);
+ redisFree(node->context);
+ node->context = NULL;
+ return 0;
+ }
+ }
if (node->context->err) {
fprintf(stderr,"Could not connect to Redis at ");
fprintf(stderr,"%s:%d: %s\n", node->ip, node->port,
@@ -2350,7 +2547,12 @@ static int clusterManagerNodeConnect(clusterManagerNode *node) {
* errors. */
anetKeepAlive(NULL, node->context->fd, REDIS_CLI_KEEPALIVE_INTERVAL);
if (config.auth) {
- redisReply *reply = redisCommand(node->context,"AUTH %s",config.auth);
+ redisReply *reply;
+ if (config.user == NULL)
+ reply = redisCommand(node->context,"AUTH %s", config.auth);
+ else
+ reply = redisCommand(node->context,"AUTH %s %s",
+ config.user,config.auth);
int ok = clusterManagerCheckRedisReply(node, reply, NULL);
if (reply != NULL) freeReplyObject(reply);
if (!ok) return 0;
@@ -3222,7 +3424,7 @@ static redisReply *clusterManagerMigrateKeysInReply(clusterManagerNode *source,
redisReply *entry = reply->element[i];
size_t idx = i + offset;
assert(entry->type == REDIS_REPLY_STRING);
- argv[idx] = (char *) sdsnew(entry->str);
+ argv[idx] = (char *) sdsnewlen(entry->str, entry->len);
argv_len[idx] = entry->len;
if (dots) dots[i] = '.';
}
@@ -7678,6 +7880,7 @@ int main(int argc, char **argv) {
config.hotkeys = 0;
config.stdinarg = 0;
config.auth = NULL;
+ config.user = NULL;
config.eval = NULL;
config.eval_ldb = 0;
config.eval_ldb_end = 0;