Commit | Line | Data |
---|---|---|
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. */ | |
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: | |
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 | ||
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 */ |