Redis: fix server-specified-in-lookup
[exim.git] / src / src / lookups / redis.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2009 */
6 /* See the file NOTICE for conditions of use and distribution. */
7
8 #include "../exim.h"
9
10 #ifdef EXPERIMENTAL_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(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("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 ssize = 0;
84 int offset = 0;
85 int yield = DEFER;
86 int i, j;
87 uschar *result = NULL;
88 uschar *server_copy = NULL;
89 uschar *tmp, *ttmp;
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 (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 != NULL; 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("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   }
181 else
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 */
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("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_cat(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
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     *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, Ustrlen(ttmp));
260     break;
261
262   case REDIS_REPLY_STRING:
263   case REDIS_REPLY_STATUS:
264     result = string_cat(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_cat(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, Ustrlen(tmp));
285           break;
286         case REDIS_REPLY_STRING:
287           result = string_cat(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_cat(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,
303                                     US ttmp, Ustrlen(ttmp));
304                 break;
305               case REDIS_REPLY_STRING:
306                 result = string_cat(result, &ssize, &offset,
307                                     US tentry->str, tentry->len);
308                 break;
309               case REDIS_REPLY_ARRAY:
310                 DEBUG(D_lookup)
311                   debug_printf("REDIS: result has nesting of arrays which"
312                     " is not supported. Ignoring!\n");
313                 break;
314               default:
315                 DEBUG(D_lookup) debug_printf(
316                           "REDIS: result has unsupported type. Ignoring!\n");
317                 break;
318               }
319             }
320             break;
321           default:
322             DEBUG(D_lookup) debug_printf("REDIS: query returned unsupported type\n");
323             break;
324           }
325         }
326       break;
327   }
328
329
330 if (result)
331   {
332   result[offset] = 0;
333   store_reset(result + offset + 1);
334   }
335 else
336   {
337   yield = FAIL;
338   *errmsg = US"REDIS: no data found";
339   }
340
341 REDIS_EXIT:
342
343 /* Free store for any result that was got; don't close the connection,
344 as it is cached. */
345
346 if (redis_reply) freeReplyObject(redis_reply);
347
348 /* Non-NULL result indicates a sucessful result */
349
350 if (result)
351   {
352   *resultptr = result;
353   return OK;
354   }
355 else
356   {
357   DEBUG(D_lookup) debug_printf("%s\n", *errmsg);
358   /* NOTE: Required to close connection since it needs to be reopened */
359   return yield;      /* FAIL or DEFER */
360   }
361 }
362
363
364
365 /*************************************************
366 *               Find entry point                 *
367 *************************************************/
368 /*
369  * See local README for interface description. The handle and filename
370  * arguments are not used. The code to loop through a list of servers while the
371  * query is deferred with a retryable error is now in a separate function that is
372  * shared with other noSQL lookups.
373  */
374
375 static int
376 redis_find(void *handle __attribute__((unused)),
377   uschar *filename __attribute__((unused)),
378   const uschar *command, int length, uschar **result, uschar **errmsg,
379   uint *do_cache)
380 {
381 return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
382   result, errmsg, do_cache, perform_redis_search);
383 }
384
385
386
387 /*************************************************
388 *               Quote entry point                *
389 *************************************************/
390
391 /* Prefix any whitespace, or backslash, with a backslash.
392 This is not a Redis thing but instead to let the argv splitting
393 we do to split on whitespace, yet provide means for getting
394 whitespace into an argument.
395
396 Arguments:
397   s          the string to be quoted
398   opt        additional option text or NULL if none
399
400 Returns:     the processed string or NULL for a bad option
401 */
402
403 static uschar *
404 redis_quote(uschar *s, uschar *opt)
405 {
406 register int c;
407 int count = 0;
408 uschar *t = s;
409 uschar *quoted;
410
411 if (opt) return NULL;     /* No options recognized */
412
413 while ((c = *t++) != 0)
414   if (isspace(c) || c == '\\') count++;
415
416 if (count == 0) return s;
417 t = quoted = store_get(Ustrlen(s) + count + 1);
418
419 while ((c = *s++) != 0)
420   {
421   if (isspace(c) || c == '\\') *t++ = '\\';
422   *t++ = c;
423   }
424
425 *t = 0;
426 return quoted;
427 }
428
429
430 /*************************************************
431 *         Version reporting entry point          *
432 *************************************************/
433 #include "../version.h"
434
435 void
436 redis_version_report(FILE *f)
437 {
438 fprintf(f, "Library version: REDIS: Compile: %d [%d]\n",
439                HIREDIS_MAJOR, HIREDIS_MINOR);
440 #ifdef DYNLOOKUP
441 fprintf(f, "                        Exim version %s\n", EXIM_VERSION_STR);
442 #endif
443 }
444
445
446
447 /* These are the lookup_info blocks for this driver */
448 static lookup_info redis_lookup_info = {
449   US"redis",                     /* lookup name */
450   lookup_querystyle,             /* query-style lookup */
451   redis_open,                    /* open function */
452   NULL,                          /* no check function */
453   redis_find,                    /* find function */
454   NULL,                          /* no close function */
455   redis_tidy,                    /* tidy function */
456   redis_quote,                   /* quoting function */
457   redis_version_report           /* version reporting */
458 };
459
460 #ifdef DYNLOOKUP
461 #define redis_lookup_module_info _lookup_module_info
462 #endif /* DYNLOOKUP */
463
464 static lookup_info *_lookup_list[] = { &redis_lookup_info };
465 lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
466
467 #endif /* EXPERIMENTAL_REDIS */
468 /* End of lookups/redis.c */