diff options
Diffstat (limited to 'examples/redis-unstable/src/cli_common.c')
| -rw-r--r-- | examples/redis-unstable/src/cli_common.c | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/examples/redis-unstable/src/cli_common.c b/examples/redis-unstable/src/cli_common.c new file mode 100644 index 0000000..0c269de --- /dev/null +++ b/examples/redis-unstable/src/cli_common.c @@ -0,0 +1,424 @@ +/* CLI (command line interface) common methods + * + * Copyright (c) 2020-Present, Redis Ltd. + * All rights reserved. + * + * Licensed under your choice of (a) the Redis Source Available License 2.0 + * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the + * GNU Affero General Public License v3 (AGPLv3). + */ + +#include "fmacros.h" +#include "cli_common.h" +#include "version.h" + +#include <stdio.h> +#include <stdlib.h> +#include <fcntl.h> +#include <errno.h> +#include <hiredis.h> +#include <sdscompat.h> /* Use hiredis' sds compat header that maps sds calls to their hi_ variants */ +#include <sds.h> /* use sds.h from hiredis, so that only one set of sds functions will be present in the binary */ +#include <unistd.h> +#include <string.h> +#include <ctype.h> +#ifdef USE_OPENSSL +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <hiredis_ssl.h> +#endif + +#define UNUSED(V) ((void) V) + +char *redisGitSHA1(void); +char *redisGitDirty(void); + +/* Wrapper around redisSecureConnection to avoid hiredis_ssl dependencies if + * not building with TLS support. + */ +int cliSecureConnection(redisContext *c, cliSSLconfig config, 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, config.skip_cert_verify ? SSL_VERIFY_NONE : 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; + } + if (config.ciphers && !SSL_CTX_set_cipher_list(ssl_ctx, config.ciphers)) { + *err = "Error while configuring ciphers"; + goto error; + } +#ifdef TLS1_3_VERSION + if (config.ciphersuites && !SSL_CTX_set_ciphersuites(ssl_ctx, config.ciphersuites)) { + *err = "Error while setting cypher suites"; + goto error; + } +#endif + } + + 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) config; + (void) c; + (void) err; + return REDIS_OK; +#endif +} + +/* Wrapper around hiredis to allow arbitrary reads and writes. + * + * We piggybacks on top of hiredis to achieve transparent TLS support, + * and use its internal buffers so it can co-exist with commands + * previously/later issued on the connection. + * + * Interface is close to enough to read()/write() so things should mostly + * work transparently. + */ + +/* Write a raw buffer through a redisContext. If we already have something + * in the buffer (leftovers from hiredis operations) it will be written + * as well. + */ +ssize_t cliWriteConn(redisContext *c, const char *buf, size_t buf_len) +{ + int done = 0; + + /* Append data to buffer which is *usually* expected to be empty + * but we don't assume that, and write. + */ + c->obuf = sdscatlen(c->obuf, buf, buf_len); + if (redisBufferWrite(c, &done) == REDIS_ERR) { + if (!(c->flags & REDIS_BLOCK)) + errno = EAGAIN; + + /* On error, we assume nothing was written and we roll back the + * buffer to its original state. + */ + if (sdslen(c->obuf) > buf_len) + sdsrange(c->obuf, 0, -(buf_len+1)); + else + sdsclear(c->obuf); + + return -1; + } + + /* If we're done, free up everything. We may have written more than + * buf_len (if c->obuf was not initially empty) but we don't have to + * tell. + */ + if (done) { + sdsclear(c->obuf); + return buf_len; + } + + /* Write was successful but we have some leftovers which we should + * remove from the buffer. + * + * Do we still have data that was there prior to our buf? If so, + * restore buffer to it's original state and report no new data was + * written. + */ + if (sdslen(c->obuf) > buf_len) { + sdsrange(c->obuf, 0, -(buf_len+1)); + return 0; + } + + /* At this point we're sure no prior data is left. We flush the buffer + * and report how much we've written. + */ + size_t left = sdslen(c->obuf); + sdsclear(c->obuf); + return buf_len - left; +} + +/* Wrapper around OpenSSL (libssl and libcrypto) initialisation + */ +int cliSecureInit(void) +{ +#ifdef USE_OPENSSL + ERR_load_crypto_strings(); + SSL_load_error_strings(); + SSL_library_init(); +#endif + return REDIS_OK; +} + +/* Create an sds from stdin */ +sds readArgFromStdin(void) { + char buf[1024]; + sds arg = sdsempty(); + + while(1) { + int nread = read(fileno(stdin),buf,1024); + + if (nread == 0) break; + else if (nread == -1) { + perror("Reading from standard input"); + exit(1); + } + arg = sdscatlen(arg,buf,nread); + } + return arg; +} + +/* 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. + * + * The caller should free the resulting array of sds strings with + * sdsfreesplitres(). + */ +sds *getSdsArrayFromArgv(int argc,char **argv, int quoted) { + sds *res = sds_malloc(sizeof(sds) * argc); + + 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 res; +} + +/* Unquote a null-terminated string and return it as a binary-safe sds. */ +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; +} + + +/* URL-style percent decoding. */ +#define isHexChar(c) (isdigit(c) || ((c) >= 'a' && (c) <= 'f')) +#define decodeHexChar(c) (isdigit(c) ? (c) - '0' : (c) - 'a' + 10) +#define decodeHex(h, l) ((decodeHexChar(h) << 4) + decodeHexChar(l)) + +static sds percentDecode(const char *pe, size_t len) { + const char *end = pe + len; + sds ret = sdsempty(); + const char *curr = pe; + + while (curr < end) { + if (*curr == '%') { + if ((end - curr) < 2) { + fprintf(stderr, "Incomplete URI encoding\n"); + exit(1); + } + + char h = tolower(*(++curr)); + char l = tolower(*(++curr)); + if (!isHexChar(h) || !isHexChar(l)) { + fprintf(stderr, "Illegal character in URI encoding\n"); + exit(1); + } + char c = decodeHex(h, l); + ret = sdscatlen(ret, &c, 1); + curr++; + } else { + ret = sdscatlen(ret, curr++, 1); + } + } + + return ret; +} + +/* Parse a URI and extract the server connection information. + * URI scheme is based on the provisional specification[1] excluding support + * for query parameters. Valid URIs are: + * scheme: "redis://" + * authority: [[<username> ":"] <password> "@"] [<hostname> [":" <port>]] + * path: ["/" [<db>]] + * + * [1]: https://www.iana.org/assignments/uri-schemes/prov/redis */ +void parseRedisUri(const char *uri, const char* tool_name, cliConnInfo *connInfo, int *tls_flag) { +#ifdef USE_OPENSSL + UNUSED(tool_name); +#else + UNUSED(tls_flag); +#endif + + const char *scheme = "redis://"; + const char *tlsscheme = "rediss://"; + const char *curr = uri; + const char *end = uri + strlen(uri); + const char *userinfo, *username, *port, *host, *path; + + /* URI must start with a valid scheme. */ + if (!strncasecmp(tlsscheme, curr, strlen(tlsscheme))) { +#ifdef USE_OPENSSL + *tls_flag = 1; + curr += strlen(tlsscheme); +#else + fprintf(stderr,"rediss:// is only supported when %s is compiled with OpenSSL\n", tool_name); + exit(1); +#endif + } else if (!strncasecmp(scheme, curr, strlen(scheme))) { + curr += strlen(scheme); + } else { + fprintf(stderr,"Invalid URI scheme\n"); + exit(1); + } + if (curr == end) return; + + /* Extract user info. */ + if ((userinfo = strchr(curr,'@'))) { + if ((username = strchr(curr, ':')) && username < userinfo) { + connInfo->user = percentDecode(curr, username - curr); + curr = username + 1; + } + + connInfo->auth = percentDecode(curr, userinfo - curr); + curr = userinfo + 1; + } + if (curr == end) return; + + /* Extract host and port. */ + path = strchr(curr, '/'); + if (*curr != '/') { + host = path ? path - 1 : end; + if (*curr == '[') { + curr += 1; + if ((port = strchr(curr, ']'))) { + if (*(port+1) == ':') { + connInfo->hostport = atoi(port + 2); + } + host = port - 1; + } + } else { + if ((port = strchr(curr, ':'))) { + connInfo->hostport = atoi(port + 1); + host = port - 1; + } + } + sdsfree(connInfo->hostip); + connInfo->hostip = sdsnewlen(curr, host - curr + 1); + } + curr = path ? path + 1 : end; + if (curr == end) return; + + /* Extract database number. */ + connInfo->input_dbnum = atoi(curr); +} + +void freeCliConnInfo(cliConnInfo connInfo){ + if (connInfo.hostip) sdsfree(connInfo.hostip); + if (connInfo.auth) sdsfree(connInfo.auth); + if (connInfo.user) sdsfree(connInfo.user); +} + +/* + * Escape a Unicode string for JSON output (--json), following RFC 7159: + * https://datatracker.ietf.org/doc/html/rfc7159#section-7 +*/ +sds escapeJsonString(sds s, const char *p, size_t len) { + s = sdscatlen(s,"\"",1); + while(len--) { + switch(*p) { + case '\\': + case '"': + s = sdscatprintf(s,"\\%c",*p); + break; + case '\n': s = sdscatlen(s,"\\n",2); break; + case '\f': s = sdscatlen(s,"\\f",2); break; + case '\r': s = sdscatlen(s,"\\r",2); break; + case '\t': s = sdscatlen(s,"\\t",2); break; + case '\b': s = sdscatlen(s,"\\b",2); break; + default: + s = sdscatprintf(s,*(unsigned char *)p <= 0x1f ? "\\u%04x" : "%c",*p); + } + p++; + } + return sdscatlen(s,"\"",1); +} + +sds cliVersion(void) { + sds version = sdscatprintf(sdsempty(), "%s", REDIS_VERSION); + + /* Add git commit and working tree status when available. */ + if (strtoll(redisGitSHA1(),NULL,16)) { + version = sdscatprintf(version, " (git:%s", redisGitSHA1()); + if (strtoll(redisGitDirty(),NULL,10)) + version = sdscatprintf(version, "-dirty"); + version = sdscat(version, ")"); + } + return version; +} + +/* This is a wrapper to call redisConnect or redisConnectWithTimeout. */ +redisContext *redisConnectWrapper(const char *ip, int port, const struct timeval tv) { + if (tv.tv_sec == 0 && tv.tv_usec == 0) { + return redisConnect(ip, port); + } else { + return redisConnectWithTimeout(ip, port, tv); + } +} + +/* This is a wrapper to call redisConnectUnix or redisConnectUnixWithTimeout. */ +redisContext *redisConnectUnixWrapper(const char *path, const struct timeval tv) { + if (tv.tv_sec == 0 && tv.tv_usec == 0) { + return redisConnectUnix(path); + } else { + return redisConnectUnixWithTimeout(path, tv); + } +} |
