1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) The Exim Maintainers 2020 - 2024 */
6 /* Copyright (c) University of Cambridge 1995 - 2018 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 /* SPDX-License-Identifier: GPL-2.0-or-later */
14 #include "lf_functions.h"
16 #include <hiredis/hiredis.h>
19 # define nele(arr) (sizeof(arr) / sizeof(*arr))
22 /* Structure and anchor for caching connections. */
23 typedef struct redis_connection {
24 struct redis_connection *next;
29 static redis_connection *redis_connections = NULL;
33 redis_open(const uschar * filename, uschar ** errmsg)
44 /* XXX: Not sure how often this is called!
45 Guess its called after every lookup which probably would mean to just
46 not use the _tidy() function at all and leave with exim exiting to
49 while ((cn = redis_connections))
51 redis_connections = cn->next;
52 DEBUG(D_lookup) debug_printf_indent("close REDIS connection: %s\n", cn->server);
53 redisFree(cn->handle);
58 /* This function is called from the find entry point to do the search for a
62 query the query string
63 server the server string
64 resultptr where to store the result
65 errmsg where to point an error message
66 defer_break TRUE if no more servers are to be tried after DEFER
67 do_cache set false if data is changed
70 The server string is of the form "host/dbnumber/password". The host can be
71 host:port. This string is in a nextinlist temporary buffer, so can be
74 Returns: OK, FAIL, or DEFER
78 perform_redis_search(const uschar *command, uschar *server, uschar **resultptr,
79 uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
81 redisContext *redis_handle = NULL; /* Keep compilers happy */
82 redisReply *redis_reply = NULL;
83 redisReply *entry = NULL;
84 redisReply *tentry = NULL;
88 gstring * result = NULL;
89 uschar *server_copy = NULL;
92 /* Disaggregate the parameters from the server argument.
93 The order is host:port(socket)
94 We can write to the string, since it is in a nextinlist temporary buffer.
95 This copy is also used for debugging output. */
97 memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */;
98 for (int i = 2; i > 0; i--)
100 uschar *pp = Ustrrchr(server, '/');
104 *errmsg = string_sprintf("incomplete Redis server data: %s",
105 i == 2 ? server : server_copy);
111 if (i == 2) server_copy = string_copy(server); /* sans password */
113 sdata[0] = server; /* What's left at the start */
115 /* If the database or password is an empty string, set it NULL */
116 if (sdata[1][0] == 0) sdata[1] = NULL;
117 if (sdata[2][0] == 0) sdata[2] = NULL;
119 /* See if we have a cached connection to the server */
121 for (cn = redis_connections; cn; cn = cn->next)
122 if (Ustrcmp(cn->server, server_copy) == 0)
124 redis_handle = cn->handle;
131 uschar *socket = NULL;
133 /* int redis_err = REDIS_OK; */
135 if ((p = Ustrchr(sdata[0], '(')))
139 while (*p != 0 && *p != ')') p++;
143 if ((p = Ustrchr(sdata[0], ':')))
149 port = Uatoi("6379");
151 if (Ustrchr(server, '/'))
153 *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s",
160 debug_printf_indent("REDIS new connection: host=%s port=%d socket=%s database=%s\n",
161 sdata[0], port, socket, sdata[1]);
163 /* Get store for a new handle, initialize it, and connect to the server */
164 /* XXX: Use timeouts ? */
166 socket ? redisConnectUnix(CCS socket) : redisConnect(CCS server, port);
169 *errmsg = US"REDIS connection failed";
170 *defer_break = FALSE;
174 /* Add the connection to the cache */
175 cn = store_get(sizeof(redis_connection), GET_UNTAINTED);
176 cn->server = server_copy;
177 cn->handle = redis_handle;
178 cn->next = redis_connections;
179 redis_connections = cn;
184 debug_printf_indent("REDIS using cached connection for %s\n", server_copy);
187 /* Authenticate if there is a password */
189 if (!(redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])))
191 *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr);
192 *defer_break = FALSE;
196 /* Select the database if there is a dbnumber passed */
199 if (!(redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])))
201 *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr);
202 *defer_break = FALSE;
205 DEBUG(D_lookup) debug_printf_indent("REDIS: Selecting database=%s\n", sdata[1]);
208 /* split string on whitespace into argv */
211 const uschar * s = command;
215 Uskip_whitespace(&s);
217 for (i = 0; *s && i < nele(argv); i++)
221 for (g = NULL; (c = *s) && !isspace(c); s++)
222 if (c != '\\' || *++s) /* backslash protects next char */
223 g = string_catn(g, s, 1);
224 argv[i] = string_from_gstring(g);
226 DEBUG(D_lookup) debug_printf_indent("REDIS: argv[%d] '%s'\n", i, argv[i]);
227 Uskip_whitespace(&s);
230 /* Run the command. We use the argv form rather than plain as that parses
231 into args by whitespace yet has no escaping mechanism. */
233 if (!(redis_reply = redisCommandArgv(redis_handle, i, CCSS argv, NULL)))
235 *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr);
236 *defer_break = FALSE;
241 switch (redis_reply->type)
243 case REDIS_REPLY_ERROR:
244 *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str);
246 /* trap MOVED cluster responses and follow them */
247 if (Ustrncmp(redis_reply->str, "MOVED", 5) == 0)
250 debug_printf_indent("REDIS: cluster redirect %s\n", redis_reply->str);
252 This is cheating, we simply set defer_break = FALSE to move on to
253 the next server in the redis_servers list */
254 *defer_break = FALSE;
263 case REDIS_REPLY_NIL:
265 debug_printf_indent("REDIS: query was not one that returned any data\n");
266 result = string_catn(result, US"", 1);
271 case REDIS_REPLY_INTEGER:
272 result = string_cat(result, redis_reply->integer != 0 ? US"true" : US"false");
275 case REDIS_REPLY_STRING:
276 case REDIS_REPLY_STATUS:
277 result = string_catn(result, US redis_reply->str, redis_reply->len);
280 case REDIS_REPLY_ARRAY:
282 /* NOTE: For now support 1 nested array result. If needed a limitless
283 result can be parsed */
285 for (int i = 0; i < redis_reply->elements; i++)
287 entry = redis_reply->element[i];
290 result = string_catn(result, US"\n", 1);
294 case REDIS_REPLY_INTEGER:
295 result = string_fmt_append(result, "%d", entry->integer);
297 case REDIS_REPLY_STRING:
298 result = string_catn(result, US entry->str, entry->len);
300 case REDIS_REPLY_ARRAY:
301 for (int j = 0; j < entry->elements; j++)
303 tentry = entry->element[j];
306 result = string_catn(result, US"\n", 1);
308 switch (tentry->type)
310 case REDIS_REPLY_INTEGER:
311 result = string_fmt_append(result, "%d", tentry->integer);
313 case REDIS_REPLY_STRING:
314 result = string_catn(result, US tentry->str, tentry->len);
316 case REDIS_REPLY_ARRAY:
318 debug_printf_indent("REDIS: result has nesting of arrays which"
319 " is not supported. Ignoring!\n");
322 DEBUG(D_lookup) debug_printf_indent(
323 "REDIS: result has unsupported type. Ignoring!\n");
329 DEBUG(D_lookup) debug_printf_indent("REDIS: query returned unsupported type\n");
338 gstring_release_unused(result);
342 *errmsg = US"REDIS: no data found";
347 /* Free store for any result that was got; don't close the connection,
350 if (redis_reply) freeReplyObject(redis_reply);
352 /* Non-NULL result indicates a successful result */
356 *resultptr = string_from_gstring(result);
361 DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
362 /* NOTE: Required to close connection since it needs to be reopened */
363 return yield; /* FAIL or DEFER */
369 /*************************************************
371 *************************************************/
373 * See local README for interface description. The handle and filename
374 * arguments are not used. The code to loop through a list of servers while the
375 * query is deferred with a retryable error is now in a separate function that is
376 * shared with other noSQL lookups.
380 redis_find(void * handle __attribute__((unused)),
381 const uschar * filename __attribute__((unused)),
382 const uschar * command, int length, uschar ** result, uschar ** errmsg,
383 uint * do_cache, const uschar * opts)
385 return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
386 result, errmsg, do_cache, opts, perform_redis_search);
391 /*************************************************
392 * Quote entry point *
393 *************************************************/
395 /* Prefix any whitespace, or backslash, with a backslash.
396 This is not a Redis thing but instead to let the argv splitting
397 we do to split on whitespace, yet provide means for getting
398 whitespace into an argument.
401 s the string to be quoted
402 opt additional option text or NULL if none
403 idx lookup type index
405 Returns: the processed string or NULL for a bad option
409 redis_quote(uschar * s, uschar * opt, unsigned idx)
412 uschar * t = s, * quoted;
414 if (opt) return NULL; /* No options recognized */
417 if (isspace(c) || c == '\\') count++;
419 t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
423 if (isspace(c) || c == '\\') *t++ = '\\';
432 /*************************************************
433 * Version reporting entry point *
434 *************************************************/
435 #include "../version.h"
438 redis_version_report(gstring * g)
440 g = string_fmt_append(g,
441 "Library version: REDIS: Compile: %d [%d]\n", HIREDIS_MAJOR, HIREDIS_MINOR);
443 g = string_fmt_append(g,
444 " Exim version %s\n", EXIM_VERSION_STR);
451 /* These are the lookup_info blocks for this driver */
452 static lookup_info redis_lookup_info = {
453 .name = US"redis", /* lookup name */
454 .type = lookup_querystyle, /* query-style lookup */
455 .open = redis_open, /* open function */
456 .check = NULL, /* no check function */
457 .find = redis_find, /* find function */
458 .close = NULL, /* no close function */
459 .tidy = redis_tidy, /* tidy function */
460 .quote = redis_quote, /* quoting function */
461 .version_report = redis_version_report /* version reporting */
465 # define redis_lookup_module_info _lookup_module_info
466 #endif /* DYNLOOKUP */
468 static lookup_info *_lookup_list[] = { &redis_lookup_info };
469 lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
471 #endif /* LOOKUP_REDIS */
472 /* End of lookups/redis.c */