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