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