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