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