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