Testsuite: make 4560 more testhost-name independent
[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
TL
30static void *
31redis_open(uschar *filename, uschar **errmsg)
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;
50 DEBUG(D_lookup) debug_printf("close REDIS connection: %s\n", cn->server);
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
66
67 The server string is of the form "host/dbnumber/password". The host can be
68 host:port. This string is in a nextinlist temporary buffer, so can be
69 overwritten.
70
71 Returns: OK, FAIL, or DEFER
72*/
73
9bdd29ad 74static int
2d8d625b 75perform_redis_search(const uschar *command, uschar *server, uschar **resultptr,
14b3c5bc 76 uschar **errmsg, BOOL *defer_break, uint *do_cache)
9bdd29ad 77{
2d8d625b
JH
78redisContext *redis_handle = NULL; /* Keep compilers happy */
79redisReply *redis_reply = NULL;
80redisReply *entry = NULL;
81redisReply *tentry = NULL;
82redis_connection *cn;
2d8d625b
JH
83int yield = DEFER;
84int i, j;
acec9514 85gstring * result = NULL;
2d8d625b 86uschar *server_copy = NULL;
2d8d625b
JH
87uschar *sdata[3];
88
89/* Disaggregate the parameters from the server argument.
90The order is host:port(socket)
91We can write to the string, since it is in a nextinlist temporary buffer.
92This copy is also used for debugging output. */
93
94memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */;
95for (i = 2; i > 0; i--)
96 {
97 uschar *pp = Ustrrchr(server, '/');
98
99 if (!pp)
100 {
101 *errmsg = string_sprintf("incomplete Redis server data: %s",
102 i == 2 ? server : server_copy);
103 *defer_break = TRUE;
104 return DEFER;
105 }
106 *pp++ = 0;
107 sdata[i] = pp;
108 if (i == 2) server_copy = string_copy(server); /* sans password */
109 }
110sdata[0] = server; /* What's left at the start */
111
112/* If the database or password is an empty string, set it NULL */
113if (sdata[1][0] == 0) sdata[1] = NULL;
114if (sdata[2][0] == 0) sdata[2] = NULL;
115
116/* See if we have a cached connection to the server */
117
de78e2d5 118for (cn = redis_connections; cn; cn = cn->next)
2d8d625b
JH
119 if (Ustrcmp(cn->server, server_copy) == 0)
120 {
121 redis_handle = cn->handle;
122 break;
123 }
124
125if (!cn)
126 {
127 uschar *p;
128 uschar *socket = NULL;
129 int port = 0;
130 /* int redis_err = REDIS_OK; */
131
132 if ((p = Ustrchr(sdata[0], '(')))
133 {
134 *p++ = 0;
135 socket = p;
136 while (*p != 0 && *p != ')') p++;
137 *p = 0;
138 }
139
140 if ((p = Ustrchr(sdata[0], ':')))
141 {
142 *p++ = 0;
143 port = Uatoi(p);
144 }
145 else
146 port = Uatoi("6379");
147
148 if (Ustrchr(server, '/'))
149 {
150 *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s",
151 sdata[0]);
152 *defer_break = TRUE;
153 return DEFER;
154 }
155
156 DEBUG(D_lookup)
157 debug_printf("REDIS new connection: host=%s port=%d socket=%s database=%s\n",
158 sdata[0], port, socket, sdata[1]);
159
160 /* Get store for a new handle, initialize it, and connect to the server */
161 /* XXX: Use timeouts ? */
162 redis_handle =
163 socket ? redisConnectUnix(CCS socket) : redisConnect(CCS server, port);
164 if (!redis_handle)
165 {
166 *errmsg = string_sprintf("REDIS connection failed");
167 *defer_break = FALSE;
168 goto REDIS_EXIT;
169 }
170
171 /* Add the connection to the cache */
172 cn = store_get(sizeof(redis_connection));
173 cn->server = server_copy;
174 cn->handle = redis_handle;
175 cn->next = redis_connections;
176 redis_connections = cn;
177 }
178else
179 {
180 DEBUG(D_lookup)
181 debug_printf("REDIS using cached connection for %s\n", server_copy);
182}
183
184/* Authenticate if there is a password */
185if(sdata[2])
186 if (!(redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])))
187 {
188 *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr);
189 *defer_break = FALSE;
190 goto REDIS_EXIT;
191 }
192
193/* Select the database if there is a dbnumber passed */
194if(sdata[1])
195 {
196 if (!(redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])))
197 {
198 *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr);
199 *defer_break = FALSE;
200 goto REDIS_EXIT;
201 }
202 DEBUG(D_lookup) debug_printf("REDIS: Selecting database=%s\n", sdata[1]);
203 }
204
205/* split string on whitespace into argv */
206 {
207 uschar * argv[32];
208 int i;
42653575 209 const uschar * s = command;
2d8d625b
JH
210 int siz, ptr;
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
2d8d625b
JH
224 DEBUG(D_lookup) debug_printf("REDIS: argv[%d] '%s'\n", i, argv[i]);
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
231 redis_reply = redisCommandArgv(redis_handle, i, (const char **) argv, NULL);
232 if (!redis_reply)
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);
244 *defer_break = FALSE;
245 *do_cache = 0;
246 goto REDIS_EXIT;
247 /* NOTREACHED */
248
249 case REDIS_REPLY_NIL:
250 DEBUG(D_lookup)
251 debug_printf("REDIS: query was not one that returned any data\n");
acec9514 252 result = string_catn(result, US"", 1);
2d8d625b
JH
253 *do_cache = 0;
254 goto REDIS_EXIT;
255 /* NOTREACHED */
256
257 case REDIS_REPLY_INTEGER:
acec9514 258 result = string_cat(result, redis_reply->integer != 0 ? US"true" : US"false");
2d8d625b
JH
259 break;
260
261 case REDIS_REPLY_STRING:
262 case REDIS_REPLY_STATUS:
acec9514 263 result = string_catn(result, US redis_reply->str, redis_reply->len);
2d8d625b
JH
264 break;
265
266 case REDIS_REPLY_ARRAY:
267
268 /* NOTE: For now support 1 nested array result. If needed a limitless
269 result can be parsed */
270
271 for (i = 0; i < redis_reply->elements; i++)
272 {
273 entry = redis_reply->element[i];
274
275 if (result)
acec9514 276 result = string_catn(result, US"\n", 1);
2d8d625b
JH
277
278 switch (entry->type)
279 {
280 case REDIS_REPLY_INTEGER:
acec9514 281 result = string_cat(result, string_sprintf("%d", entry->integer));
2d8d625b
JH
282 break;
283 case REDIS_REPLY_STRING:
acec9514 284 result = string_catn(result, US entry->str, entry->len);
2d8d625b
JH
285 break;
286 case REDIS_REPLY_ARRAY:
287 for (j = 0; j < entry->elements; j++)
288 {
289 tentry = entry->element[j];
290
291 if (result)
acec9514 292 result = string_catn(result, US"\n", 1);
2d8d625b
JH
293
294 switch (tentry->type)
295 {
296 case REDIS_REPLY_INTEGER:
acec9514 297 result = string_cat(result, string_sprintf("%d", tentry->integer));
2d8d625b
JH
298 break;
299 case REDIS_REPLY_STRING:
acec9514 300 result = string_catn(result, US tentry->str, tentry->len);
2d8d625b
JH
301 break;
302 case REDIS_REPLY_ARRAY:
303 DEBUG(D_lookup)
304 debug_printf("REDIS: result has nesting of arrays which"
305 " is not supported. Ignoring!\n");
306 break;
307 default:
308 DEBUG(D_lookup) debug_printf(
309 "REDIS: result has unsupported type. Ignoring!\n");
310 break;
311 }
312 }
313 break;
314 default:
315 DEBUG(D_lookup) debug_printf("REDIS: query returned unsupported type\n");
316 break;
317 }
318 }
319 break;
320 }
321
322
323if (result)
acec9514 324 store_reset(result->s + result->ptr + 1);
2d8d625b
JH
325else
326 {
327 yield = FAIL;
328 *errmsg = US"REDIS: no data found";
329 }
330
331REDIS_EXIT:
332
333/* Free store for any result that was got; don't close the connection,
334as it is cached. */
335
336if (redis_reply) freeReplyObject(redis_reply);
337
4c04137d 338/* Non-NULL result indicates a successful result */
2d8d625b
JH
339
340if (result)
341 {
acec9514 342 *resultptr = string_from_gstring(result);
2d8d625b
JH
343 return OK;
344 }
345else
346 {
347 DEBUG(D_lookup) debug_printf("%s\n", *errmsg);
348 /* NOTE: Required to close connection since it needs to be reopened */
349 return yield; /* FAIL or DEFER */
350 }
9bdd29ad
TL
351}
352
2d8d625b
JH
353
354
9bdd29ad
TL
355/*************************************************
356* Find entry point *
357*************************************************/
358/*
359 * See local README for interface description. The handle and filename
360 * arguments are not used. The code to loop through a list of servers while the
361 * query is deferred with a retryable error is now in a separate function that is
362 * shared with other noSQL lookups.
363 */
364
365static int
2d8d625b
JH
366redis_find(void *handle __attribute__((unused)),
367 uschar *filename __attribute__((unused)),
368 const uschar *command, int length, uschar **result, uschar **errmsg,
369 uint *do_cache)
9bdd29ad 370{
2d8d625b
JH
371return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
372 result, errmsg, do_cache, perform_redis_search);
9bdd29ad
TL
373}
374
2d8d625b
JH
375
376
377/*************************************************
378* Quote entry point *
379*************************************************/
380
381/* Prefix any whitespace, or backslash, with a backslash.
382This is not a Redis thing but instead to let the argv splitting
383we do to split on whitespace, yet provide means for getting
384whitespace into an argument.
385
386Arguments:
387 s the string to be quoted
388 opt additional option text or NULL if none
389
390Returns: the processed string or NULL for a bad option
391*/
392
393static uschar *
394redis_quote(uschar *s, uschar *opt)
395{
396register int c;
397int count = 0;
398uschar *t = s;
399uschar *quoted;
400
401if (opt) return NULL; /* No options recognized */
402
403while ((c = *t++) != 0)
404 if (isspace(c) || c == '\\') count++;
405
406if (count == 0) return s;
407t = quoted = store_get(Ustrlen(s) + count + 1);
408
409while ((c = *s++) != 0)
410 {
411 if (isspace(c) || c == '\\') *t++ = '\\';
412 *t++ = c;
413 }
414
415*t = 0;
416return quoted;
417}
418
419
9bdd29ad
TL
420/*************************************************
421* Version reporting entry point *
422*************************************************/
423#include "../version.h"
424
425void
426redis_version_report(FILE *f)
427{
2d8d625b 428fprintf(f, "Library version: REDIS: Compile: %d [%d]\n",
9bdd29ad
TL
429 HIREDIS_MAJOR, HIREDIS_MINOR);
430#ifdef DYNLOOKUP
2d8d625b 431fprintf(f, " Exim version %s\n", EXIM_VERSION_STR);
9bdd29ad
TL
432#endif
433}
434
2d8d625b
JH
435
436
9bdd29ad
TL
437/* These are the lookup_info blocks for this driver */
438static lookup_info redis_lookup_info = {
439 US"redis", /* lookup name */
440 lookup_querystyle, /* query-style lookup */
441 redis_open, /* open function */
442 NULL, /* no check function */
443 redis_find, /* find function */
444 NULL, /* no close function */
445 redis_tidy, /* tidy function */
2d8d625b 446 redis_quote, /* quoting function */
9bdd29ad
TL
447 redis_version_report /* version reporting */
448};
449
450#ifdef DYNLOOKUP
de78e2d5 451# define redis_lookup_module_info _lookup_module_info
9bdd29ad
TL
452#endif /* DYNLOOKUP */
453
454static lookup_info *_lookup_list[] = { &redis_lookup_info };
455lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
456
de78e2d5 457#endif /* LOOKUP_REDIS */
9bdd29ad 458/* End of lookups/redis.c */