aboutsummaryrefslogtreecommitdiff
path: root/examples/redis-unstable/src/cli_common.c
diff options
context:
space:
mode:
Diffstat (limited to 'examples/redis-unstable/src/cli_common.c')
-rw-r--r--examples/redis-unstable/src/cli_common.c424
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
33char *redisGitSHA1(void);
34char *redisGitDirty(void);
35
36/* Wrapper around redisSecureConnection to avoid hiredis_ssl dependencies if
37 * not building with TLS support.
38 */
39int 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
99error:
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 */
125ssize_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 */
179int 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 */
190sds 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 */
214sds *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. */
235sds 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
257static 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 */
294void 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
364void 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*/
374sds 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
395sds 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. */
409redisContext *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. */
418redisContext *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}