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 | */ |
54 | int 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 | |
114 | error: |
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 | */ |
140 | ssize_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 | */ |
194 | int 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 */ |
205 | sds 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 | */ |
229 | sds *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. */ |
250 | sds 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 | |
272 | static 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 */ |
309 | void 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 | |
369 | void 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 | */ |
379 | sds 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 | |