1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainers 2020 */
7 /* See the file NOTICE for conditions of use and distribution. */
13 #include "lf_functions.h"
15 #include <hiredis/hiredis.h>
18 # define nele(arr) (sizeof(arr) / sizeof(*arr))
21 /* Structure and anchor for caching connections. */
22 typedef struct redis_connection
{
23 struct redis_connection
*next
;
28 static redis_connection
*redis_connections
= NULL
;
32 redis_open(const uschar
* filename
, uschar
** errmsg
)
43 /* XXX: Not sure how often this is called!
44 Guess its called after every lookup which probably would mean to just
45 not use the _tidy() function at all and leave with exim exiting to
48 while ((cn
= redis_connections
))
50 redis_connections
= cn
->next
;
51 DEBUG(D_lookup
) debug_printf_indent("close REDIS connection: %s\n", cn
->server
);
52 redisFree(cn
->handle
);
57 /* This function is called from the find entry point to do the search for a
61 query the query string
62 server the server string
63 resultptr where to store the result
64 errmsg where to point an error message
65 defer_break TRUE if no more servers are to be tried after DEFER
66 do_cache set false if data is changed
69 The server string is of the form "host/dbnumber/password". The host can be
70 host:port. This string is in a nextinlist temporary buffer, so can be
73 Returns: OK, FAIL, or DEFER
77 perform_redis_search(const uschar
*command
, uschar
*server
, uschar
**resultptr
,
78 uschar
**errmsg
, BOOL
*defer_break
, uint
*do_cache
, const uschar
* opts
)
80 redisContext
*redis_handle
= NULL
; /* Keep compilers happy */
81 redisReply
*redis_reply
= NULL
;
82 redisReply
*entry
= NULL
;
83 redisReply
*tentry
= NULL
;
87 gstring
* result
= NULL
;
88 uschar
*server_copy
= NULL
;
91 /* Disaggregate the parameters from the server argument.
92 The order is host:port(socket)
93 We can write to the string, since it is in a nextinlist temporary buffer.
94 This copy is also used for debugging output. */
96 memset(sdata
, 0, sizeof(sdata
)) /* Set all to NULL */;
97 for (int i
= 2; i
> 0; i
--)
99 uschar
*pp
= Ustrrchr(server
, '/');
103 *errmsg
= string_sprintf("incomplete Redis server data: %s",
104 i
== 2 ? server
: server_copy
);
110 if (i
== 2) server_copy
= string_copy(server
); /* sans password */
112 sdata
[0] = server
; /* What's left at the start */
114 /* If the database or password is an empty string, set it NULL */
115 if (sdata
[1][0] == 0) sdata
[1] = NULL
;
116 if (sdata
[2][0] == 0) sdata
[2] = NULL
;
118 /* See if we have a cached connection to the server */
120 for (cn
= redis_connections
; cn
; cn
= cn
->next
)
121 if (Ustrcmp(cn
->server
, server_copy
) == 0)
123 redis_handle
= cn
->handle
;
130 uschar
*socket
= NULL
;
132 /* int redis_err = REDIS_OK; */
134 if ((p
= Ustrchr(sdata
[0], '(')))
138 while (*p
!= 0 && *p
!= ')') p
++;
142 if ((p
= Ustrchr(sdata
[0], ':')))
148 port
= Uatoi("6379");
150 if (Ustrchr(server
, '/'))
152 *errmsg
= string_sprintf("unexpected slash in Redis server hostname: %s",
159 debug_printf_indent("REDIS new connection: host=%s port=%d socket=%s database=%s\n",
160 sdata
[0], port
, socket
, sdata
[1]);
162 /* Get store for a new handle, initialize it, and connect to the server */
163 /* XXX: Use timeouts ? */
165 socket
? redisConnectUnix(CCS socket
) : redisConnect(CCS server
, port
);
168 *errmsg
= US
"REDIS connection failed";
169 *defer_break
= FALSE
;
173 /* Add the connection to the cache */
174 cn
= store_get(sizeof(redis_connection
), FALSE
);
175 cn
->server
= server_copy
;
176 cn
->handle
= redis_handle
;
177 cn
->next
= redis_connections
;
178 redis_connections
= cn
;
183 debug_printf_indent("REDIS using cached connection for %s\n", server_copy
);
186 /* Authenticate if there is a password */
188 if (!(redis_reply
= redisCommand(redis_handle
, "AUTH %s", sdata
[2])))
190 *errmsg
= string_sprintf("REDIS Authentication failed: %s\n", redis_handle
->errstr
);
191 *defer_break
= FALSE
;
195 /* Select the database if there is a dbnumber passed */
198 if (!(redis_reply
= redisCommand(redis_handle
, "SELECT %s", sdata
[1])))
200 *errmsg
= string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata
[1], redis_handle
->errstr
);
201 *defer_break
= FALSE
;
204 DEBUG(D_lookup
) debug_printf_indent("REDIS: Selecting database=%s\n", sdata
[1]);
207 /* split string on whitespace into argv */
210 const uschar
* s
= command
;
214 while (isspace(*s
)) s
++;
216 for (i
= 0; *s
&& i
< nele(argv
); i
++)
220 for (g
= NULL
; (c
= *s
) && !isspace(c
); s
++)
221 if (c
!= '\\' || *++s
) /* backslash protects next char */
222 g
= string_catn(g
, s
, 1);
223 argv
[i
] = string_from_gstring(g
);
225 DEBUG(D_lookup
) debug_printf_indent("REDIS: argv[%d] '%s'\n", i
, argv
[i
]);
226 while (isspace(*s
)) s
++;
229 /* Run the command. We use the argv form rather than plain as that parses
230 into args by whitespace yet has no escaping mechanism. */
232 if (!(redis_reply
= redisCommandArgv(redis_handle
, i
, CCSS argv
, NULL
)))
234 *errmsg
= string_sprintf("REDIS: query failed: %s\n", redis_handle
->errstr
);
235 *defer_break
= FALSE
;
240 switch (redis_reply
->type
)
242 case REDIS_REPLY_ERROR
:
243 *errmsg
= string_sprintf("REDIS: lookup result failed: %s\n", redis_reply
->str
);
245 /* trap MOVED cluster responses and follow them */
246 if (Ustrncmp(redis_reply
->str
, "MOVED", 5) == 0)
249 debug_printf_indent("REDIS: cluster redirect %s\n", redis_reply
->str
);
251 This is cheating, we simply set defer_break = FALSE to move on to
252 the next server in the redis_servers list */
253 *defer_break
= FALSE
;
262 case REDIS_REPLY_NIL
:
264 debug_printf_indent("REDIS: query was not one that returned any data\n");
265 result
= string_catn(result
, US
"", 1);
270 case REDIS_REPLY_INTEGER
:
271 result
= string_cat(result
, redis_reply
->integer
!= 0 ? US
"true" : US
"false");
274 case REDIS_REPLY_STRING
:
275 case REDIS_REPLY_STATUS
:
276 result
= string_catn(result
, US redis_reply
->str
, redis_reply
->len
);
279 case REDIS_REPLY_ARRAY
:
281 /* NOTE: For now support 1 nested array result. If needed a limitless
282 result can be parsed */
284 for (int i
= 0; i
< redis_reply
->elements
; i
++)
286 entry
= redis_reply
->element
[i
];
289 result
= string_catn(result
, US
"\n", 1);
293 case REDIS_REPLY_INTEGER
:
294 result
= string_fmt_append(result
, "%d", entry
->integer
);
296 case REDIS_REPLY_STRING
:
297 result
= string_catn(result
, US entry
->str
, entry
->len
);
299 case REDIS_REPLY_ARRAY
:
300 for (int j
= 0; j
< entry
->elements
; j
++)
302 tentry
= entry
->element
[j
];
305 result
= string_catn(result
, US
"\n", 1);
307 switch (tentry
->type
)
309 case REDIS_REPLY_INTEGER
:
310 result
= string_fmt_append(result
, "%d", tentry
->integer
);
312 case REDIS_REPLY_STRING
:
313 result
= string_catn(result
, US tentry
->str
, tentry
->len
);
315 case REDIS_REPLY_ARRAY
:
317 debug_printf_indent("REDIS: result has nesting of arrays which"
318 " is not supported. Ignoring!\n");
321 DEBUG(D_lookup
) debug_printf_indent(
322 "REDIS: result has unsupported type. Ignoring!\n");
328 DEBUG(D_lookup
) debug_printf_indent("REDIS: query returned unsupported type\n");
337 gstring_release_unused(result
);
341 *errmsg
= US
"REDIS: no data found";
346 /* Free store for any result that was got; don't close the connection,
349 if (redis_reply
) freeReplyObject(redis_reply
);
351 /* Non-NULL result indicates a successful result */
355 *resultptr
= string_from_gstring(result
);
360 DEBUG(D_lookup
) debug_printf_indent("%s\n", *errmsg
);
361 /* NOTE: Required to close connection since it needs to be reopened */
362 return yield
; /* FAIL or DEFER */
368 /*************************************************
370 *************************************************/
372 * See local README for interface description. The handle and filename
373 * arguments are not used. The code to loop through a list of servers while the
374 * query is deferred with a retryable error is now in a separate function that is
375 * shared with other noSQL lookups.
379 redis_find(void * handle
__attribute__((unused
)),
380 const uschar
* filename
__attribute__((unused
)),
381 const uschar
* command
, int length
, uschar
** result
, uschar
** errmsg
,
382 uint
* do_cache
, const uschar
* opts
)
384 return lf_sqlperform(US
"Redis", US
"redis_servers", redis_servers
, command
,
385 result
, errmsg
, do_cache
, opts
, perform_redis_search
);
390 /*************************************************
391 * Quote entry point *
392 *************************************************/
394 /* Prefix any whitespace, or backslash, with a backslash.
395 This is not a Redis thing but instead to let the argv splitting
396 we do to split on whitespace, yet provide means for getting
397 whitespace into an argument.
400 s the string to be quoted
401 opt additional option text or NULL if none
403 Returns: the processed string or NULL for a bad option
407 redis_quote(uschar
*s
, uschar
*opt
)
414 if (opt
) return NULL
; /* No options recognized */
416 while ((c
= *t
++) != 0)
417 if (isspace(c
) || c
== '\\') count
++;
419 if (count
== 0) return s
;
420 t
= quoted
= store_get(Ustrlen(s
) + count
+ 1, is_tainted(s
));
422 while ((c
= *s
++) != 0)
424 if (isspace(c
) || c
== '\\') *t
++ = '\\';
433 /*************************************************
434 * Version reporting entry point *
435 *************************************************/
436 #include "../version.h"
439 redis_version_report(FILE *f
)
441 fprintf(f
, "Library version: REDIS: Compile: %d [%d]\n",
442 HIREDIS_MAJOR
, HIREDIS_MINOR
);
444 fprintf(f
, " Exim version %s\n", EXIM_VERSION_STR
);
450 /* These are the lookup_info blocks for this driver */
451 static lookup_info redis_lookup_info
= {
452 .name
= US
"redis", /* lookup name */
453 .type
= lookup_querystyle
, /* query-style lookup */
454 .open
= redis_open
, /* open function */
455 .check
= NULL
, /* no check function */
456 .find
= redis_find
, /* find function */
457 .close
= NULL
, /* no close function */
458 .tidy
= redis_tidy
, /* tidy function */
459 .quote
= redis_quote
, /* quoting function */
460 .version_report
= redis_version_report
/* version reporting */
464 # define redis_lookup_module_info _lookup_module_info
465 #endif /* DYNLOOKUP */
467 static lookup_info
*_lookup_list
[] = { &redis_lookup_info
};
468 lookup_module_info redis_lookup_module_info
= { LOOKUP_MODULE_INFO_MAGIC
, _lookup_list
, 1 };
470 #endif /* LOOKUP_REDIS */
471 /* End of lookups/redis.c */