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