diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/scripts/MakeLinks | 1 | ||||
-rwxr-xr-x | src/scripts/lookups-Makefile | 17 | ||||
-rw-r--r-- | src/src/EDITME | 7 | ||||
-rw-r--r-- | src/src/config.h.defaults | 1 | ||||
-rw-r--r-- | src/src/deliver.c | 3 | ||||
-rw-r--r-- | src/src/drtables.c | 7 | ||||
-rw-r--r-- | src/src/exim.c | 3 | ||||
-rw-r--r-- | src/src/expand.c | 3 | ||||
-rw-r--r-- | src/src/globals.c | 4 | ||||
-rw-r--r-- | src/src/globals.h | 4 | ||||
-rw-r--r-- | src/src/lookups/Makefile | 2 | ||||
-rw-r--r-- | src/src/lookups/redis.c | 349 | ||||
-rw-r--r-- | src/src/readconf.c | 3 | ||||
-rw-r--r-- | src/src/route.c | 3 |
14 files changed, 407 insertions, 0 deletions
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks index a9abdab25..2eb8a967e 100755 --- a/src/scripts/MakeLinks +++ b/src/scripts/MakeLinks @@ -38,6 +38,7 @@ ln -s ../../src/lookups/ldap.h ldap.h ln -s ../../src/lookups/ldap.c ldap.c ln -s ../../src/lookups/lsearch.c lsearch.c ln -s ../../src/lookups/mysql.c mysql.c +ln -s ../../src/lookups/redis.c redis.c ln -s ../../src/lookups/nis.c nis.c ln -s ../../src/lookups/nisplus.c nisplus.c ln -s ../../src/lookups/oracle.c oracle.c diff --git a/src/scripts/lookups-Makefile b/src/scripts/lookups-Makefile index e7aeaa08a..51fbd944b 100755 --- a/src/scripts/lookups-Makefile +++ b/src/scripts/lookups-Makefile @@ -76,6 +76,15 @@ want_at_all() { grep -q "^[ $tab]*$re" "$defs_source" } +# Adapted want_at_all above to work for EXPERIMENTAL features +want_experimental() { + local want_name="$1" + local re="EXPERIMENTAL_${want_name}[ $tab]*=[ $tab]*." + env | grep -q "^$re" + if [ $? -eq 0 ]; then return 0; fi + grep -q "^[ $tab]*$re" "$defs_source" +} + # The values of these variables will be emitted into the Makefile. MODS="" @@ -139,6 +148,14 @@ fi OBJ="${OBJ} spf.o" +# Because the variable is EXPERIMENTAL_REDIS and not LOOKUP_REDIS we +# use a different function to check for EXPERIMENTAL_* features +# requested. Don't use the SPF method with dummy functions above. +if want_experimental REDIS +then + OBJ="${OBJ} redis.o" +fi + echo "MODS = $MODS" echo "OBJ = $OBJ" diff --git a/src/src/EDITME b/src/src/EDITME index 1db70f715..f44a1e3a5 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -473,6 +473,13 @@ EXIM_MONITOR=eximon.bin # eg. for logging to a database. # EXPERIMENTAL_TPDA=yes +# Uncomment the following line to add Redis lookup support +# You need to have hiredis installed on your system (https://github.com/redis/hiredis). +# Depending on where it is installed you may have to edit the CFLAGS and LDFLAGS lines. +# EXPERIMENTAL_REDIS=yes +# CFLAGS += -I/usr/local/include +# LDFLAGS += -lhiredis + ############################################################################### # THESE ARE THINGS YOU MIGHT WANT TO SPECIFY # diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index bf7ac63fb..19bc1b180 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -168,6 +168,7 @@ it's a default value. */ #define EXPERIMENTAL_DMARC #define EXPERIMENTAL_OCSP #define EXPERIMENTAL_PRDR +#define EXPERIMENTAL_REDIS #define EXPERIMENTAL_SPF #define EXPERIMENTAL_SRS #define EXPERIMENTAL_TPDA diff --git a/src/src/deliver.c b/src/src/deliver.c index bc6a69fbf..8e1d17793 100644 --- a/src/src/deliver.c +++ b/src/src/deliver.c @@ -945,6 +945,9 @@ if (addr->message != NULL) if (((Ustrstr(addr->message, "failed to expand") != NULL) || (Ustrstr(addr->message, "expansion of ") != NULL)) && (Ustrstr(addr->message, "mysql") != NULL || Ustrstr(addr->message, "pgsql") != NULL || +#ifdef EXPERIMENTAL_REDIS + Ustrstr(addr->message, "redis") != NULL || +#endif Ustrstr(addr->message, "sqlite") != NULL || Ustrstr(addr->message, "ldap:") != NULL || Ustrstr(addr->message, "ldapdn:") != NULL || diff --git a/src/src/drtables.c b/src/src/drtables.c index c1332ed0b..699f32762 100644 --- a/src/src/drtables.c +++ b/src/src/drtables.c @@ -447,6 +447,9 @@ extern lookup_module_info sqlite_lookup_module_info; #ifdef EXPERIMENTAL_SPF extern lookup_module_info spf_lookup_module_info; #endif +#ifdef EXPERIMENTAL_REDIS +extern lookup_module_info redis_lookup_module_info; +#endif #if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2 extern lookup_module_info pgsql_lookup_module_info; #endif @@ -555,6 +558,10 @@ void init_lookup_list(void) addlookupmodule(NULL, &pgsql_lookup_module_info); #endif +#ifdef EXPERIMENTAL_REDIS + addlookupmodule(NULL, &redis_lookup_module_info); +#endif + #ifdef EXPERIMENTAL_SPF addlookupmodule(NULL, &spf_lookup_module_info); #endif diff --git a/src/src/exim.c b/src/src/exim.c index c5053ba7c..a715c0b39 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -828,6 +828,9 @@ fprintf(f, "Support for:"); #ifdef EXPERIMENTAL_TPDA fprintf(f, " Experimental_TPDA"); #endif +#ifdef EXPERIMENTAL_REDIS + fprintf(f, " Experimental_Redis"); +#endif fprintf(f, "\n"); fprintf(f, "Lookups (built-in):"); diff --git a/src/src/expand.c b/src/src/expand.c index a22ee2af4..5a764d3df 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -6644,6 +6644,9 @@ for (i = 1; i < argc; i++) #ifdef LOOKUP_PGSQL pgsql_servers = argv[i]; #endif + #ifdef EXPERIMENTAL_REDIS + redis_servers = argv[i]; + #endif } #ifdef EXIM_PERL else opt_perl_startup = argv[i]; diff --git a/src/src/globals.c b/src/src/globals.c index d4589cd18..1dfd23ce2 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -83,6 +83,10 @@ uschar *oracle_servers = NULL; uschar *pgsql_servers = NULL; #endif +#ifdef EXPERIMENTAL_REDIS +uschar *redis_servers = NULL; +#endif + #ifdef LOOKUP_SQLITE int sqlite_lock_timeout = 5; #endif diff --git a/src/src/globals.h b/src/src/globals.h index 104b5fa7a..4acc7f8c2 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -62,6 +62,10 @@ extern uschar *oracle_servers; /* List of servers and connect info */ extern uschar *pgsql_servers; /* List of servers and connect info */ #endif +#ifdef EXPERIMENTAL_REDIS +extern uschar *redis_servers; /* List of servers and connect info */ +#endif + #ifdef LOOKUP_SQLITE extern int sqlite_lock_timeout; /* Internal lock waiting timeout */ #endif diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile index 035f6f23f..6ba0cb169 100644 --- a/src/src/lookups/Makefile +++ b/src/src/lookups/Makefile @@ -41,6 +41,7 @@ nisplus.o: $(PHDRS) nisplus.c oracle.o: $(PHDRS) oracle.c passwd.o: $(PHDRS) passwd.c pgsql.o: $(PHDRS) pgsql.c +redis.o: $(PHDRS) redis.c spf.o: $(PHDRS) spf.c sqlite.o: $(PHDRS) sqlite.c testdb.o: $(PHDRS) testdb.c @@ -59,6 +60,7 @@ nisplus.so: $(PHDRS) nisplus.c oracle.so: $(PHDRS) oracle.c passwd.so: $(PHDRS) passwd.c pgsql.so: $(PHDRS) pgsql.c +redis.so: $(PHDRS) redis.c spf.so: $(PHDRS) spf.c sqlite.so: $(PHDRS) sqlite.c testdb.so: $(PHDRS) testdb.c diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c new file mode 100644 index 000000000..87cc9fd1a --- /dev/null +++ b/src/src/lookups/redis.c @@ -0,0 +1,349 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +#ifdef EXPERIMENTAL_REDIS + +#include "lf_functions.h" + +#include <hiredis/hiredis.h> + +/* Structure and anchor for caching connections. */ +typedef struct redis_connection { + struct redis_connection *next; + uschar *server; + redisContext *handle; +} redis_connection; + +static redis_connection *redis_connections = NULL; + +static void * +redis_open(uschar *filename, uschar **errmsg) +{ + return (void *)(1); +} + +void +redis_tidy(void) +{ + redis_connection *cn; + + /* + * XXX: Not sure how often this is called! + * Guess its called after every lookup which probably would mean to just + * not use the _tidy() function at all and leave with exim exiting to + * GC connections! + */ + while ((cn = redis_connections) != NULL) { + redis_connections = cn->next; + DEBUG(D_lookup) debug_printf("close REDIS connection: %s\n", cn->server); + redisFree(cn->handle); + } +} + +/* This function is called from the find entry point to do the search for a + * single server. + * + * Arguments: + * query the query string + * server the server string + * resultptr where to store the result + * errmsg where to point an error message + * defer_break TRUE if no more servers are to be tried after DEFER + * do_cache set false if data is changed + * + * The server string is of the form "host/dbnumber/password". The host can be + * host:port. This string is in a nextinlist temporary buffer, so can be + * overwritten. + * + * Returns: OK, FAIL, or DEFER + */ +static int +perform_redis_search(uschar *command, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break, BOOL *do_cache) +{ + redisContext *redis_handle = NULL; /* Keep compilers happy */ + redisReply *redis_reply = NULL; + redisReply *entry = NULL; + redisReply *tentry = NULL; + redis_connection *cn; + int ssize = 0; + int offset = 0; + int yield = DEFER; + int i, j; + uschar *result = NULL; + uschar *server_copy = NULL; + uschar *tmp, *ttmp; + uschar *sdata[3]; + + /* + * Disaggregate the parameters from the server argument. + * The order is host:port(socket) + * We can write to the string, since it is in a nextinlist temporary buffer. + * This copy is also used for debugging output. + */ + memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */; + for (i = 2; i > 0; i--) { + uschar *pp = Ustrrchr(server, '/'); + if (pp == NULL) { + *errmsg = string_sprintf("incomplete Redis server data: %s", (i == 2) ? server : server_copy); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 2) server_copy = string_copy(server); /* sans password */ + } + sdata[0] = server; /* What's left at the start */ + + /* If the database or password is an empty string, set it NULL */ + if (sdata[1][0] == 0) sdata[1] = NULL; + if (sdata[2][0] == 0) sdata[2] = NULL; + + /* See if we have a cached connection to the server */ + for (cn = redis_connections; cn != NULL; cn = cn->next) { + if (Ustrcmp(cn->server, server_copy) == 0) { + redis_handle = cn->handle; + break; + } + } + + if (cn == NULL) { + uschar *p; + uschar *socket = NULL; + int port = 0; + /* int redis_err = REDIS_OK; */ + + if ((p = Ustrchr(sdata[0], '(')) != NULL) { + *p++ = 0; + socket = p; + while (*p != 0 && *p != ')') + p++; + *p = 0; + } + + if ((p = Ustrchr(sdata[0], ':')) != NULL) { + *p++ = 0; + port = Uatoi(p); + } else { + port = Uatoi("6379"); + } + + if (Ustrchr(server, '/') != NULL) { + *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s", sdata[0]); + *defer_break = TRUE; + return DEFER; + } + + DEBUG(D_lookup) + debug_printf("REDIS new connection: host=%s port=%d socket=%s database=%s\n", sdata[0], port, socket, sdata[1]); + + /* Get store for a new handle, initialize it, and connect to the server */ + /* XXX: Use timeouts ? */ + if (socket != NULL) + redis_handle = redisConnectUnix(CCS socket); + else + redis_handle = redisConnect(CCS server, port); + if (redis_handle == NULL) { + *errmsg = string_sprintf("REDIS connection failed"); + *defer_break = FALSE; + goto REDIS_EXIT; + } + + /* Add the connection to the cache */ + cn = store_get(sizeof(redis_connection)); + cn->server = server_copy; + cn->handle = redis_handle; + cn->next = redis_connections; + redis_connections = cn; + } else { + DEBUG(D_lookup) + debug_printf("REDIS using cached connection for %s\n", server_copy); + } + + /* Authenticate if there is a password */ + if(sdata[2] != NULL) { + if ((redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])) == NULL) { + *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr); + *defer_break = FALSE; + goto REDIS_EXIT; + } + } + + /* Select the database if there is a dbnumber passed */ + if(sdata[1] != NULL) { + if ((redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])) == NULL) { + *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr); + *defer_break = FALSE; + goto REDIS_EXIT; + } else { + DEBUG(D_lookup) debug_printf("REDIS: Selecting database=%s\n", sdata[1]); + } + } + + /* Run the command */ + if ((redis_reply = redisCommand(redis_handle, CS command)) == NULL) { + *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr); + *defer_break = FALSE; + goto REDIS_EXIT; + } + + switch (redis_reply->type) { + case REDIS_REPLY_ERROR: + *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str); + *defer_break = FALSE; + *do_cache = FALSE; + goto REDIS_EXIT; + /* NOTREACHED */ + + break; + case REDIS_REPLY_NIL: + DEBUG(D_lookup) debug_printf("REDIS: query was not one that returned any data\n"); + result = string_sprintf(""); + *do_cache = FALSE; + goto REDIS_EXIT; + /* NOTREACHED */ + + break; + case REDIS_REPLY_INTEGER: + ttmp = (redis_reply->integer == 1) ? US"true" : US"false"; + result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp)); + break; + case REDIS_REPLY_STRING: + case REDIS_REPLY_STATUS: + result = string_cat(result, &ssize, &offset, US redis_reply->str, redis_reply->len); + break; + case REDIS_REPLY_ARRAY: + + /* NOTE: For now support 1 nested array result. If needed a limitless result can be parsed */ + for (i = 0; i < redis_reply->elements; i++) { + entry = redis_reply->element[i]; + + if (result != NULL) + result = string_cat(result, &ssize, &offset, US"\n", 1); + + switch (entry->type) { + case REDIS_REPLY_INTEGER: + tmp = string_sprintf("%d", entry->integer); + result = string_cat(result, &ssize, &offset, US tmp, Ustrlen(tmp)); + break; + case REDIS_REPLY_STRING: + result = string_cat(result, &ssize, &offset, US entry->str, entry->len); + break; + case REDIS_REPLY_ARRAY: + for (j = 0; j < entry->elements; j++) { + tentry = entry->element[j]; + + if (result != NULL) + result = string_cat(result, &ssize, &offset, US"\n", 1); + + switch (tentry->type) { + case REDIS_REPLY_INTEGER: + ttmp = string_sprintf("%d", tentry->integer); + result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp)); + break; + case REDIS_REPLY_STRING: + result = string_cat(result, &ssize, &offset, US tentry->str, tentry->len); + break; + case REDIS_REPLY_ARRAY: + DEBUG(D_lookup) debug_printf("REDIS: result has nesting of arrays which is not supported. Ignoring!\n"); + break; + default: + DEBUG(D_lookup) debug_printf("REDIS: result has unsupported type. Ignoring!\n"); + break; + } + } + break; + default: + DEBUG(D_lookup) debug_printf("REDIS: query returned unsupported type\n"); + break; + } + } + break; + } + + + if (result == NULL) { + yield = FAIL; + *errmsg = US"REDIS: no data found"; + } else { + result[offset] = 0; + store_reset(result + offset + 1); + } + + REDIS_EXIT: + /* Free store for any result that was got; don't close the connection, as it is cached. */ + if (redis_reply != NULL) + freeReplyObject(redis_reply); + + /* Non-NULL result indicates a sucessful result */ + if (result != NULL) { + *resultptr = result; + return OK; + } else { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + /* NOTE: Required to close connection since it needs to be reopened */ + return yield; /* FAIL or DEFER */ + } +} + +/************************************************* +* Find entry point * +*************************************************/ +/* + * See local README for interface description. The handle and filename + * arguments are not used. The code to loop through a list of servers while the + * query is deferred with a retryable error is now in a separate function that is + * shared with other noSQL lookups. + */ + +static int +redis_find(void *handle __attribute__((unused)), uschar *filename __attribute__((unused)), + uschar *command, int length, uschar **result, uschar **errmsg, BOOL *do_cache) +{ + return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command, + result, errmsg, do_cache, perform_redis_search); +} + +/************************************************* +* Version reporting entry point * +*************************************************/ +#include "../version.h" + +void +redis_version_report(FILE *f) +{ + fprintf(f, "Library version: REDIS: Compile: %d [%d]\n", + HIREDIS_MAJOR, HIREDIS_MINOR); +#ifdef DYNLOOKUP + fprintf(f, " Exim version %s\n", EXIM_VERSION_STR); +#endif +} + +/* These are the lookup_info blocks for this driver */ +static lookup_info redis_lookup_info = { + US"redis", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + redis_open, /* open function */ + NULL, /* no check function */ + redis_find, /* find function */ + NULL, /* no close function */ + redis_tidy, /* tidy function */ + NULL, /* quoting function */ + redis_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define redis_lookup_module_info _lookup_module_info +#endif /* DYNLOOKUP */ + +static lookup_info *_lookup_list[] = { &redis_lookup_info }; +lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +#endif /* EXPERIMENTAL_REDIS */ +/* End of lookups/redis.c */ diff --git a/src/src/readconf.c b/src/src/readconf.c index 218eff704..6b0f3aaf7 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -350,6 +350,9 @@ static optionlist optionlist_config[] = { { "recipient_unqualified_hosts", opt_stringptr, &recipient_unqualified_hosts }, { "recipients_max", opt_int, &recipients_max }, { "recipients_max_reject", opt_bool, &recipients_max_reject }, +#ifdef EXPERIMENTAL_REDIS + { "redis_servers", opt_stringptr, &redis_servers }, +#endif { "remote_max_parallel", opt_int, &remote_max_parallel }, { "remote_sort_domains", opt_stringptr, &remote_sort_domains }, { "retry_data_expire", opt_time, &retry_data_expire }, diff --git a/src/src/route.c b/src/src/route.c index 2fee38271..f8f3b86a5 100644 --- a/src/src/route.c +++ b/src/src/route.c @@ -1961,6 +1961,9 @@ if (yield == DEFER) { ( Ustrstr(addr->message, "mysql") != NULL || Ustrstr(addr->message, "pgsql") != NULL || +#ifdef EXPERIMENTAL_REDIS + Ustrstr(addr->message, "redis") != NULL || +#endif Ustrstr(addr->message, "sqlite") != NULL || Ustrstr(addr->message, "ldap:") != NULL || Ustrstr(addr->message, "ldapdn:") != NULL || |