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