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 @@ | |||
| 1 | /* CLI (command line interface) common methods | ||
| 2 | * | ||
| 3 | * Copyright (c) 2020-Present, Redis Ltd. | ||
| 4 | * All rights reserved. | ||
| 5 | * | ||
| 6 | * Licensed under your choice of (a) the Redis Source Available License 2.0 | ||
| 7 | * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the | ||
| 8 | * GNU Affero General Public License v3 (AGPLv3). | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include "fmacros.h" | ||
| 12 | #include "cli_common.h" | ||
| 13 | #include "version.h" | ||
| 14 | |||
| 15 | #include <stdio.h> | ||
| 16 | #include <stdlib.h> | ||
| 17 | #include <fcntl.h> | ||
| 18 | #include <errno.h> | ||
| 19 | #include <hiredis.h> | ||
| 20 | #include <sdscompat.h> /* Use hiredis' sds compat header that maps sds calls to their hi_ variants */ | ||
| 21 | #include <sds.h> /* use sds.h from hiredis, so that only one set of sds functions will be present in the binary */ | ||
| 22 | #include <unistd.h> | ||
| 23 | #include <string.h> | ||
| 24 | #include <ctype.h> | ||
| 25 | #ifdef USE_OPENSSL | ||
| 26 | #include <openssl/ssl.h> | ||
| 27 | #include <openssl/err.h> | ||
| 28 | #include <hiredis_ssl.h> | ||
| 29 | #endif | ||
| 30 | |||
| 31 | #define UNUSED(V) ((void) V) | ||
| 32 | |||
| 33 | char *redisGitSHA1(void); | ||
| 34 | char *redisGitDirty(void); | ||
| 35 | |||
| 36 | /* Wrapper around redisSecureConnection to avoid hiredis_ssl dependencies if | ||
| 37 | * not building with TLS support. | ||
| 38 | */ | ||
| 39 | int cliSecureConnection(redisContext *c, cliSSLconfig config, const char **err) { | ||
| 40 | #ifdef USE_OPENSSL | ||
| 41 | static SSL_CTX *ssl_ctx = NULL; | ||
| 42 | |||
| 43 | if (!ssl_ctx) { | ||
| 44 | ssl_ctx = SSL_CTX_new(SSLv23_client_method()); | ||
| 45 | if (!ssl_ctx) { | ||
| 46 | *err = "Failed to create SSL_CTX"; | ||
| 47 | goto error; | ||
| 48 | } | ||
| 49 | SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); | ||
| 50 | SSL_CTX_set_verify(ssl_ctx, config.skip_cert_verify ? SSL_VERIFY_NONE : SSL_VERIFY_PEER, NULL); | ||
| 51 | |||
| 52 | if (config.cacert || config.cacertdir) { | ||
| 53 | if (!SSL_CTX_load_verify_locations(ssl_ctx, config.cacert, config.cacertdir)) { | ||
| 54 | *err = "Invalid CA Certificate File/Directory"; | ||
| 55 | goto error; | ||
| 56 | } | ||
| 57 | } else { | ||
| 58 | if (!SSL_CTX_set_default_verify_paths(ssl_ctx)) { | ||
| 59 | *err = "Failed to use default CA paths"; | ||
| 60 | goto error; | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | if (config.cert && !SSL_CTX_use_certificate_chain_file(ssl_ctx, config.cert)) { | ||
| 65 | *err = "Invalid client certificate"; | ||
| 66 | goto error; | ||
| 67 | } | ||
| 68 | |||
| 69 | if (config.key && !SSL_CTX_use_PrivateKey_file(ssl_ctx, config.key, SSL_FILETYPE_PEM)) { | ||
| 70 | *err = "Invalid private key"; | ||
| 71 | goto error; | ||
| 72 | } | ||
| 73 | if (config.ciphers && !SSL_CTX_set_cipher_list(ssl_ctx, config.ciphers)) { | ||
| 74 | *err = "Error while configuring ciphers"; | ||
| 75 | goto error; | ||
| 76 | } | ||
| 77 | #ifdef TLS1_3_VERSION | ||
| 78 | if (config.ciphersuites && !SSL_CTX_set_ciphersuites(ssl_ctx, config.ciphersuites)) { | ||
| 79 | *err = "Error while setting cypher suites"; | ||
| 80 | goto error; | ||
| 81 | } | ||
| 82 | #endif | ||
| 83 | } | ||
| 84 | |||
| 85 | SSL *ssl = SSL_new(ssl_ctx); | ||
| 86 | if (!ssl) { | ||
| 87 | *err = "Failed to create SSL object"; | ||
| 88 | return REDIS_ERR; | ||
| 89 | } | ||
| 90 | |||
| 91 | if (config.sni && !SSL_set_tlsext_host_name(ssl, config.sni)) { | ||
| 92 | *err = "Failed to configure SNI"; | ||
| 93 | SSL_free(ssl); | ||
| 94 | return REDIS_ERR; | ||
| 95 | } | ||
| 96 | |||
| 97 | return redisInitiateSSL(c, ssl); | ||
| 98 | |||
| 99 | error: | ||
| 100 | SSL_CTX_free(ssl_ctx); | ||
| 101 | ssl_ctx = NULL; | ||
| 102 | return REDIS_ERR; | ||
| 103 | #else | ||
| 104 | (void) config; | ||
| 105 | (void) c; | ||
| 106 | (void) err; | ||
| 107 | return REDIS_OK; | ||
| 108 | #endif | ||
| 109 | } | ||
| 110 | |||
| 111 | /* Wrapper around hiredis to allow arbitrary reads and writes. | ||
| 112 | * | ||
| 113 | * We piggybacks on top of hiredis to achieve transparent TLS support, | ||
| 114 | * and use its internal buffers so it can co-exist with commands | ||
| 115 | * previously/later issued on the connection. | ||
| 116 | * | ||
| 117 | * Interface is close to enough to read()/write() so things should mostly | ||
| 118 | * work transparently. | ||
| 119 | */ | ||
| 120 | |||
| 121 | /* Write a raw buffer through a redisContext. If we already have something | ||
| 122 | * in the buffer (leftovers from hiredis operations) it will be written | ||
| 123 | * as well. | ||
| 124 | */ | ||
| 125 | ssize_t cliWriteConn(redisContext *c, const char *buf, size_t buf_len) | ||
| 126 | { | ||
| 127 | int done = 0; | ||
| 128 | |||
| 129 | /* Append data to buffer which is *usually* expected to be empty | ||
| 130 | * but we don't assume that, and write. | ||
| 131 | */ | ||
| 132 | c->obuf = sdscatlen(c->obuf, buf, buf_len); | ||
| 133 | if (redisBufferWrite(c, &done) == REDIS_ERR) { | ||
| 134 | if (!(c->flags & REDIS_BLOCK)) | ||
| 135 | errno = EAGAIN; | ||
| 136 | |||
| 137 | /* On error, we assume nothing was written and we roll back the | ||
| 138 | * buffer to its original state. | ||
| 139 | */ | ||
| 140 | if (sdslen(c->obuf) > buf_len) | ||
| 141 | sdsrange(c->obuf, 0, -(buf_len+1)); | ||
| 142 | else | ||
| 143 | sdsclear(c->obuf); | ||
| 144 | |||
| 145 | return -1; | ||
| 146 | } | ||
| 147 | |||
| 148 | /* If we're done, free up everything. We may have written more than | ||
| 149 | * buf_len (if c->obuf was not initially empty) but we don't have to | ||
| 150 | * tell. | ||
| 151 | */ | ||
| 152 | if (done) { | ||
| 153 | sdsclear(c->obuf); | ||
| 154 | return buf_len; | ||
| 155 | } | ||
| 156 | |||
| 157 | /* Write was successful but we have some leftovers which we should | ||
| 158 | * remove from the buffer. | ||
| 159 | * | ||
| 160 | * Do we still have data that was there prior to our buf? If so, | ||
| 161 | * restore buffer to it's original state and report no new data was | ||
| 162 | * written. | ||
| 163 | */ | ||
| 164 | if (sdslen(c->obuf) > buf_len) { | ||
| 165 | sdsrange(c->obuf, 0, -(buf_len+1)); | ||
| 166 | return 0; | ||
| 167 | } | ||
| 168 | |||
| 169 | /* At this point we're sure no prior data is left. We flush the buffer | ||
| 170 | * and report how much we've written. | ||
| 171 | */ | ||
| 172 | size_t left = sdslen(c->obuf); | ||
| 173 | sdsclear(c->obuf); | ||
| 174 | return buf_len - left; | ||
| 175 | } | ||
| 176 | |||
| 177 | /* Wrapper around OpenSSL (libssl and libcrypto) initialisation | ||
| 178 | */ | ||
| 179 | int cliSecureInit(void) | ||
| 180 | { | ||
| 181 | #ifdef USE_OPENSSL | ||
| 182 | ERR_load_crypto_strings(); | ||
| 183 | SSL_load_error_strings(); | ||
| 184 | SSL_library_init(); | ||
| 185 | #endif | ||
| 186 | return REDIS_OK; | ||
| 187 | } | ||
| 188 | |||
| 189 | /* Create an sds from stdin */ | ||
| 190 | sds readArgFromStdin(void) { | ||
| 191 | char buf[1024]; | ||
| 192 | sds arg = sdsempty(); | ||
| 193 | |||
| 194 | while(1) { | ||
| 195 | int nread = read(fileno(stdin),buf,1024); | ||
| 196 | |||
| 197 | if (nread == 0) break; | ||
| 198 | else if (nread == -1) { | ||
| 199 | perror("Reading from standard input"); | ||
| 200 | exit(1); | ||
| 201 | } | ||
| 202 | arg = sdscatlen(arg,buf,nread); | ||
| 203 | } | ||
| 204 | return arg; | ||
| 205 | } | ||
| 206 | |||
| 207 | /* Create an sds array from argv, either as-is or by dequoting every | ||
| 208 | * element. When quoted is non-zero, may return a NULL to indicate an | ||
| 209 | * invalid quoted string. | ||
| 210 | * | ||
| 211 | * The caller should free the resulting array of sds strings with | ||
| 212 | * sdsfreesplitres(). | ||
| 213 | */ | ||
| 214 | sds *getSdsArrayFromArgv(int argc,char **argv, int quoted) { | ||
| 215 | sds *res = sds_malloc(sizeof(sds) * argc); | ||
| 216 | |||
| 217 | for (int j = 0; j < argc; j++) { | ||
| 218 | if (quoted) { | ||
| 219 | sds unquoted = unquoteCString(argv[j]); | ||
| 220 | if (!unquoted) { | ||
| 221 | while (--j >= 0) sdsfree(res[j]); | ||
| 222 | sds_free(res); | ||
| 223 | return NULL; | ||
| 224 | } | ||
| 225 | res[j] = unquoted; | ||
| 226 | } else { | ||
| 227 | res[j] = sdsnew(argv[j]); | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | return res; | ||
| 232 | } | ||
| 233 | |||
| 234 | /* Unquote a null-terminated string and return it as a binary-safe sds. */ | ||
| 235 | sds unquoteCString(char *str) { | ||
| 236 | int count; | ||
| 237 | sds *unquoted = sdssplitargs(str, &count); | ||
| 238 | sds res = NULL; | ||
| 239 | |||
| 240 | if (unquoted && count == 1) { | ||
| 241 | res = unquoted[0]; | ||
| 242 | unquoted[0] = NULL; | ||
| 243 | } | ||
| 244 | |||
| 245 | if (unquoted) | ||
| 246 | sdsfreesplitres(unquoted, count); | ||
| 247 | |||
| 248 | return res; | ||
| 249 | } | ||
| 250 | |||
| 251 | |||
| 252 | /* URL-style percent decoding. */ | ||
| 253 | #define isHexChar(c) (isdigit(c) || ((c) >= 'a' && (c) <= 'f')) | ||
| 254 | #define decodeHexChar(c) (isdigit(c) ? (c) - '0' : (c) - 'a' + 10) | ||
| 255 | #define decodeHex(h, l) ((decodeHexChar(h) << 4) + decodeHexChar(l)) | ||
| 256 | |||
| 257 | static sds percentDecode(const char *pe, size_t len) { | ||
| 258 | const char *end = pe + len; | ||
| 259 | sds ret = sdsempty(); | ||
| 260 | const char *curr = pe; | ||
| 261 | |||
| 262 | while (curr < end) { | ||
| 263 | if (*curr == '%') { | ||
| 264 | if ((end - curr) < 2) { | ||
| 265 | fprintf(stderr, "Incomplete URI encoding\n"); | ||
| 266 | exit(1); | ||
| 267 | } | ||
| 268 | |||
| 269 | char h = tolower(*(++curr)); | ||
| 270 | char l = tolower(*(++curr)); | ||
| 271 | if (!isHexChar(h) || !isHexChar(l)) { | ||
| 272 | fprintf(stderr, "Illegal character in URI encoding\n"); | ||
| 273 | exit(1); | ||
| 274 | } | ||
| 275 | char c = decodeHex(h, l); | ||
| 276 | ret = sdscatlen(ret, &c, 1); | ||
| 277 | curr++; | ||
| 278 | } else { | ||
| 279 | ret = sdscatlen(ret, curr++, 1); | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | return ret; | ||
| 284 | } | ||
| 285 | |||
| 286 | /* Parse a URI and extract the server connection information. | ||
| 287 | * URI scheme is based on the provisional specification[1] excluding support | ||
| 288 | * for query parameters. Valid URIs are: | ||
| 289 | * scheme: "redis://" | ||
| 290 | * authority: [[<username> ":"] <password> "@"] [<hostname> [":" <port>]] | ||
| 291 | * path: ["/" [<db>]] | ||
| 292 | * | ||
| 293 | * [1]: https://www.iana.org/assignments/uri-schemes/prov/redis */ | ||
| 294 | void parseRedisUri(const char *uri, const char* tool_name, cliConnInfo *connInfo, int *tls_flag) { | ||
| 295 | #ifdef USE_OPENSSL | ||
| 296 | UNUSED(tool_name); | ||
| 297 | #else | ||
| 298 | UNUSED(tls_flag); | ||
| 299 | #endif | ||
| 300 | |||
| 301 | const char *scheme = "redis://"; | ||
| 302 | const char *tlsscheme = "rediss://"; | ||
| 303 | const char *curr = uri; | ||
| 304 | const char *end = uri + strlen(uri); | ||
| 305 | const char *userinfo, *username, *port, *host, *path; | ||
| 306 | |||
| 307 | /* URI must start with a valid scheme. */ | ||
| 308 | if (!strncasecmp(tlsscheme, curr, strlen(tlsscheme))) { | ||
| 309 | #ifdef USE_OPENSSL | ||
| 310 | *tls_flag = 1; | ||
| 311 | curr += strlen(tlsscheme); | ||
| 312 | #else | ||
| 313 | fprintf(stderr,"rediss:// is only supported when %s is compiled with OpenSSL\n", tool_name); | ||
| 314 | exit(1); | ||
| 315 | #endif | ||
| 316 | } else if (!strncasecmp(scheme, curr, strlen(scheme))) { | ||
| 317 | curr += strlen(scheme); | ||
| 318 | } else { | ||
| 319 | fprintf(stderr,"Invalid URI scheme\n"); | ||
| 320 | exit(1); | ||
| 321 | } | ||
| 322 | if (curr == end) return; | ||
| 323 | |||
| 324 | /* Extract user info. */ | ||
| 325 | if ((userinfo = strchr(curr,'@'))) { | ||
| 326 | if ((username = strchr(curr, ':')) && username < userinfo) { | ||
| 327 | connInfo->user = percentDecode(curr, username - curr); | ||
| 328 | curr = username + 1; | ||
| 329 | } | ||
| 330 | |||
| 331 | connInfo->auth = percentDecode(curr, userinfo - curr); | ||
| 332 | curr = userinfo + 1; | ||
| 333 | } | ||
| 334 | if (curr == end) return; | ||
| 335 | |||
| 336 | /* Extract host and port. */ | ||
| 337 | path = strchr(curr, '/'); | ||
| 338 | if (*curr != '/') { | ||
| 339 | host = path ? path - 1 : end; | ||
| 340 | if (*curr == '[') { | ||
| 341 | curr += 1; | ||
| 342 | if ((port = strchr(curr, ']'))) { | ||
| 343 | if (*(port+1) == ':') { | ||
| 344 | connInfo->hostport = atoi(port + 2); | ||
| 345 | } | ||
| 346 | host = port - 1; | ||
| 347 | } | ||
| 348 | } else { | ||
| 349 | if ((port = strchr(curr, ':'))) { | ||
| 350 | connInfo->hostport = atoi(port + 1); | ||
| 351 | host = port - 1; | ||
| 352 | } | ||
| 353 | } | ||
| 354 | sdsfree(connInfo->hostip); | ||
| 355 | connInfo->hostip = sdsnewlen(curr, host - curr + 1); | ||
| 356 | } | ||
| 357 | curr = path ? path + 1 : end; | ||
| 358 | if (curr == end) return; | ||
| 359 | |||
| 360 | /* Extract database number. */ | ||
| 361 | connInfo->input_dbnum = atoi(curr); | ||
| 362 | } | ||
| 363 | |||
| 364 | void freeCliConnInfo(cliConnInfo connInfo){ | ||
| 365 | if (connInfo.hostip) sdsfree(connInfo.hostip); | ||
| 366 | if (connInfo.auth) sdsfree(connInfo.auth); | ||
| 367 | if (connInfo.user) sdsfree(connInfo.user); | ||
| 368 | } | ||
| 369 | |||
| 370 | /* | ||
| 371 | * Escape a Unicode string for JSON output (--json), following RFC 7159: | ||
| 372 | * https://datatracker.ietf.org/doc/html/rfc7159#section-7 | ||
| 373 | */ | ||
| 374 | sds escapeJsonString(sds s, const char *p, size_t len) { | ||
| 375 | s = sdscatlen(s,"\"",1); | ||
| 376 | while(len--) { | ||
| 377 | switch(*p) { | ||
| 378 | case '\\': | ||
| 379 | case '"': | ||
| 380 | s = sdscatprintf(s,"\\%c",*p); | ||
| 381 | break; | ||
| 382 | case '\n': s = sdscatlen(s,"\\n",2); break; | ||
| 383 | case '\f': s = sdscatlen(s,"\\f",2); break; | ||
| 384 | case '\r': s = sdscatlen(s,"\\r",2); break; | ||
| 385 | case '\t': s = sdscatlen(s,"\\t",2); break; | ||
| 386 | case '\b': s = sdscatlen(s,"\\b",2); break; | ||
| 387 | default: | ||
| 388 | s = sdscatprintf(s,*(unsigned char *)p <= 0x1f ? "\\u%04x" : "%c",*p); | ||
| 389 | } | ||
| 390 | p++; | ||
| 391 | } | ||
| 392 | return sdscatlen(s,"\"",1); | ||
| 393 | } | ||
| 394 | |||
| 395 | sds cliVersion(void) { | ||
| 396 | sds version = sdscatprintf(sdsempty(), "%s", REDIS_VERSION); | ||
| 397 | |||
| 398 | /* Add git commit and working tree status when available. */ | ||
| 399 | if (strtoll(redisGitSHA1(),NULL,16)) { | ||
| 400 | version = sdscatprintf(version, " (git:%s", redisGitSHA1()); | ||
| 401 | if (strtoll(redisGitDirty(),NULL,10)) | ||
| 402 | version = sdscatprintf(version, "-dirty"); | ||
| 403 | version = sdscat(version, ")"); | ||
| 404 | } | ||
| 405 | return version; | ||
| 406 | } | ||
| 407 | |||
| 408 | /* This is a wrapper to call redisConnect or redisConnectWithTimeout. */ | ||
| 409 | redisContext *redisConnectWrapper(const char *ip, int port, const struct timeval tv) { | ||
| 410 | if (tv.tv_sec == 0 && tv.tv_usec == 0) { | ||
| 411 | return redisConnect(ip, port); | ||
| 412 | } else { | ||
| 413 | return redisConnectWithTimeout(ip, port, tv); | ||
| 414 | } | ||
| 415 | } | ||
| 416 | |||
| 417 | /* This is a wrapper to call redisConnectUnix or redisConnectUnixWithTimeout. */ | ||
| 418 | redisContext *redisConnectUnixWrapper(const char *path, const struct timeval tv) { | ||
| 419 | if (tv.tv_sec == 0 && tv.tv_usec == 0) { | ||
| 420 | return redisConnectUnix(path); | ||
| 421 | } else { | ||
| 422 | return redisConnectUnixWithTimeout(path, tv); | ||
| 423 | } | ||
| 424 | } | ||
