Bugzilla 1217: Experimental Redis lookup
[users/jgh/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 /* Structure and anchor for caching connections. */
17 typedef struct redis_connection {
18        struct redis_connection *next;
19        uschar  *server;
20        redisContext    *handle;
21 } redis_connection;
22
23 static redis_connection *redis_connections = NULL;
24
25 static void *
26 redis_open(uschar *filename, uschar **errmsg)
27 {
28        return (void *)(1);
29 }
30
31 void
32 redis_tidy(void)
33 {
34        redis_connection *cn;
35
36        /*
37         * XXX: Not sure how often this is called!
38         * Guess its called after every lookup which probably would mean to just
39         * not use the _tidy() function at all and leave with exim exiting to
40         * GC connections!
41         */
42        while ((cn = redis_connections) != NULL) {
43                redis_connections = cn->next;
44                DEBUG(D_lookup) debug_printf("close REDIS connection: %s\n", cn->server);
45                redisFree(cn->handle);
46        }
47 }
48
49 /* This function is called from the find entry point to do the search for a
50  * single server.
51  *
52  *     Arguments:
53  *       query        the query string
54  *       server       the server string
55  *       resultptr    where to store the result
56  *       errmsg       where to point an error message
57  *       defer_break  TRUE if no more servers are to be tried after DEFER
58  *       do_cache     set false if data is changed
59  *
60  *     The server string is of the form "host/dbnumber/password". The host can be
61  *     host:port. This string is in a nextinlist temporary buffer, so can be
62  *     overwritten.
63  *
64  *     Returns:       OK, FAIL, or DEFER
65  */
66 static int
67 perform_redis_search(uschar *command, uschar *server, uschar **resultptr,
68   uschar **errmsg, BOOL *defer_break, BOOL *do_cache)
69 {
70        redisContext *redis_handle = NULL;        /* Keep compilers happy */
71        redisReply *redis_reply = NULL;
72        redisReply *entry = NULL;
73        redisReply *tentry = NULL;
74        redis_connection *cn;
75        int ssize = 0;
76        int offset = 0;
77        int yield = DEFER;
78        int i, j;
79        uschar *result = NULL;
80        uschar *server_copy = NULL;
81        uschar *tmp, *ttmp;
82        uschar *sdata[3];
83
84        /*
85         * Disaggregate the parameters from the server argument.
86         * The order is host:port(socket)
87         * We can write to the string, since it is in a nextinlist temporary buffer.
88         * This copy is also used for debugging output.
89         */
90         memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */;
91                 for (i = 2; i > 0; i--) {
92                         uschar *pp = Ustrrchr(server, '/');
93                         if (pp == NULL) {
94                                 *errmsg = string_sprintf("incomplete Redis server data: %s", (i == 2) ? server : server_copy);
95                                 *defer_break = TRUE;
96                                 return DEFER;
97                         }
98                         *pp++ = 0;
99                         sdata[i] = pp;
100                         if (i == 2) server_copy = string_copy(server);  /* sans password */
101                 }
102         sdata[0] = server;   /* What's left at the start */
103
104         /* If the database or password is an empty string, set it NULL */
105         if (sdata[1][0] == 0) sdata[1] = NULL;
106         if (sdata[2][0] == 0) sdata[2] = NULL;
107
108        /* See if we have a cached connection to the server */
109        for (cn = redis_connections; cn != NULL; cn = cn->next) {
110                if (Ustrcmp(cn->server, server_copy) == 0) {
111                        redis_handle = cn->handle;
112                        break;
113                }
114        }
115
116        if (cn == NULL) {
117                uschar *p;
118                uschar *socket = NULL;
119                int port = 0;
120                /* int redis_err = REDIS_OK; */
121
122                if ((p = Ustrchr(sdata[0], '(')) != NULL) {
123                        *p++ = 0;
124                        socket = p;
125                        while (*p != 0 && *p != ')')
126                                p++;
127                        *p = 0;
128                }
129
130                if ((p = Ustrchr(sdata[0], ':')) != NULL) {
131                        *p++ = 0;
132                        port = Uatoi(p);
133                } else {
134                        port = Uatoi("6379");
135                }
136
137                if (Ustrchr(server, '/') != NULL) {
138                        *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s", sdata[0]);
139                        *defer_break = TRUE;
140                        return DEFER;
141                }
142
143                DEBUG(D_lookup)
144                debug_printf("REDIS new connection: host=%s port=%d socket=%s database=%s\n", sdata[0], port, socket, sdata[1]);
145
146                /* Get store for a new handle, initialize it, and connect to the server */
147                /* XXX: Use timeouts ? */
148                if (socket != NULL)
149                        redis_handle = redisConnectUnix(CCS socket);
150                else
151                        redis_handle = redisConnect(CCS server, port);
152                if (redis_handle == NULL) {
153                        *errmsg = string_sprintf("REDIS connection failed");
154                        *defer_break = FALSE;
155                        goto REDIS_EXIT;
156                }
157
158                /* Add the connection to the cache */
159                cn = store_get(sizeof(redis_connection));
160                cn->server = server_copy;
161                cn->handle = redis_handle;
162                cn->next = redis_connections;
163                redis_connections = cn;
164        } else {
165                DEBUG(D_lookup)
166                debug_printf("REDIS using cached connection for %s\n", server_copy);
167        }
168
169        /* Authenticate if there is a password */
170        if(sdata[2] != NULL) {
171                if ((redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])) == NULL) {
172                        *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr);
173                        *defer_break = FALSE;
174                        goto REDIS_EXIT;
175                }
176        }
177
178        /* Select the database if there is a dbnumber passed */
179        if(sdata[1] != NULL) {
180                if ((redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])) == NULL) {
181                        *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr);
182                        *defer_break = FALSE;
183                        goto REDIS_EXIT;
184                } else {
185                        DEBUG(D_lookup) debug_printf("REDIS: Selecting database=%s\n", sdata[1]);
186                }
187        }
188
189        /* Run the command */
190        if ((redis_reply = redisCommand(redis_handle, CS command)) == NULL) {
191                *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr);
192                *defer_break = FALSE;
193                goto REDIS_EXIT;
194        }
195
196        switch (redis_reply->type) {
197        case REDIS_REPLY_ERROR:
198                *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str);
199                *defer_break = FALSE;
200                *do_cache = FALSE;
201                goto REDIS_EXIT;
202                /* NOTREACHED */
203
204                break;
205        case REDIS_REPLY_NIL:
206                DEBUG(D_lookup) debug_printf("REDIS: query was not one that returned any data\n");
207                result = string_sprintf("");
208                *do_cache = FALSE;
209                goto REDIS_EXIT;
210                /* NOTREACHED */
211
212                break;
213        case REDIS_REPLY_INTEGER:
214                ttmp = (redis_reply->integer == 1) ? US"true" : US"false";
215                result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp));
216                break;
217        case REDIS_REPLY_STRING:
218        case REDIS_REPLY_STATUS:
219                result = string_cat(result, &ssize, &offset, US redis_reply->str, redis_reply->len);
220                break;
221        case REDIS_REPLY_ARRAY:
222
223                /* NOTE: For now support 1 nested array result. If needed a limitless result can be parsed */
224                for (i = 0; i < redis_reply->elements; i++) {
225                        entry = redis_reply->element[i];
226
227                        if (result != NULL)
228                                result = string_cat(result, &ssize, &offset, US"\n", 1);
229
230                        switch (entry->type) {
231                        case REDIS_REPLY_INTEGER:
232                                tmp = string_sprintf("%d", entry->integer);
233                                result = string_cat(result, &ssize, &offset, US tmp, Ustrlen(tmp));
234                                break;
235                        case REDIS_REPLY_STRING:
236                                result = string_cat(result, &ssize, &offset, US entry->str, entry->len);
237                                break;
238                        case REDIS_REPLY_ARRAY:
239                                for (j = 0; j < entry->elements; j++) {
240                                        tentry = entry->element[j];
241
242                                        if (result != NULL)
243                                                result = string_cat(result, &ssize, &offset, US"\n", 1);
244
245                                        switch (tentry->type) {
246                                        case REDIS_REPLY_INTEGER:
247                                                ttmp = string_sprintf("%d", tentry->integer);
248                                                result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp));
249                                                break;
250                                        case REDIS_REPLY_STRING:
251                                                result = string_cat(result, &ssize, &offset, US tentry->str, tentry->len);
252                                                break;
253                                        case REDIS_REPLY_ARRAY:
254                                                DEBUG(D_lookup) debug_printf("REDIS: result has nesting of arrays which is not supported. Ignoring!\n");
255                                                break;
256                                        default:
257                                                DEBUG(D_lookup) debug_printf("REDIS: result has unsupported type. Ignoring!\n");
258                                                break;
259                                        }
260                                }
261                                break;
262                        default:
263                                DEBUG(D_lookup) debug_printf("REDIS: query returned unsupported type\n");
264                                break;
265                        }
266                }
267                break;
268        }
269
270
271        if (result == NULL) {
272                yield = FAIL;
273                *errmsg = US"REDIS: no data found";
274        } else {
275                result[offset] = 0;
276                store_reset(result + offset + 1);
277        }
278
279     REDIS_EXIT:
280        /* Free store for any result that was got; don't close the connection, as it is cached. */
281        if (redis_reply != NULL)
282                freeReplyObject(redis_reply);
283
284        /* Non-NULL result indicates a sucessful result */
285        if (result != NULL) {
286                *resultptr = result;
287                return OK;
288        } else {
289                DEBUG(D_lookup) debug_printf("%s\n", *errmsg);
290                /* NOTE: Required to close connection since it needs to be reopened */
291                return yield;      /* FAIL or DEFER */
292        }
293 }
294
295 /*************************************************
296 *               Find entry point                 *
297 *************************************************/
298 /*
299  * See local README for interface description. The handle and filename
300  * arguments are not used. The code to loop through a list of servers while the
301  * query is deferred with a retryable error is now in a separate function that is
302  * shared with other noSQL lookups.
303  */
304
305 static int
306 redis_find(void *handle __attribute__((unused)), uschar *filename __attribute__((unused)),
307            uschar *command, int length, uschar **result, uschar **errmsg, BOOL *do_cache)
308 {
309        return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
310          result, errmsg, do_cache, perform_redis_search);
311 }
312
313 /*************************************************
314 *         Version reporting entry point          *
315 *************************************************/
316 #include "../version.h"
317
318 void
319 redis_version_report(FILE *f)
320 {
321        fprintf(f, "Library version: REDIS: Compile: %d [%d]\n",
322                HIREDIS_MAJOR, HIREDIS_MINOR);
323 #ifdef DYNLOOKUP
324        fprintf(f, "                        Exim version %s\n", EXIM_VERSION_STR);
325 #endif
326 }
327
328 /* These are the lookup_info blocks for this driver */
329 static lookup_info redis_lookup_info = {
330   US"redis",                     /* lookup name */
331   lookup_querystyle,             /* query-style lookup */
332   redis_open,                    /* open function */
333   NULL,                          /* no check function */
334   redis_find,                    /* find function */
335   NULL,                          /* no close function */
336   redis_tidy,                    /* tidy function */
337   NULL,                                /* quoting function */
338   redis_version_report           /* version reporting */
339 };
340
341 #ifdef DYNLOOKUP
342 #define redis_lookup_module_info _lookup_module_info
343 #endif /* DYNLOOKUP */
344
345 static lookup_info *_lookup_list[] = { &redis_lookup_info };
346 lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
347
348 #endif /* EXPERIMENTAL_REDIS */
349 /* End of lookups/redis.c */