I18N: Move EAI support from Experimental to mainline
[exim.git] / src / src / lookups / redis.c
CommitLineData
9bdd29ad
TL
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. */
17typedef struct redis_connection {
18 struct redis_connection *next;
19 uschar *server;
20 redisContext *handle;
21} redis_connection;
22
23static redis_connection *redis_connections = NULL;
24
25static void *
26redis_open(uschar *filename, uschar **errmsg)
27{
28 return (void *)(1);
29}
30
31void
32redis_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 */
66static int
67perform_redis_search(uschar *command, uschar *server, uschar **resultptr,
14b3c5bc 68 uschar **errmsg, BOOL *defer_break, uint *do_cache)
9bdd29ad
TL
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;
14b3c5bc 200 *do_cache = 0;
9bdd29ad
TL
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("");
14b3c5bc 208 *do_cache = 0;
9bdd29ad
TL
209 goto REDIS_EXIT;
210 /* NOTREACHED */
211
212 break;
213 case REDIS_REPLY_INTEGER:
972af88e 214 ttmp = (redis_reply->integer != 0) ? US"true" : US"false";
9bdd29ad
TL
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
305static int
306redis_find(void *handle __attribute__((unused)), uschar *filename __attribute__((unused)),
14b3c5bc 307 uschar *command, int length, uschar **result, uschar **errmsg, uint *do_cache)
9bdd29ad
TL
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
318void
319redis_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 */
329static 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
345static lookup_info *_lookup_list[] = { &redis_lookup_info };
346lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
347
348#endif /* EXPERIMENTAL_REDIS */
349/* End of lookups/redis.c */