tidying
[exim.git] / src / src / lookups / redis.c
CommitLineData
9bdd29ad
TL
1/*************************************************
2* Exim - an Internet mail transport agent *
3*************************************************/
4
f9ba5e22 5/* Copyright (c) University of Cambridge 1995 - 2018 */
9bdd29ad
TL
6/* See the file NOTICE for conditions of use and distribution. */
7
8#include "../exim.h"
9
de78e2d5 10#ifdef LOOKUP_REDIS
9bdd29ad
TL
11
12#include "lf_functions.h"
13
14#include <hiredis/hiredis.h>
15
2d8d625b
JH
16#ifndef nele
17# define nele(arr) (sizeof(arr) / sizeof(*arr))
18#endif
19
9bdd29ad
TL
20/* Structure and anchor for caching connections. */
21typedef struct redis_connection {
2d8d625b
JH
22 struct redis_connection *next;
23 uschar *server;
24 redisContext *handle;
9bdd29ad
TL
25} redis_connection;
26
27static redis_connection *redis_connections = NULL;
28
2d8d625b 29
9bdd29ad 30static void *
d447dbd1 31redis_open(const uschar * filename, uschar ** errmsg)
9bdd29ad 32{
2d8d625b 33return (void *)(1);
9bdd29ad
TL
34}
35
2d8d625b 36
9bdd29ad
TL
37void
38redis_tidy(void)
39{
2d8d625b
JH
40redis_connection *cn;
41
42/* XXX: Not sure how often this is called!
43 Guess its called after every lookup which probably would mean to just
44 not use the _tidy() function at all and leave with exim exiting to
45 GC connections! */
46
47while ((cn = redis_connections))
48 {
49 redis_connections = cn->next;
42c7f0b4 50 DEBUG(D_lookup) debug_printf_indent("close REDIS connection: %s\n", cn->server);
2d8d625b
JH
51 redisFree(cn->handle);
52 }
9bdd29ad
TL
53}
54
2d8d625b 55
9bdd29ad 56/* This function is called from the find entry point to do the search for a
2d8d625b
JH
57single server.
58
59 Arguments:
60 query the query string
61 server the server string
62 resultptr where to store the result
63 errmsg where to point an error message
64 defer_break TRUE if no more servers are to be tried after DEFER
65 do_cache set false if data is changed
0b4dfe7a 66 opts options
2d8d625b
JH
67
68 The server string is of the form "host/dbnumber/password". The host can be
69 host:port. This string is in a nextinlist temporary buffer, so can be
70 overwritten.
71
72 Returns: OK, FAIL, or DEFER
73*/
74
9bdd29ad 75static int
2d8d625b 76perform_redis_search(const uschar *command, uschar *server, uschar **resultptr,
0b4dfe7a 77 uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
9bdd29ad 78{
2d8d625b
JH
79redisContext *redis_handle = NULL; /* Keep compilers happy */
80redisReply *redis_reply = NULL;
81redisReply *entry = NULL;
82redisReply *tentry = NULL;
83redis_connection *cn;
2d8d625b
JH
84int yield = DEFER;
85int i, j;
acec9514 86gstring * result = NULL;
2d8d625b 87uschar *server_copy = NULL;
2d8d625b
JH
88uschar *sdata[3];
89
90/* Disaggregate the parameters from the server argument.
91The order is host:port(socket)
92We can write to the string, since it is in a nextinlist temporary buffer.
93This copy is also used for debugging output. */
94
95memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */;
d7978c0f 96for (int i = 2; i > 0; i--)
2d8d625b
JH
97 {
98 uschar *pp = Ustrrchr(server, '/');
99
100 if (!pp)
101 {
102 *errmsg = string_sprintf("incomplete Redis server data: %s",
103 i == 2 ? server : server_copy);
104 *defer_break = TRUE;
105 return DEFER;
106 }
107 *pp++ = 0;
108 sdata[i] = pp;
109 if (i == 2) server_copy = string_copy(server); /* sans password */
110 }
111sdata[0] = server; /* What's left at the start */
112
113/* If the database or password is an empty string, set it NULL */
114if (sdata[1][0] == 0) sdata[1] = NULL;
115if (sdata[2][0] == 0) sdata[2] = NULL;
116
117/* See if we have a cached connection to the server */
118
de78e2d5 119for (cn = redis_connections; cn; cn = cn->next)
2d8d625b
JH
120 if (Ustrcmp(cn->server, server_copy) == 0)
121 {
122 redis_handle = cn->handle;
123 break;
124 }
125
126if (!cn)
127 {
128 uschar *p;
129 uschar *socket = NULL;
130 int port = 0;
131 /* int redis_err = REDIS_OK; */
132
133 if ((p = Ustrchr(sdata[0], '(')))
134 {
135 *p++ = 0;
136 socket = p;
137 while (*p != 0 && *p != ')') p++;
138 *p = 0;
139 }
140
141 if ((p = Ustrchr(sdata[0], ':')))
142 {
143 *p++ = 0;
144 port = Uatoi(p);
145 }
146 else
147 port = Uatoi("6379");
148
149 if (Ustrchr(server, '/'))
150 {
151 *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s",
152 sdata[0]);
153 *defer_break = TRUE;
154 return DEFER;
155 }
156
157 DEBUG(D_lookup)
42c7f0b4 158 debug_printf_indent("REDIS new connection: host=%s port=%d socket=%s database=%s\n",
2d8d625b
JH
159 sdata[0], port, socket, sdata[1]);
160
161 /* Get store for a new handle, initialize it, and connect to the server */
162 /* XXX: Use timeouts ? */
163 redis_handle =
164 socket ? redisConnectUnix(CCS socket) : redisConnect(CCS server, port);
165 if (!redis_handle)
166 {
f3ebb786 167 *errmsg = US"REDIS connection failed";
2d8d625b
JH
168 *defer_break = FALSE;
169 goto REDIS_EXIT;
170 }
171
172 /* Add the connection to the cache */
f3ebb786 173 cn = store_get(sizeof(redis_connection), FALSE);
2d8d625b
JH
174 cn->server = server_copy;
175 cn->handle = redis_handle;
176 cn->next = redis_connections;
177 redis_connections = cn;
178 }
179else
180 {
181 DEBUG(D_lookup)
42c7f0b4 182 debug_printf_indent("REDIS using cached connection for %s\n", server_copy);
2d8d625b
JH
183}
184
185/* Authenticate if there is a password */
186if(sdata[2])
187 if (!(redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])))
188 {
189 *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr);
190 *defer_break = FALSE;
191 goto REDIS_EXIT;
192 }
193
194/* Select the database if there is a dbnumber passed */
195if(sdata[1])
196 {
197 if (!(redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])))
198 {
199 *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr);
200 *defer_break = FALSE;
201 goto REDIS_EXIT;
202 }
42c7f0b4 203 DEBUG(D_lookup) debug_printf_indent("REDIS: Selecting database=%s\n", sdata[1]);
2d8d625b
JH
204 }
205
206/* split string on whitespace into argv */
207 {
208 uschar * argv[32];
42653575 209 const uschar * s = command;
d7978c0f 210 int siz, ptr, i;
2d8d625b
JH
211 uschar c;
212
213 while (isspace(*s)) s++;
214
42653575 215 for (i = 0; *s && i < nele(argv); i++)
2d8d625b 216 {
acec9514
JH
217 gstring * g;
218
219 for (g = NULL; (c = *s) && !isspace(c); s++)
2d8d625b 220 if (c != '\\' || *++s) /* backslash protects next char */
acec9514
JH
221 g = string_catn(g, s, 1);
222 argv[i] = string_from_gstring(g);
223
42c7f0b4 224 DEBUG(D_lookup) debug_printf_indent("REDIS: argv[%d] '%s'\n", i, argv[i]);
2d8d625b
JH
225 while (isspace(*s)) s++;
226 }
227
228 /* Run the command. We use the argv form rather than plain as that parses
229 into args by whitespace yet has no escaping mechanism. */
230
3d2e82c5 231 if (!(redis_reply = redisCommandArgv(redis_handle, i, CCSS argv, NULL)))
2d8d625b
JH
232 {
233 *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr);
234 *defer_break = FALSE;
235 goto REDIS_EXIT;
236 }
237 }
238
239switch (redis_reply->type)
240 {
241 case REDIS_REPLY_ERROR:
242 *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str);
0d0ace19
GF
243
244 /* trap MOVED cluster responses and follow them */
3bbf85f2 245 if (Ustrncmp(redis_reply->str, "MOVED", 5) == 0)
0d0ace19
GF
246 {
247 DEBUG(D_lookup)
42c7f0b4 248 debug_printf_indent("REDIS: cluster redirect %s\n", redis_reply->str);
0d0ace19 249 /* follow redirect
e29b631d 250 This is cheating, we simply set defer_break = FALSE to move on to
0d0ace19 251 the next server in the redis_servers list */
e29b631d 252 *defer_break = FALSE;
0d0ace19
GF
253 return DEFER;
254 } else {
e29b631d 255 *defer_break = TRUE;
0d0ace19 256 }
2d8d625b
JH
257 *do_cache = 0;
258 goto REDIS_EXIT;
259 /* NOTREACHED */
260
261 case REDIS_REPLY_NIL:
262 DEBUG(D_lookup)
42c7f0b4 263 debug_printf_indent("REDIS: query was not one that returned any data\n");
acec9514 264 result = string_catn(result, US"", 1);
2d8d625b
JH
265 *do_cache = 0;
266 goto REDIS_EXIT;
267 /* NOTREACHED */
268
269 case REDIS_REPLY_INTEGER:
acec9514 270 result = string_cat(result, redis_reply->integer != 0 ? US"true" : US"false");
2d8d625b
JH
271 break;
272
273 case REDIS_REPLY_STRING:
274 case REDIS_REPLY_STATUS:
acec9514 275 result = string_catn(result, US redis_reply->str, redis_reply->len);
2d8d625b
JH
276 break;
277
278 case REDIS_REPLY_ARRAY:
279
280 /* NOTE: For now support 1 nested array result. If needed a limitless
281 result can be parsed */
282
d7978c0f 283 for (int i = 0; i < redis_reply->elements; i++)
2d8d625b
JH
284 {
285 entry = redis_reply->element[i];
286
287 if (result)
acec9514 288 result = string_catn(result, US"\n", 1);
2d8d625b
JH
289
290 switch (entry->type)
291 {
292 case REDIS_REPLY_INTEGER:
52f12a7c 293 result = string_fmt_append(result, "%d", entry->integer);
2d8d625b
JH
294 break;
295 case REDIS_REPLY_STRING:
acec9514 296 result = string_catn(result, US entry->str, entry->len);
2d8d625b
JH
297 break;
298 case REDIS_REPLY_ARRAY:
d7978c0f 299 for (int j = 0; j < entry->elements; j++)
2d8d625b
JH
300 {
301 tentry = entry->element[j];
302
303 if (result)
acec9514 304 result = string_catn(result, US"\n", 1);
2d8d625b
JH
305
306 switch (tentry->type)
307 {
308 case REDIS_REPLY_INTEGER:
52f12a7c 309 result = string_fmt_append(result, "%d", tentry->integer);
2d8d625b
JH
310 break;
311 case REDIS_REPLY_STRING:
acec9514 312 result = string_catn(result, US tentry->str, tentry->len);
2d8d625b
JH
313 break;
314 case REDIS_REPLY_ARRAY:
315 DEBUG(D_lookup)
42c7f0b4 316 debug_printf_indent("REDIS: result has nesting of arrays which"
2d8d625b
JH
317 " is not supported. Ignoring!\n");
318 break;
319 default:
42c7f0b4 320 DEBUG(D_lookup) debug_printf_indent(
2d8d625b
JH
321 "REDIS: result has unsupported type. Ignoring!\n");
322 break;
323 }
324 }
325 break;
326 default:
42c7f0b4 327 DEBUG(D_lookup) debug_printf_indent("REDIS: query returned unsupported type\n");
2d8d625b
JH
328 break;
329 }
330 }
331 break;
332 }
333
334
335if (result)
f3ebb786 336 gstring_release_unused(result);
2d8d625b
JH
337else
338 {
339 yield = FAIL;
340 *errmsg = US"REDIS: no data found";
341 }
342
343REDIS_EXIT:
344
345/* Free store for any result that was got; don't close the connection,
346as it is cached. */
347
348if (redis_reply) freeReplyObject(redis_reply);
349
4c04137d 350/* Non-NULL result indicates a successful result */
2d8d625b
JH
351
352if (result)
353 {
acec9514 354 *resultptr = string_from_gstring(result);
2d8d625b
JH
355 return OK;
356 }
357else
358 {
42c7f0b4 359 DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
2d8d625b
JH
360 /* NOTE: Required to close connection since it needs to be reopened */
361 return yield; /* FAIL or DEFER */
362 }
9bdd29ad
TL
363}
364
2d8d625b
JH
365
366
9bdd29ad
TL
367/*************************************************
368* Find entry point *
369*************************************************/
370/*
371 * See local README for interface description. The handle and filename
372 * arguments are not used. The code to loop through a list of servers while the
373 * query is deferred with a retryable error is now in a separate function that is
374 * shared with other noSQL lookups.
375 */
376
377static int
d447dbd1
JH
378redis_find(void * handle __attribute__((unused)),
379 const uschar * filename __attribute__((unused)),
380 const uschar * command, int length, uschar ** result, uschar ** errmsg,
67a57a5a 381 uint * do_cache, const uschar * opts)
9bdd29ad 382{
2d8d625b 383return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
0b4dfe7a 384 result, errmsg, do_cache, opts, perform_redis_search);
9bdd29ad
TL
385}
386
2d8d625b
JH
387
388
389/*************************************************
390* Quote entry point *
391*************************************************/
392
393/* Prefix any whitespace, or backslash, with a backslash.
394This is not a Redis thing but instead to let the argv splitting
395we do to split on whitespace, yet provide means for getting
396whitespace into an argument.
397
398Arguments:
399 s the string to be quoted
400 opt additional option text or NULL if none
401
402Returns: the processed string or NULL for a bad option
403*/
404
405static uschar *
406redis_quote(uschar *s, uschar *opt)
407{
408register int c;
409int count = 0;
410uschar *t = s;
411uschar *quoted;
412
413if (opt) return NULL; /* No options recognized */
414
415while ((c = *t++) != 0)
416 if (isspace(c) || c == '\\') count++;
417
418if (count == 0) return s;
f3ebb786 419t = quoted = store_get(Ustrlen(s) + count + 1, is_tainted(s));
2d8d625b
JH
420
421while ((c = *s++) != 0)
422 {
423 if (isspace(c) || c == '\\') *t++ = '\\';
424 *t++ = c;
425 }
426
427*t = 0;
428return quoted;
429}
430
431
9bdd29ad
TL
432/*************************************************
433* Version reporting entry point *
434*************************************************/
435#include "../version.h"
436
437void
438redis_version_report(FILE *f)
439{
2d8d625b 440fprintf(f, "Library version: REDIS: Compile: %d [%d]\n",
9bdd29ad
TL
441 HIREDIS_MAJOR, HIREDIS_MINOR);
442#ifdef DYNLOOKUP
2d8d625b 443fprintf(f, " Exim version %s\n", EXIM_VERSION_STR);
9bdd29ad
TL
444#endif
445}
446
2d8d625b
JH
447
448
9bdd29ad
TL
449/* These are the lookup_info blocks for this driver */
450static lookup_info redis_lookup_info = {
451 US"redis", /* lookup name */
452 lookup_querystyle, /* query-style lookup */
453 redis_open, /* open function */
454 NULL, /* no check function */
455 redis_find, /* find function */
456 NULL, /* no close function */
457 redis_tidy, /* tidy function */
2d8d625b 458 redis_quote, /* quoting function */
9bdd29ad
TL
459 redis_version_report /* version reporting */
460};
461
462#ifdef DYNLOOKUP
de78e2d5 463# define redis_lookup_module_info _lookup_module_info
9bdd29ad
TL
464#endif /* DYNLOOKUP */
465
466static lookup_info *_lookup_list[] = { &redis_lookup_info };
467lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
468
de78e2d5 469#endif /* LOOKUP_REDIS */
9bdd29ad 470/* End of lookups/redis.c */