diff options
Diffstat (limited to 'src/lookups')
-rw-r--r-- | src/lookups/Makefile | 71 | ||||
-rw-r--r-- | src/lookups/README | 175 | ||||
-rw-r--r-- | src/lookups/cdb.c | 513 | ||||
-rw-r--r-- | src/lookups/dbmdb.c | 287 | ||||
-rw-r--r-- | src/lookups/dnsdb.c | 609 | ||||
-rw-r--r-- | src/lookups/dsearch.c | 157 | ||||
-rw-r--r-- | src/lookups/ibase.c | 585 | ||||
-rw-r--r-- | src/lookups/ldap.c | 1640 | ||||
-rw-r--r-- | src/lookups/ldap.h | 13 | ||||
-rw-r--r-- | src/lookups/lf_check_file.c | 113 | ||||
-rw-r--r-- | src/lookups/lf_functions.h | 18 | ||||
-rw-r--r-- | src/lookups/lf_quote.c | 64 | ||||
-rw-r--r-- | src/lookups/lf_sqlperform.c | 138 | ||||
-rw-r--r-- | src/lookups/lmdb.c | 160 | ||||
-rw-r--r-- | src/lookups/lsearch.c | 484 | ||||
-rw-r--r-- | src/lookups/mysql.c | 505 | ||||
-rw-r--r-- | src/lookups/nis.c | 137 | ||||
-rw-r--r-- | src/lookups/nisplus.c | 293 | ||||
-rw-r--r-- | src/lookups/oracle.c | 631 | ||||
-rw-r--r-- | src/lookups/passwd.c | 90 | ||||
-rw-r--r-- | src/lookups/pgsql.c | 510 | ||||
-rw-r--r-- | src/lookups/redis.c | 471 | ||||
-rw-r--r-- | src/lookups/spf.c | 121 | ||||
-rw-r--r-- | src/lookups/sqlite.c | 187 | ||||
-rw-r--r-- | src/lookups/testdb.c | 103 | ||||
-rw-r--r-- | src/lookups/whoson.c | 101 |
26 files changed, 8176 insertions, 0 deletions
diff --git a/src/lookups/Makefile b/src/lookups/Makefile new file mode 100644 index 0000000..7c97006 --- /dev/null +++ b/src/lookups/Makefile @@ -0,0 +1,71 @@ +# Make file for building Exim's lookup modules. +# This is called from the main make file, after cd'ing +# to the lookups subdirectory. + +# nb: at build time, the version of this file used will have had some +# extra variable definitions and prepended to it and module build rules +# interpolated below. + +# MAGIC-TAG-MODS-OBJ-RULES-GO-HERE + + +all: lookups.a lf_quote.o lf_check_file.o lf_sqlperform.o $(MODS) + +lookups.a: $(OBJ) + @$(RM_COMMAND) -f lookups.a + @echo "$(AR) lookups.a" + @$(AR) lookups.a $(OBJ) + $(RANLIB) $@ + +.SUFFIXES: .o .c .so +.c.o:; @echo "$(CC) $*.c" + $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c + +.c.so:; @echo "$(CC) -shared $*.c" + $(FE)$(CC) $(LOOKUP_$*_INCLUDE) $(LOOKUP_$*_LIBS) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@ + +lf_check_file.o: $(PHDRS) lf_check_file.c lf_functions.h +lf_quote.o: $(PHDRS) lf_quote.c lf_functions.h +lf_sqlperform.o: $(PHDRS) lf_sqlperform.c lf_functions.h + +cdb.o: $(PHDRS) cdb.c +dbmdb.o: $(PHDRS) dbmdb.c +dnsdb.o: $(PHDRS) dnsdb.c +dsearch.o: $(PHDRS) dsearch.c +ibase.o: $(PHDRS) ibase.c +ldap.o: $(PHDRS) ldap.c +lmdb.o: $(PHDRS) lmdb.c +lsearch.o: $(PHDRS) lsearch.c +mysql.o: $(PHDRS) mysql.c +nis.o: $(PHDRS) nis.c +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 +whoson.o: $(PHDRS) whoson.c + +cdb.so: $(PHDRS) cdb.c +dbmdb.so: $(PHDRS) dbmdb.c +dnsdb.so: $(PHDRS) dnsdb.c +dsearch.so: $(PHDRS) dsearch.c +ibase.so: $(PHDRS) ibase.c +ldap.so: $(PHDRS) ldap.c +lmdb.so: $(PHDRS) lmdb.c +lsearch.so: $(PHDRS) lsearch.c +mysql.so: $(PHDRS) mysql.c +nis.so: $(PHDRS) nis.c +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 +whoson.so: $(PHDRS) whoson.c + +# End diff --git a/src/lookups/README b/src/lookups/README new file mode 100644 index 0000000..31fea64 --- /dev/null +++ b/src/lookups/README @@ -0,0 +1,175 @@ +LOOKUPS +------- + +Each lookup type is implemented by 6 functions, xxx_open(), xxx_check(), +xxx_find(), xxx_close(), xxx_tidy(), and xxx_quote(), where xxx is the name of +the lookup type (e.g. lsearch, dbm, or whatever). In addition, there is +a version reporting function used to trace compile-vs-runtime conflicts and +to help administrators ensure that the modules from the correct build are +in use by the main binary. + +The xxx_check(), xxx_close(), xxx_tidy(), and xxx_quote() functions need not +exist. There is a table in drtables.c which links the lookup names to the +various sets of functions, with NULL entries for any that don't exist. When +the code for a lookup type is omitted from the binary, all its entries are +NULL. + +One of the fields in the table contains flags describing the kind of lookup. +These are + + lookup_querystyle + +This is a "query style" lookup without a file name, as opposed to the "single +key" style, where the key is associated with a "file name". + + lookup_absfile + +For single key lookups, this means that the file name must be an absolute path. +It is set for lsearch and dbm, but not for NIS, for example. + + lookup_absfilequery + +This is a query-style lookup that must start with an absolute file name. For +example, the sqlite lookup is of this type. + +When a single-key or absfilequery lookup file is opened, the handle returned by +the xxx_open() function is saved, along with the file name and lookup type, in +a tree. The xxx_close() function is not called when the first lookup is +completed. If there are subsequent lookups of the same type that quote the same +file name, xxx_open() isn't called; instead the cached handle is re-used. + +Exim calls the function search_tidyup() at strategic points in its processing +(e.g. after all routing and directing has been done) and this function walks +the tree and calls the xxx_close() functions for all the cached handles. + +Query-style lookups don't have the concept of an open file that can be cached +this way. Of course, the local code for the lookup can manage its own caching +information in any way it pleases. This means that the xxx_close() +function, even it it exists, is never called. However, if an xxx_tidy() +function exists, it is called once whenever Exim calls search_tidyup(). + +A single-key lookup type may also have an xxx_tidy() function, which is called +by search_tidyup() after all cached handles have been closed via the +xxx_close() function. + +The lookup functions are wrapped into a special store pool (POOL_SEARCH). You +can safely use store_get to allocate store for your handle caching. The store +will be reset after all xxx_tidy() functions are called. + +The function interfaces are as follows: + + +xxx_open() +---------- + +This function is called to initiate the lookup. For things that involve files +it should do a real open; for other kinds of lookup it may do nothing at all. +The arguments are: + + uschar *filename the name of the "file" to open, for non-query-style + lookups; NULL for query-style lookups + uschar **errmsg where to put an error message if there is a problem + +The yield of xxx_open() is a void * value representing the open file or +database. For real files is is normally the FILE or DBM value. For other +kinds of lookup, if there is no natural value to use, (-1) is recommended. +The value should not be NULL (or 0) as that is taken to indicate failure of +the xxx_open() function. For single-key lookups, the handle is cached along +with the filename and type, and may be used for several lookups. + + +xxx_check() +----------- + +If this function exists, it is called after a successful open to check on the +ownership and mode of the file(s). The arguments are: + + void *handle the handle passed back from xxx_open() + uschar *filename the filename passed to xxx_open() + int modemask mode bits that must not be set + int *owners permitted owners of the file(s) + int *owngroups permitted group owners of the file(s) + uschar **errmsg where to put an error message if there is a problem + +In the owners and owngroups vectors, the first element is the count of the +remaining elements. There is a common function that can be called to check +a file: + +int search_check_file(int fd, char *filename, int modemask, int *owners, + int *owngroups, char *type, char **errmsg); + +If fd is >= 0, it is checked using fstat(), and filename is used only in +error messages. If fd is < 0 then filename is checked using stat(). The yield +is zero if all is well, +1 if the mode or uid or gid is wrong, or -1 if the +stat() fails. + +The yield of xxx_check() is TRUE if all is well, FALSE otherwise. The +function should not close the file(s) on failure. That is done from outside +by calling the xxx_close() function. + + +xxx_find() +---------- + +This is called to search an open file/database. The result is OK, FAIL, or +DEFER. The arguments are: + + void *handle the handle passed back from xxx_open() + uschar *filename the filename passed to xxx_open() (NULL for querystyle) + uschar *keyquery the key to look up, or query to process, zero-terminated + int length the length of the key + uschar **result point to the yield, in dynamic store, on success + uschar **errmsg where to put an error message on failure; + this is initially set to "", and should be left + as that for a standard "entry not found" error + uint *do_cache the lookup should set this to 0 when it changes data. + This is MAXINT by default. When set to 0 the cache tree + of the current search handle will be cleaned and the + current result will NOT be cached. Currently the mysql + and pgsql lookups use this when UPDATE/INSERT queries are + executed. + If set to a nonzero number of seconds, the cached value + becomes unusable after this time. Currently the dnsdb + lookup uses this to support the TTL value. + +Even though the key is zero-terminated, the length is passed because in the +common case it has been computed already and is often needed. + + +xxx_close() +----------- + +This is called for single-key lookups when a file is finished with. There is no +yield, and the only argument is the handle that was passed back from +xxx_open(). It is not called for query style lookups. + + +xxx_tidy() +---------- + +This function is called once at the end of search_tidyup() for every lookup +type for which it exists. + + +xxx_quote() +----------- + +This is called by the string expansion code for expansions of the form +${quote_xxx:<string>}, if it exists. If not, the expansion code makes no change +to the string. The function must apply any quoting rules that are specific to +the lookup, and return a pointer to the revised string. If quoting is not +needed, it can return its single argument, which is a uschar *. This function +does NOT use the POOL_SEARCH store, because it's usually never called from any +lookup code. + +xxx_report_version() +-------------------- + +This is called to report diagnostic information to a file stream. +Typically it would report both compile-time and run-time version information. +The arguments are: + + FILE *stream where to fprintf() the data to + + +**** diff --git a/src/lookups/cdb.c b/src/lookups/cdb.c new file mode 100644 index 0000000..153fcf2 --- /dev/null +++ b/src/lookups/cdb.c @@ -0,0 +1,513 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * Exim - CDB database lookup module + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 1998 Nigel Metheringham, Planet Online Ltd + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * -------------------------------------------------------------- + * Modified by PH for Exim 4: + * Changed over to using unsigned chars + * Makes use of lf_check_file() for file checking + * -------------------------------------------------------------- + * Modified by The Exim Maintainers 2015: + * const propagation + * -------------------------------------------------------------- + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * + * This code implements Dan Bernstein's Constant DataBase (cdb) spec. + * Information, the spec and sample code for cdb can be obtained from + * http://www.pobox.com/~djb/cdb.html + * + * This implementation borrows some code from Dan Bernstein's + * implementation (which has no license restrictions applied to it). + * This (read-only) implementation is completely contained within + * cdb.[ch] it does *not* link against an external cdb library. + * + * + * There are 2 variants included within this code. One uses MMAP and + * should give better performance especially for multiple lookups on a + * modern machine. The other is the default implementation which is + * used in the case where the MMAP fails or if MMAP was not compiled + * in. this implementation is the same as the original reference cdb + * implementation. The MMAP version is compiled in if the HAVE_MMAP + * preprocessor define is defined - this should be set in the system + * specific os.h file. + * + */ + + +#include "../exim.h" +#include "lf_functions.h" + +#ifdef HAVE_MMAP +# include <sys/mman.h> +/* Not all implementations declare MAP_FAILED */ +# ifndef MAP_FAILED +# define MAP_FAILED ((void *) -1) +# endif /* MAP_FAILED */ +#endif /* HAVE_MMAP */ + + +#define CDB_HASH_SPLIT 256 /* num pieces the hash table is split into */ +#define CDB_HASH_MASK 255 /* mask to and off split value */ +#define CDB_HASH_ENTRY 8 /* how big each offset it */ +#define CDB_HASH_TABLE (CDB_HASH_SPLIT * CDB_HASH_ENTRY) + +/* State information for cdb databases that are open NB while the db + * is open its contents will not change (cdb dbs are normally updated + * atomically by renaming). However the lifetime of one of these + * state structures should be limited - ie a long running daemon + * that opens one may hit problems.... + */ + +struct cdb_state { + int fileno; + off_t filelen; + uschar *cdb_map; + uschar *cdb_offsets; +}; + +/* 32 bit unsigned type - this is an int on all modern machines */ +typedef unsigned int uint32; + +/* + * cdb_hash() + * Internal function to make hash value */ + +static uint32 +cdb_hash(const uschar *buf, unsigned int len) +{ + uint32 h; + + h = 5381; + while (len) { + --len; + h += (h << 5); + h ^= (uint32) *buf++; + } + return h; +} + +/* + * cdb_bread() + * Internal function to read len bytes from disk, coping with oddities */ + +static int +cdb_bread(int fd, + uschar *buf, + int len) +{ + int r; + while (len > 0) { + do + r = Uread(fd,buf,len); + while ((r == -1) && (errno == EINTR)); + if (r == -1) return -1; + if (r == 0) { errno = EIO; return -1; } + buf += r; + len -= r; + } + return 0; +} + +/* + * cdb_bread() + * Internal function to parse 4 byte number (endian independent) */ + +static uint32 +cdb_unpack(uschar *buf) +{ + uint32 num; + num = buf[3]; num <<= 8; + num += buf[2]; num <<= 8; + num += buf[1]; num <<= 8; + num += buf[0]; + return num; +} + +static void cdb_close(void *handle); + +static void * +cdb_open(uschar *filename, + uschar **errmsg) +{ + int fileno; + struct cdb_state *cdbp; + struct stat statbuf; + void * mapbuf; + + fileno = Uopen(filename, O_RDONLY, 0); + if (fileno == -1) { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s for cdb lookup", filename); + errno = save_errno; + return NULL; + } + + if (fstat(fileno, &statbuf) == 0) { + /* If this is a valid file, then it *must* be at least + * CDB_HASH_TABLE bytes long */ + if (statbuf.st_size < CDB_HASH_TABLE) { + int save_errno = errno; + *errmsg = string_open_failed(errno, + "%s too short for cdb lookup", + filename); + errno = save_errno; + return NULL; + } + } else { + int save_errno = errno; + *errmsg = string_open_failed(errno, + "fstat(%s) failed - cannot do cdb lookup", + filename); + errno = save_errno; + return NULL; + } + + /* Having got a file open we need the structure to put things in */ + cdbp = store_get(sizeof(struct cdb_state)); + /* store_get() does not return if memory was not available... */ + /* preload the structure.... */ + cdbp->fileno = fileno; + cdbp->filelen = statbuf.st_size; + cdbp->cdb_map = NULL; + cdbp->cdb_offsets = NULL; + + /* if we are allowed to we use mmap here.... */ +#ifdef HAVE_MMAP + mapbuf = mmap(NULL, + statbuf.st_size, + PROT_READ, + MAP_SHARED, + fileno, + 0); + if (mapbuf != MAP_FAILED) { + /* We have an mmap-ed section. Now we can just use it */ + cdbp->cdb_map = mapbuf; + /* The offsets can be set to the same value since they should + * effectively be cached as well + */ + cdbp->cdb_offsets = mapbuf; + + /* Now return the state struct */ + return(cdbp); + } else { + /* If we got here the map failed. Basically we can ignore + * this since we fall back to slower methods.... + * However lets debug log it... + */ + DEBUG(D_lookup) debug_printf("cdb mmap failed - %d\n", errno); + } +#endif /* HAVE_MMAP */ + + /* In this case we have either not got MMAP allowed, or it failed */ + + /* get a buffer to stash the basic offsets in - this should speed + * things up a lot - especially on multiple lookups */ + cdbp->cdb_offsets = store_get(CDB_HASH_TABLE); + + /* now fill the buffer up... */ + if (cdb_bread(fileno, cdbp->cdb_offsets, CDB_HASH_TABLE) == -1) { + /* read of hash table failed, oh dear, oh..... + * time to give up I think.... + * call the close routine (deallocs the memory), and return NULL */ + *errmsg = string_open_failed(errno, + "cannot read header from %s for cdb lookup", + filename); + cdb_close(cdbp); + return NULL; + } + + /* Everything else done - return the cache structure */ + return cdbp; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +static BOOL +cdb_check(void *handle, + uschar *filename, + int modemask, + uid_t *owners, + gid_t *owngroups, + uschar **errmsg) +{ + struct cdb_state * cdbp = handle; + return lf_check_file(cdbp->fileno, + filename, + S_IFREG, + modemask, + owners, + owngroups, + "cdb", + errmsg) == 0; +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +static int +cdb_find(void *handle, + uschar *filename, + const uschar *keystring, + int key_len, + uschar **result, + uschar **errmsg, + uint *do_cache) +{ +struct cdb_state * cdbp = handle; +uint32 item_key_len, +item_dat_len, +key_hash, +item_hash, +item_posn, +cur_offset, +end_offset, +hash_offset_entry, +hash_offset, +hash_offlen, +hash_slotnm; +int loop; + +/* Keep picky compilers happy */ +do_cache = do_cache; + +key_hash = cdb_hash(keystring, key_len); + +hash_offset_entry = CDB_HASH_ENTRY * (key_hash & CDB_HASH_MASK); +hash_offset = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry); +hash_offlen = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry + 4); + +/* If the offset length is zero this key cannot be in the file */ + +if (hash_offlen == 0) + return FAIL; + +hash_slotnm = (key_hash >> 8) % hash_offlen; + +/* check to ensure that the file is not corrupt + * if the hash_offset + (hash_offlen * CDB_HASH_ENTRY) is longer + * than the file, then we have problems.... */ + +if ((hash_offset + (hash_offlen * CDB_HASH_ENTRY)) > cdbp->filelen) + { + *errmsg = string_sprintf("cdb: corrupt cdb file %s (too short)", + filename); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + +cur_offset = hash_offset + (hash_slotnm * CDB_HASH_ENTRY); +end_offset = hash_offset + (hash_offlen * CDB_HASH_ENTRY); + +/* if we are allowed to we use mmap here.... */ + +#ifdef HAVE_MMAP +/* make sure the mmap was OK */ +if (cdbp->cdb_map != NULL) + { + uschar * cur_pos = cur_offset + cdbp->cdb_map; + uschar * end_pos = end_offset + cdbp->cdb_map; + + for (loop = 0; (loop < hash_offlen); ++loop) + { + item_hash = cdb_unpack(cur_pos); + cur_pos += 4; + item_posn = cdb_unpack(cur_pos); + cur_pos += 4; + + /* if the position is zero then we have a definite miss */ + + if (item_posn == 0) + return FAIL; + + if (item_hash == key_hash) + { /* matching hash value */ + uschar * item_ptr = cdbp->cdb_map + item_posn; + + item_key_len = cdb_unpack(item_ptr); + item_ptr += 4; + item_dat_len = cdb_unpack(item_ptr); + item_ptr += 4; + + /* check key length matches */ + + if (item_key_len == key_len) + { + /* finally check if key matches */ + if (Ustrncmp(keystring, item_ptr, key_len) == 0) + { + /* we have a match.... * make item_ptr point to data */ + + item_ptr += item_key_len; + + /* ... and the returned result */ + + *result = store_get(item_dat_len + 1); + memcpy(*result, item_ptr, item_dat_len); + (*result)[item_dat_len] = 0; + return OK; + } + } + } + /* handle warp round of table */ + if (cur_pos == end_pos) + cur_pos = cdbp->cdb_map + hash_offset; + } + /* looks like we failed... */ + return FAIL; + } + +#endif /* HAVE_MMAP */ + +for (loop = 0; (loop < hash_offlen); ++loop) + { + uschar packbuf[8]; + + if (lseek(cdbp->fileno, (off_t) cur_offset, SEEK_SET) == -1) return DEFER; + if (cdb_bread(cdbp->fileno, packbuf, 8) == -1) return DEFER; + + item_hash = cdb_unpack(packbuf); + item_posn = cdb_unpack(packbuf + 4); + + /* if the position is zero then we have a definite miss */ + + if (item_posn == 0) + return FAIL; + + if (item_hash == key_hash) + { /* matching hash value */ + if (lseek(cdbp->fileno, (off_t) item_posn, SEEK_SET) == -1) return DEFER; + if (cdb_bread(cdbp->fileno, packbuf, 8) == -1) return DEFER; + + item_key_len = cdb_unpack(packbuf); + + /* check key length matches */ + + if (item_key_len == key_len) + { /* finally check if key matches */ + uschar * item_key = store_get(key_len); + + if (cdb_bread(cdbp->fileno, item_key, key_len) == -1) return DEFER; + if (Ustrncmp(keystring, item_key, key_len) == 0) { + + /* Reclaim some store */ + store_reset(item_key); + + /* matches - get data length */ + item_dat_len = cdb_unpack(packbuf + 4); + + /* then we build a new result string. We know we have enough + memory so disable Coverity errors about the tainted item_dat_ken */ + + *result = store_get(item_dat_len + 1); + /* coverity[tainted_data] */ + if (cdb_bread(cdbp->fileno, *result, item_dat_len) == -1) + return DEFER; + + /* coverity[tainted_data] */ + (*result)[item_dat_len] = 0; + return OK; + } + /* Reclaim some store */ + store_reset(item_key); + } + } + cur_offset += 8; + + /* handle warp round of table */ + if (cur_offset == end_offset) + cur_offset = hash_offset; + } +return FAIL; +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +static void +cdb_close(void *handle) +{ +struct cdb_state * cdbp = handle; + +#ifdef HAVE_MMAP + if (cdbp->cdb_map) { + munmap(CS cdbp->cdb_map, cdbp->filelen); + if (cdbp->cdb_map == cdbp->cdb_offsets) + cdbp->cdb_offsets = NULL; + } +#endif /* HAVE_MMAP */ + + (void)close(cdbp->fileno); +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +cdb_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: CDB: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +lookup_info cdb_lookup_info = { + US"cdb", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + cdb_open, /* open function */ + cdb_check, /* check function */ + cdb_find, /* find function */ + cdb_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + cdb_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define cdb_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &cdb_lookup_info }; +lookup_module_info cdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/cdb.c */ diff --git a/src/lookups/dbmdb.c b/src/lookups/dbmdb.c new file mode 100644 index 0000000..9e3d68e --- /dev/null +++ b/src/lookups/dbmdb.c @@ -0,0 +1,287 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +static void * +dbmdb_open(uschar *filename, uschar **errmsg) +{ +uschar * dirname = string_copy(filename); +uschar * s; +EXIM_DB *yield = NULL; + +if ((s = Ustrrchr(dirname, '/'))) *s = '\0'; +EXIM_DBOPEN(filename, dirname, O_RDONLY, 0, &yield); +if (yield == NULL) + { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s as a %s file", filename, EXIM_DBTYPE); + errno = save_errno; + } +return yield; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +/* This needs to know more about the underlying files than is good for it! +We need to know what the real file names are in order to check the owners and +modes. If USE_DB is set, we know it is Berkeley DB, which uses an unmodified +file name. If USE_TDB or USE_GDBM is set, we know it is tdb or gdbm, which do +the same. Otherwise, for safety, we have to check for x.db or x.dir and x.pag. +*/ + +static BOOL +dbmdb_check(void *handle, uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +int rc; +handle = handle; /* Keep picky compilers happy */ + +#if defined(USE_DB) || defined(USE_TDB) || defined(USE_GDBM) +rc = lf_check_file(-1, filename, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); +#else + { + uschar filebuffer[256]; + (void)sprintf(CS filebuffer, "%.250s.db", filename); + rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); + if (rc < 0) /* stat() failed */ + { + (void)sprintf(CS filebuffer, "%.250s.dir", filename); + rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); + if (rc == 0) /* x.dir was OK */ + { + (void)sprintf(CS filebuffer, "%.250s.pag", filename); + rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); + } + } + } +#endif + +return rc == 0; +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. This function adds 1 to +the keylength in order to include the terminating zero. */ + +static int +dbmdb_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +EXIM_DB *d = (EXIM_DB *)handle; +EXIM_DATUM key, data; + +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +do_cache = do_cache; + +EXIM_DATUM_INIT(key); /* Some DBM libraries require datums to */ +EXIM_DATUM_INIT(data); /* be cleared before use. */ +EXIM_DATUM_DATA(key) = CS keystring; +EXIM_DATUM_SIZE(key) = length + 1; + +if (EXIM_DBGET(d, key, data)) + { + *result = string_copyn(US EXIM_DATUM_DATA(data), EXIM_DATUM_SIZE(data)); + EXIM_DATUM_FREE(data); /* Some DBM libraries need a free() call */ + return OK; + } +return FAIL; +} + + + +/************************************************* +* Find entry point - no zero on key * +*************************************************/ + +/* See local README for interface description */ + +int +static dbmnz_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +return dbmdb_find(handle, filename, keystring, length-1, result, errmsg, + do_cache); +} + + + +/************************************************* +* Find entry point - zero-joined list key * +*************************************************/ + +/* + * The parameter passed as a key is a list in normal Exim list syntax. + * The elements of that list are joined together on NUL, with no trailing + * NUL, to form the key. + */ + +static int +dbmjz_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +uschar *key_item, *key_buffer, *key_p; +const uschar *key_elems = keystring; +int buflen, bufleft, key_item_len, sep = 0; + +/* To a first approximation, the size of the lookup key needs to be about, +or less than, the length of the delimited list passed in + 1. */ + +buflen = length + 3; +key_buffer = store_get(buflen); + +key_buffer[0] = '\0'; + +key_p = key_buffer; +bufleft = buflen; + +/* In all cases of an empty list item, we can set 1 and advance by 1 and then +pick up the trailing NUL from the previous list item, EXCEPT when at the +beginning of the output string, in which case we need to supply that NUL +ourselves. */ +while ((key_item = string_nextinlist(&key_elems, &sep, key_p, bufleft)) != NULL) + { + key_item_len = Ustrlen(key_item) + 1; + if (key_item_len == 1) + { + key_p[0] = '\0'; + if (key_p == key_buffer) + { + key_p[1] = '\0'; + key_item_len += 1; + } + } + + bufleft -= key_item_len; + if (bufleft <= 0) + { + /* The string_nextinlist() will stop at buffer size, but we should always + have at least 1 character extra, so some assumption has failed. */ + *errmsg = string_copy(US"Ran out of buffer space for joining elements"); + return DEFER; + } + key_p += key_item_len; + } + +if (key_p == key_buffer) + { + *errmsg = string_copy(US"empty list key"); + return FAIL; + } + +/* We do not pass in the final NULL; if needed, the list should include an +empty element to put one in. Boundary: key length 1, is a NULL */ +key_item_len = key_p - key_buffer - 1; + +DEBUG(D_lookup) debug_printf("NUL-joined key length: %d\n", key_item_len); + +/* beware that dbmdb_find() adds 1 to length to get back terminating NUL, so +because we've calculated the real length, we need to subtract one more here */ +return dbmdb_find(handle, filename, + key_buffer, key_item_len - 1, + result, errmsg, do_cache); +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +void +static dbmdb_close(void *handle) +{ +EXIM_DBCLOSE((EXIM_DB *)handle); +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +dbm_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: DBM: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +lookup_info dbm_lookup_info = { + US"dbm", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + dbmdb_open, /* open function */ + dbmdb_check, /* check function */ + dbmdb_find, /* find function */ + dbmdb_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + dbm_version_report /* version reporting */ +}; + +lookup_info dbmz_lookup_info = { + US"dbmnz", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + dbmdb_open, /* sic */ /* open function */ + dbmdb_check, /* sic */ /* check function */ + dbmnz_find, /* find function */ + dbmdb_close, /* sic */ /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + NULL /* no version reporting (redundant) */ +}; + +lookup_info dbmjz_lookup_info = { + US"dbmjz", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + dbmdb_open, /* sic */ /* open function */ + dbmdb_check, /* sic */ /* check function */ + dbmjz_find, /* find function */ + dbmdb_close, /* sic */ /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + NULL /* no version reporting (redundant) */ +}; + +#ifdef DYNLOOKUP +#define dbmdb_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &dbm_lookup_info, &dbmz_lookup_info, &dbmjz_lookup_info }; +lookup_module_info dbmdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 3 }; + +/* End of lookups/dbmdb.c */ diff --git a/src/lookups/dnsdb.c b/src/lookups/dnsdb.c new file mode 100644 index 0000000..e75bd1e --- /dev/null +++ b/src/lookups/dnsdb.c @@ -0,0 +1,609 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + + + +/* Ancient systems (e.g. SunOS4) don't appear to have T_TXT defined in their +header files. */ + +#ifndef T_TXT +# define T_TXT 16 +#endif + +/* Many systems do not have T_SPF. */ +#ifndef T_SPF +# define T_SPF 99 +#endif + +/* New TLSA record for DANE */ +#ifndef T_TLSA +# define T_TLSA 52 +#endif + +/* Table of recognized DNS record types and their integer values. */ + +static const char *type_names[] = { + "a", +#if HAVE_IPV6 + "a+", + "aaaa", +#endif + "cname", + "csa", + "mx", + "mxh", + "ns", + "ptr", + "soa", + "spf", + "srv", + "tlsa", + "txt", + "zns" +}; + +static int type_values[] = { + T_A, +#if HAVE_IPV6 + T_ADDRESSES, /* Private type for AAAA + A */ + T_AAAA, +#endif + T_CNAME, + T_CSA, /* Private type for "Client SMTP Authorization". */ + T_MX, + T_MXH, /* Private type for "MX hostnames" */ + T_NS, + T_PTR, + T_SOA, + T_SPF, + T_SRV, + T_TLSA, + T_TXT, + T_ZNS /* Private type for "zone nameservers" */ +}; + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +dnsdb_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; /* Ditto */ +return (void *)(-1); /* Any non-0 value */ +} + + + +/************************************************* +* Find entry point for dnsdb * +*************************************************/ + +/* See local README for interface description. The query in the "keystring" may +consist of a number of parts. + +(a) If the first significant character is '>' then the next character is the +separator character that is used when multiple records are found. The default +separator is newline. + +(b) If the next character is ',' then the next character is the separator +character used for multiple items of text in "TXT" records. Alternatively, +if the next character is ';' then these multiple items are concatenated with +no separator. With neither of these options specified, only the first item +is output. Similarly for "SPF" records, but the default for joining multiple +items in one SPF record is the empty string, for direct concatenation. + +(c) Options, all comma-terminated, in any order. Any unrecognised option +terminates option processing. Recognised options are: + +- 'defer_FOO': set the defer behaviour to FOO. The possible behaviours are: +'strict', where any defer causes the whole lookup to defer; 'lax', where a defer +causes the whole lookup to defer only if none of the DNS queries succeeds; and +'never', where all defers are as if the lookup failed. The default is 'lax'. + +- 'dnssec_FOO', with 'strict', 'lax' and 'never' (default). The meanings are +require, try and don't-try dnssec respectively. + +- 'retrans_VAL', set the timeout value. VAL is an Exim time specification +(eg "5s"). The default is set by the main configuration option 'dns_retrans'. + +- 'retry_VAL', set the retry count on timeouts. VAL is an integer. The +default is set by the main configuration option "dns_retry". + +(d) If the next sequence of characters is a sequence of letters and digits +followed by '=', it is interpreted as the name of the DNS record type. The +default is "TXT". + +(e) Then there follows list of domain names. This is a generalized Exim list, +which may start with '<' in order to set a specific separator. The default +separator, as always, is colon. */ + +static int +dnsdb_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +int rc; +int sep = 0; +int defer_mode = PASS; +int dnssec_mode = OK; +int save_retrans = dns_retrans; +int save_retry = dns_retry; +int type; +int failrc = FAIL; +const uschar *outsep = CUS"\n"; +const uschar *outsep2 = NULL; +uschar *equals, *domain, *found; + +/* Because we're working in the search pool, we try to reclaim as much +store as possible later, so we preallocate the result here */ + +gstring * yield = string_get(256); + +dns_record * rr; +dns_answer dnsa; +dns_scan dnss; + +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; +do_cache = do_cache; + +/* If the string starts with '>' we change the output separator. +If it's followed by ';' or ',' we set the TXT output separator. */ + +while (isspace(*keystring)) keystring++; +if (*keystring == '>') + { + outsep = keystring + 1; + keystring += 2; + if (*keystring == ',') + { + outsep2 = keystring + 1; + keystring += 2; + } + else if (*keystring == ';') + { + outsep2 = US""; + keystring++; + } + while (isspace(*keystring)) keystring++; + } + +/* Check for a modifier keyword. */ + +for (;;) + { + if (strncmpic(keystring, US"defer_", 6) == 0) + { + keystring += 6; + if (strncmpic(keystring, US"strict", 6) == 0) + { defer_mode = DEFER; keystring += 6; } + else if (strncmpic(keystring, US"lax", 3) == 0) + { defer_mode = PASS; keystring += 3; } + else if (strncmpic(keystring, US"never", 5) == 0) + { defer_mode = OK; keystring += 5; } + else + { + *errmsg = US"unsupported dnsdb defer behaviour"; + return DEFER; + } + } + else if (strncmpic(keystring, US"dnssec_", 7) == 0) + { + keystring += 7; + if (strncmpic(keystring, US"strict", 6) == 0) + { dnssec_mode = DEFER; keystring += 6; } + else if (strncmpic(keystring, US"lax", 3) == 0) + { dnssec_mode = PASS; keystring += 3; } + else if (strncmpic(keystring, US"never", 5) == 0) + { dnssec_mode = OK; keystring += 5; } + else + { + *errmsg = US"unsupported dnsdb dnssec behaviour"; + return DEFER; + } + } + else if (strncmpic(keystring, US"retrans_", 8) == 0) + { + int timeout_sec; + if ((timeout_sec = readconf_readtime(keystring += 8, ',', FALSE)) <= 0) + { + *errmsg = US"unsupported dnsdb timeout value"; + return DEFER; + } + dns_retrans = timeout_sec; + while (*keystring != ',') keystring++; + } + else if (strncmpic(keystring, US"retry_", 6) == 0) + { + int retries; + if ((retries = (int)strtol(CCS keystring + 6, CSS &keystring, 0)) < 0) + { + *errmsg = US"unsupported dnsdb retry count"; + return DEFER; + } + dns_retry = retries; + } + else + break; + + while (isspace(*keystring)) keystring++; + if (*keystring++ != ',') + { + *errmsg = US"dnsdb modifier syntax error"; + return DEFER; + } + while (isspace(*keystring)) keystring++; + } + +/* Figure out the "type" value if it is not T_TXT. +If the keystring contains an = this must be preceded by a valid type name. */ + +type = T_TXT; +if ((equals = Ustrchr(keystring, '=')) != NULL) + { + int i, len; + uschar *tend = equals; + + while (tend > keystring && isspace(tend[-1])) tend--; + len = tend - keystring; + + for (i = 0; i < nelem(type_names); i++) + if (len == Ustrlen(type_names[i]) && + strncmpic(keystring, US type_names[i], len) == 0) + { + type = type_values[i]; + break; + } + + if (i >= nelem(type_names)) + { + *errmsg = US"unsupported DNS record type"; + return DEFER; + } + + keystring = equals + 1; + while (isspace(*keystring)) keystring++; + } + +/* Initialize the resolver in case this is the first time it has been used. */ + +dns_init(FALSE, FALSE, dnssec_mode != OK); + +/* The remainder of the string must be a list of domains. As long as the lookup +for at least one of them succeeds, we return success. Failure means that none +of them were found. + +The original implementation did not support a list of domains. Adding the list +feature is compatible, except in one case: when PTR records are being looked up +for a single IPv6 address. Fortunately, we can hack in a compatibility feature +here: If the type is PTR and no list separator is specified, and the entire +remaining string is valid as an IP address, set an impossible separator so that +it is treated as one item. */ + +if (type == T_PTR && keystring[0] != '<' && + string_is_ip_address(keystring, NULL) != 0) + sep = -1; + +/* SPF strings should be concatenated without a separator, thus make +it the default if not defined (see RFC 4408 section 3.1.3). +Multiple SPF records are forbidden (section 3.1.2) but are currently +not handled specially, thus they are concatenated with \n by default. +MX priority and value are space-separated by default. +SRV and TLSA record parts are space-separated by default. */ + +if (!outsep2) switch(type) + { + case T_SPF: outsep2 = US""; break; + case T_SRV: case T_MX: case T_TLSA: outsep2 = US" "; break; + } + +/* Now scan the list and do a lookup for each item */ + +while ((domain = string_nextinlist(&keystring, &sep, NULL, 0))) + { + uschar rbuffer[256]; + int searchtype = (type == T_CSA)? T_SRV : /* record type we want */ + (type == T_MXH)? T_MX : + (type == T_ZNS)? T_NS : type; + + /* If the type is PTR or CSA, we have to construct the relevant magic lookup + key if the original is an IP address (some experimental protocols are using + PTR records for different purposes where the key string is a host name, and + Exim's extended CSA can be keyed by domains or IP addresses). This code for + doing the reversal is now in a separate function. */ + + if ((type == T_PTR || type == T_CSA) && + string_is_ip_address(domain, NULL) != 0) + { + dns_build_reverse(domain, rbuffer); + domain = rbuffer; + } + + do + { + DEBUG(D_lookup) debug_printf("dnsdb key: %s\n", domain); + + /* Do the lookup and sort out the result. There are four special types that + are handled specially: T_CSA, T_ZNS, T_ADDRESSES and T_MXH. + The first two are handled in a special lookup function so that the facility + could be used from other parts of the Exim code. T_ADDRESSES is handled by looping + over the types of A lookup. T_MXH affects only what happens later on in + this function, but for tidiness it is handled by the "special". If the + lookup fails, continue with the next domain. In the case of DEFER, adjust + the final "nothing found" result, but carry on to the next domain. */ + + found = domain; +#if HAVE_IPV6 + if (type == T_ADDRESSES) /* NB cannot happen unless HAVE_IPV6 */ + { + if (searchtype == T_ADDRESSES) searchtype = T_AAAA; + else if (searchtype == T_AAAA) searchtype = T_A; + rc = dns_special_lookup(&dnsa, domain, searchtype, CUSS &found); + } + else +#endif + rc = dns_special_lookup(&dnsa, domain, type, CUSS &found); + + lookup_dnssec_authenticated = dnssec_mode==OK ? NULL + : dns_is_secure(&dnsa) ? US"yes" : US"no"; + + if (rc == DNS_NOMATCH || rc == DNS_NODATA) continue; + if ( rc != DNS_SUCCEED + || (dnssec_mode == DEFER && !dns_is_secure(&dnsa)) + ) + { + if (defer_mode == DEFER) + { + dns_retrans = save_retrans; + dns_retry = save_retry; + dns_init(FALSE, FALSE, FALSE); /* clr dnssec bit */ + return DEFER; /* always defer */ + } + if (defer_mode == PASS) failrc = DEFER; /* defer only if all do */ + continue; /* treat defer as fail */ + } + + + /* Search the returned records */ + + for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); rr; + rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) if (rr->type == searchtype) + { + if (*do_cache > rr->ttl) + *do_cache = rr->ttl; + + if (type == T_A || type == T_AAAA || type == T_ADDRESSES) + { + dns_address *da; + for (da = dns_address_from_rr(&dnsa, rr); da; da = da->next) + { + if (yield->ptr) yield = string_catn(yield, outsep, 1); + yield = string_cat(yield, da->address); + } + continue; + } + + /* Other kinds of record just have one piece of data each, but there may be + several of them, of course. */ + + if (yield->ptr) yield = string_catn(yield, outsep, 1); + + if (type == T_TXT || type == T_SPF) + { + if (outsep2 == NULL) /* output only the first item of data */ + yield = string_catn(yield, US (rr->data+1), (rr->data)[0]); + else + { + /* output all items */ + int data_offset = 0; + while (data_offset < rr->size) + { + uschar chunk_len = (rr->data)[data_offset++]; + if (outsep2[0] != '\0' && data_offset != 1) + yield = string_catn(yield, outsep2, 1); + yield = string_catn(yield, US ((rr->data)+data_offset), chunk_len); + data_offset += chunk_len; + } + } + } + else if (type == T_TLSA) + { + uint8_t usage, selector, matching_type; + uint16_t payload_length; + uschar s[MAX_TLSA_EXPANDED_SIZE]; + uschar * sp = s; + uschar * p = US rr->data; + + usage = *p++; + selector = *p++; + matching_type = *p++; + /* What's left after removing the first 3 bytes above */ + payload_length = rr->size - 3; + sp += sprintf(CS s, "%d%c%d%c%d%c", usage, *outsep2, + selector, *outsep2, matching_type, *outsep2); + /* Now append the cert/identifier, one hex char at a time */ + while (payload_length-- > 0 && sp-s < (MAX_TLSA_EXPANDED_SIZE - 4)) + sp += sprintf(CS sp, "%02x", *p++); + + yield = string_cat(yield, s); + } + else /* T_CNAME, T_CSA, T_MX, T_MXH, T_NS, T_PTR, T_SOA, T_SRV */ + { + int priority, weight, port; + uschar s[264]; + uschar * p = US rr->data; + + switch (type) + { + case T_MXH: + /* mxh ignores the priority number and includes only the hostnames */ + GETSHORT(priority, p); + break; + + case T_MX: + GETSHORT(priority, p); + sprintf(CS s, "%d%c", priority, *outsep2); + yield = string_cat(yield, s); + break; + + case T_SRV: + GETSHORT(priority, p); + GETSHORT(weight, p); + GETSHORT(port, p); + sprintf(CS s, "%d%c%d%c%d%c", priority, *outsep2, + weight, *outsep2, port, *outsep2); + yield = string_cat(yield, s); + break; + + case T_CSA: + /* See acl_verify_csa() for more comments about CSA. */ + GETSHORT(priority, p); + GETSHORT(weight, p); + GETSHORT(port, p); + + if (priority != 1) continue; /* CSA version must be 1 */ + + /* If the CSA record we found is not the one we asked for, analyse + the subdomain assertions in the port field, else analyse the direct + authorization status in the weight field. */ + + if (Ustrcmp(found, domain) != 0) + { + if (port & 1) *s = 'X'; /* explicit authorization required */ + else *s = '?'; /* no subdomain assertions here */ + } + else + { + if (weight < 2) *s = 'N'; /* not authorized */ + else if (weight == 2) *s = 'Y'; /* authorized */ + else if (weight == 3) *s = '?'; /* unauthorizable */ + else continue; /* invalid */ + } + + s[1] = ' '; + yield = string_catn(yield, s, 2); + break; + + default: + break; + } + + /* GETSHORT() has advanced the pointer to the target domain. */ + + rc = dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p, + (DN_EXPAND_ARG4_TYPE)s, sizeof(s)); + + /* If an overlong response was received, the data will have been + truncated and dn_expand may fail. */ + + if (rc < 0) + { + log_write(0, LOG_MAIN, "host name alias list truncated: type=%s " + "domain=%s", dns_text_type(type), domain); + break; + } + else yield = string_cat(yield, s); + + if (type == T_SOA && outsep2 != NULL) + { + unsigned long serial, refresh, retry, expire, minimum; + + p += rc; + yield = string_catn(yield, outsep2, 1); + + rc = dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p, + (DN_EXPAND_ARG4_TYPE)s, sizeof(s)); + if (rc < 0) + { + log_write(0, LOG_MAIN, "responsible-mailbox truncated: type=%s " + "domain=%s", dns_text_type(type), domain); + break; + } + else yield = string_cat(yield, s); + + p += rc; + GETLONG(serial, p); GETLONG(refresh, p); + GETLONG(retry, p); GETLONG(expire, p); GETLONG(minimum, p); + sprintf(CS s, "%c%lu%c%lu%c%lu%c%lu%c%lu", + *outsep2, serial, *outsep2, refresh, + *outsep2, retry, *outsep2, expire, *outsep2, minimum); + yield = string_cat(yield, s); + } + } + } /* Loop for list of returned records */ + + /* Loop for set of A-lookup types */ + } while (type == T_ADDRESSES && searchtype != T_A); + + } /* Loop for list of domains */ + +/* Reclaim unused memory */ + +store_reset(yield->s + yield->ptr + 1); + +/* If yield NULL we have not found anything. Otherwise, insert the terminating +zero and return the result. */ + +dns_retrans = save_retrans; +dns_retry = save_retry; +dns_init(FALSE, FALSE, FALSE); /* clear the dnssec bit for getaddrbyname */ + +if (!yield || !yield->ptr) return failrc; + +*result = string_from_gstring(yield); +return OK; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +dnsdb_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: DNSDB: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"dnsdb", /* lookup name */ + lookup_querystyle, /* query style */ + dnsdb_open, /* open function */ + NULL, /* check function */ + dnsdb_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + dnsdb_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define dnsdb_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info dnsdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* vi: aw ai sw=2 +*/ +/* End of lookups/dnsdb.c */ diff --git a/src/lookups/dsearch.c b/src/lookups/dsearch.c new file mode 100644 index 0000000..9f7dd8d --- /dev/null +++ b/src/lookups/dsearch.c @@ -0,0 +1,157 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* The idea for this code came from Matthew Byng-Maddick, but his original has +been heavily reworked a lot for Exim 4 (and it now uses stat() (more precisely: +lstat()) rather than a directory scan). */ + + +#include "../exim.h" +#include "lf_functions.h" + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. We open the directory to test +whether it exists and whether it is searchable. However, we don't need to keep +it open, because the "search" can be done by a call to lstat() rather than +actually scanning through the list of files. */ + +static void * +dsearch_open(uschar *dirname, uschar **errmsg) +{ +DIR *dp = opendir(CS dirname); +if (dp == NULL) + { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s for directory search", dirname); + errno = save_errno; + return NULL; + } +closedir(dp); +return (void *)(-1); +} + + +/************************************************* +* Check entry point * +*************************************************/ + +/* The handle will always be (void *)(-1), but don't try casting it to an +integer as this gives warnings on 64-bit systems. */ + +BOOL +static dsearch_check(void *handle, uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +handle = handle; +return lf_check_file(-1, filename, S_IFDIR, modemask, owners, owngroups, + "dsearch", errmsg) == 0; +} + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. We use lstat() instead of +scanning the directory, as it is hopefully faster to let the OS do the scanning +for us. */ + +int +static dsearch_find(void *handle, uschar *dirname, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +struct stat statbuf; +int save_errno; +uschar filename[PATH_MAX]; + +handle = handle; /* Keep picky compilers happy */ +length = length; +do_cache = do_cache; + +if (Ustrchr(keystring, '/') != 0) + { + *errmsg = string_sprintf("key for dsearch lookup contains a slash: %s", + keystring); + return DEFER; + } + +if (!string_format(filename, sizeof(filename), "%s/%s", dirname, keystring)) + { + *errmsg = US"path name too long"; + return DEFER; + } + +if (Ulstat(filename, &statbuf) >= 0) + { + *result = string_copy(keystring); + return OK; + } + +if (errno == ENOENT) return FAIL; + +save_errno = errno; +*errmsg = string_sprintf("%s: lstat failed", filename); +errno = save_errno; +return DEFER; +} + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +void +static dsearch_close(void *handle) +{ +handle = handle; /* Avoid compiler warning */ +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +dsearch_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: dsearch: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"dsearch", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + dsearch_open, /* open function */ + dsearch_check, /* check function */ + dsearch_find, /* find function */ + dsearch_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + dsearch_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define dsearch_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info dsearch_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/dsearch.c */ diff --git a/src/lookups/ibase.c b/src/lookups/ibase.c new file mode 100644 index 0000000..e52ca27 --- /dev/null +++ b/src/lookups/ibase.c @@ -0,0 +1,585 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* The code in this module was contributed by Ard Biesheuvel. */ + +#include "../exim.h" +#include "lf_functions.h" + +#include <ibase.h> /* The system header */ + +/* Structure and anchor for caching connections. */ + +typedef struct ibase_connection { + struct ibase_connection *next; + uschar *server; + isc_db_handle dbh; + isc_tr_handle transh; +} ibase_connection; + +static ibase_connection *ibase_connections = NULL; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void *ibase_open(uschar * filename, uschar ** errmsg) +{ + return (void *) (1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void ibase_tidy(void) +{ + ibase_connection *cn; + ISC_STATUS status[20]; + + while ((cn = ibase_connections) != NULL) { + ibase_connections = cn->next; + DEBUG(D_lookup) debug_printf("close Interbase connection: %s\n", + cn->server); + isc_commit_transaction(status, &cn->transh); + isc_detach_database(status, &cn->dbh); + } +} + +static int fetch_field(char *buffer, int buffer_size, XSQLVAR * var) +{ + if (buffer_size < var->sqllen) + return 0; + + switch (var->sqltype & ~1) { + case SQL_VARYING: + strncpy(buffer, &var->sqldata[2], *(short *) var->sqldata); + return *(short *) var->sqldata; + case SQL_TEXT: + strncpy(buffer, var->sqldata, var->sqllen); + return var->sqllen; + case SQL_SHORT: + return sprintf(buffer, "%d", *(short *) var->sqldata); + case SQL_LONG: + return sprintf(buffer, "%ld", *(ISC_LONG *) var->sqldata); +#ifdef SQL_INT64 + case SQL_INT64: + return sprintf(buffer, "%lld", *(ISC_INT64 *) var->sqldata); +#endif + default: + /* not implemented */ + return 0; + } +} + +/************************************************* +* Internal search function * +*************************************************/ + +/* 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 + +The server string is of the form "host:dbname|user|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_ibase_search(uschar * query, uschar * server, uschar ** resultptr, + uschar ** errmsg, BOOL * defer_break) +{ +isc_stmt_handle stmth = NULL; +XSQLDA *out_sqlda; +XSQLVAR *var; + +char buffer[256]; +ISC_STATUS status[20], *statusp = status; + +gstring * result; +int i; +int yield = DEFER; +ibase_connection *cn; +uschar *server_copy = NULL; +uschar *sdata[3]; + +/* Disaggregate the parameters from the server argument. The order is host, +database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 2; i > 0; i--) + { + uschar *pp = Ustrrchr(server, '|'); + + if (pp == NULL) + { + *errmsg = string_sprintf("incomplete Interbase server data: %s", + (i == 3) ? 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 */ + +/* See if we have a cached connection to the server */ + +for (cn = ibase_connections; cn != NULL; cn = cn->next) + if (Ustrcmp(cn->server, server_copy) == 0) + break; + +/* Use a previously cached connection ? */ + +if (cn) + { + static char db_info_options[] = { isc_info_base_level }; + + /* test if the connection is alive */ + if (isc_database_info(status, &cn->dbh, sizeof(db_info_options), + db_info_options, sizeof(buffer), buffer)) + { + /* error occurred: assume connection is down */ + DEBUG(D_lookup) + debug_printf + ("Interbase cleaning up cached connection: %s\n", + cn->server); + isc_detach_database(status, &cn->dbh); + } + else + { + DEBUG(D_lookup) debug_printf("Interbase using cached connection for %s\n", + server_copy); + } + } +else + { + cn = store_get(sizeof(ibase_connection)); + cn->server = server_copy; + cn->dbh = NULL; + cn->transh = NULL; + cn->next = ibase_connections; + ibase_connections = cn; + } + +/* If no cached connection, we must set one up. */ + +if (cn->dbh == NULL || cn->transh == NULL) + { + char *dpb, *p; + short dpb_length; + static char trans_options[] = + { isc_tpb_version3, isc_tpb_read, isc_tpb_read_committed, + isc_tpb_rec_version + }; + + /* Construct the database parameter buffer. */ + dpb = buffer; + *dpb++ = isc_dpb_version1; + *dpb++ = isc_dpb_user_name; + *dpb++ = strlen(sdata[1]); + for (p = sdata[1]; *p;) + *dpb++ = *p++; + *dpb++ = isc_dpb_password; + *dpb++ = strlen(sdata[2]); + for (p = sdata[2]; *p;) + *dpb++ = *p++; + dpb_length = dpb - buffer; + + DEBUG(D_lookup) + debug_printf("new Interbase connection: database=%s user=%s\n", + sdata[0], sdata[1]); + + /* Connect to the database */ + if (isc_attach_database + (status, 0, sdata[0], &cn->dbh, dpb_length, buffer)) + { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase attach() failed: %s", buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + + /* Now start a read-only read-committed transaction */ + if (isc_start_transaction + (status, &cn->transh, 1, &cn->dbh, sizeof(trans_options), + trans_options)) + { + isc_interprete(buffer, &statusp); + isc_detach_database(status, &cn->dbh); + *errmsg = + string_sprintf("Interbase start_transaction() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + } + +/* Run the query */ +if (isc_dsql_allocate_statement(status, &cn->dbh, &stmth)) + { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase alloc_statement() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + +out_sqlda = store_get(XSQLDA_LENGTH(1)); +out_sqlda->version = SQLDA_VERSION1; +out_sqlda->sqln = 1; + +if (isc_dsql_prepare + (status, &cn->transh, &stmth, 0, query, 1, out_sqlda)) + { + isc_interprete(buffer, &statusp); + store_reset(out_sqlda); + out_sqlda = NULL; + *errmsg = + string_sprintf("Interbase prepare_statement() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + +/* re-allocate the output structure if there's more than one field */ +if (out_sqlda->sqln < out_sqlda->sqld) + { + XSQLDA *new_sqlda = store_get(XSQLDA_LENGTH(out_sqlda->sqld)); + if (isc_dsql_describe + (status, &stmth, out_sqlda->version, new_sqlda)) + { + isc_interprete(buffer, &statusp); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + store_reset(out_sqlda); + out_sqlda = NULL; + *errmsg = string_sprintf("Interbase describe_statement() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + out_sqlda = new_sqlda; + } + +/* allocate storage for every returned field */ +for (i = 0, var = out_sqlda->sqlvar; i < out_sqlda->sqld; i++, var++) + { + switch (var->sqltype & ~1) + { + case SQL_VARYING: + var->sqldata = CS store_get(sizeof(char) * var->sqllen + 2); + break; + case SQL_TEXT: + var->sqldata = CS store_get(sizeof(char) * var->sqllen); + break; + case SQL_SHORT: + var->sqldata = CS store_get(sizeof(short)); + break; + case SQL_LONG: + var->sqldata = CS store_get(sizeof(ISC_LONG)); + break; +#ifdef SQL_INT64 + case SQL_INT64: + var->sqldata = CS store_get(sizeof(ISC_INT64)); + break; +#endif + case SQL_FLOAT: + var->sqldata = CS store_get(sizeof(float)); + break; + case SQL_DOUBLE: + var->sqldata = CS store_get(sizeof(double)); + break; +#ifdef SQL_TIMESTAMP + case SQL_DATE: + var->sqldata = CS store_get(sizeof(ISC_QUAD)); + break; +#else + case SQL_TIMESTAMP: + var->sqldata = CS store_get(sizeof(ISC_TIMESTAMP)); + break; + case SQL_TYPE_DATE: + var->sqldata = CS store_get(sizeof(ISC_DATE)); + break; + case SQL_TYPE_TIME: + var->sqldata = CS store_get(sizeof(ISC_TIME)); + break; + #endif + } + if (var->sqltype & 1) + var->sqlind = (short *) store_get(sizeof(short)); + } + +/* finally, we're ready to execute the statement */ +if (isc_dsql_execute + (status, &cn->transh, &stmth, out_sqlda->version, NULL)) + { + isc_interprete(buffer, &statusp); + *errmsg = string_sprintf("Interbase describe_statement() failed: %s", + buffer); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + *defer_break = FALSE; + goto IBASE_EXIT; + } + +while (isc_dsql_fetch(status, &stmth, out_sqlda->version, out_sqlda) != 100L) + { + /* check if an error occurred */ + if (status[0] & status[1]) + { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase fetch() failed: %s", buffer); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + *defer_break = FALSE; + goto IBASE_EXIT; + } + + if (result) + result = string_catn(result, US "\n", 1); + + /* Find the number of fields returned. If this is one, we don't add field + names to the data. Otherwise we do. */ + if (out_sqlda->sqld == 1) + { + if (out_sqlda->sqlvar[0].sqlind == NULL || *out_sqlda->sqlvar[0].sqlind != -1) /* NULL value yields nothing */ + result = string_catn(result, US buffer, + fetch_field(buffer, sizeof(buffer), + &out_sqlda->sqlvar[0])); + } + + else + for (i = 0; i < out_sqlda->sqld; i++) + { + int len = fetch_field(buffer, sizeof(buffer), &out_sqlda->sqlvar[i]); + + result = string_catn(result, US out_sqlda->sqlvar[i].aliasname, + out_sqlda->sqlvar[i].aliasname_length); + result = string_catn(result, US "=", 1); + + /* Quote the value if it contains spaces or is empty */ + + if (*out_sqlda->sqlvar[i].sqlind == -1) /* NULL value */ + result = string_catn(result, US "\"\"", 2); + + else if (buffer[0] == 0 || Ustrchr(buffer, ' ') != NULL) + { + int j; + + result = string_catn(result, US "\"", 1); + for (j = 0; j < len; j++) + { + if (buffer[j] == '\"' || buffer[j] == '\\') + result = string_cat(result, US "\\", 1); + result = string_cat(result, US buffer + j, 1); + } + result = string_catn(result, US "\"", 1); + } + else + result = string_catn(result, US buffer, len); + result = string_catn(result, US " ", 1); + } + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + +if (!result) + { + yield = FAIL; + *errmsg = US "Interbase: no data found"; + } +else + store_reset(result->s + result->ptr + 1); + + +/* Get here by goto from various error checks. */ + +IBASE_EXIT: + +if (stmth) + isc_dsql_free_statement(status, &stmth, DSQL_drop); + +/* Non-NULL result indicates a successful result */ + +if (result) + { + *resultptr = string_from_gstring(result); + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return yield; /* FAIL or DEFER */ + } +} + + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The handle and filename +arguments are not used. Loop through a list of servers while the query is +deferred with a retryable error. */ + +static int +ibase_find(void *handle, uschar * filename, uschar * query, int length, + uschar ** result, uschar ** errmsg, uint *do_cache) +{ + int sep = 0; + uschar *server; + uschar *list = ibase_servers; + uschar buffer[512]; + + /* Keep picky compilers happy */ + do_cache = do_cache; + + DEBUG(D_lookup) debug_printf("Interbase query: %s\n", query); + + while ((server = + string_nextinlist(&list, &sep, buffer, + sizeof(buffer))) != NULL) { + BOOL defer_break = FALSE; + int rc = perform_ibase_search(query, server, result, errmsg, + &defer_break); + if (rc != DEFER || defer_break) + return rc; + } + + if (ibase_servers == NULL) + *errmsg = US "no Interbase servers defined (ibase_servers option)"; + + return DEFER; +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only characters that need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent, and underscore and not escaped. They are only special in contexts +where they can be wild cards, and this isn't usually the case for data inserted +from messages, since that isn't likely to be treated as a pattern of any kind. +Sadly, MySQL doesn't seem to behave like other programs. If you use something +like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really +can't quote "on spec". + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar *ibase_quote(uschar * s, uschar * opt) +{ + register int c; + int count = 0; + uschar *t = s; + uschar *quoted; + + if (opt != NULL) + return NULL; /* No options recognized */ + + while ((c = *t++) != 0) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) + count++; + + if (count == 0) + return s; + t = quoted = store_get(Ustrlen(s) + count + 1); + + while ((c = *s++) != 0) { + if (Ustrchr("'", c) != NULL) { + *t++ = '\''; + *t++ = '\''; +/* switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + }*/ + } else + *t++ = c; + } + + *t = 0; + return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +ibase_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: ibase: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"ibase", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + ibase_open, /* open function */ + NULL, /* no check function */ + ibase_find, /* find function */ + NULL, /* no close function */ + ibase_tidy, /* tidy function */ + ibase_quote, /* quoting function */ + ibase_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define ibase_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info ibase_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/ibase.c */ diff --git a/src/lookups/ldap.c b/src/lookups/ldap.c new file mode 100644 index 0000000..63c0edf --- /dev/null +++ b/src/lookups/ldap.c @@ -0,0 +1,1640 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Many thanks to Stuart Lynne for contributing the original code for this +driver. Further contributions from Michael Haardt, Brian Candler, Barry +Pederson, Peter Savitch and Christian Kellner. Particular thanks to Brian for +researching how to handle the different kinds of error. */ + + +#include "../exim.h" +#include "lf_functions.h" + + +/* Include LDAP headers. The code below uses some "old" LDAP interfaces that +are deprecated in OpenLDAP. I don't know their status in other LDAP +implementations. LDAP_DEPRECATED causes their prototypes to be defined in +ldap.h. */ + +#define LDAP_DEPRECATED 1 + +#include <lber.h> +#include <ldap.h> + + +/* Annoyingly, the different LDAP libraries handle errors in different ways, +and some other things too. There doesn't seem to be an automatic way of +distinguishing between them. Local/Makefile should contain a setting of +LDAP_LIB_TYPE, which in turn causes appropriate macros to be defined for the +different kinds. Those that matter are: + +LDAP_LIB_NETSCAPE +LDAP_LIB_SOLARIS with synonym LDAP_LIB_SOLARIS7 +LDAP_LIB_OPENLDAP2 + +These others may be defined, but are in fact the default, so are not tested: + +LDAP_LIB_UMICHIGAN +LDAP_LIB_OPENLDAP1 +*/ + +#if defined(LDAP_LIB_SOLARIS7) && ! defined(LDAP_LIB_SOLARIS) +#define LDAP_LIB_SOLARIS +#endif + + +/* Just in case LDAP_NO_LIMIT is not defined by some of these libraries. */ + +#ifndef LDAP_NO_LIMIT +#define LDAP_NO_LIMIT 0 +#endif + + +/* Just in case LDAP_DEREF_NEVER is not defined */ + +#ifndef LDAP_DEREF_NEVER +#define LDAP_DEREF_NEVER 0 +#endif + + +/* Four types of LDAP search are implemented */ + +#define SEARCH_LDAP_MULTIPLE 0 /* Get attributes from multiple entries */ +#define SEARCH_LDAP_SINGLE 1 /* Get attributes from one entry only */ +#define SEARCH_LDAP_DN 2 /* Get just the DN from one entry */ +#define SEARCH_LDAP_AUTH 3 /* Just checking for authentication */ + +/* In all 4 cases, the DN is left in $ldap_dn (which post-dates the +SEARCH_LDAP_DN lookup). */ + + +/* Structure and anchor for caching connections. */ + +typedef struct ldap_connection { + struct ldap_connection *next; + uschar *host; + uschar *user; + uschar *password; + BOOL bound; + int port; + BOOL is_start_tls_called; + LDAP *ld; +} LDAP_CONNECTION; + +static LDAP_CONNECTION *ldap_connections = NULL; + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* This is the function that actually does the work. It is called (indirectly +via control_ldap_search) from eldap_find(), eldapauth_find(), eldapdn_find(), +and eldapm_find(), with a difference in the "search_type" argument. + +The case of eldapauth_find() is special in that all it does is do +authentication, returning OK or FAIL as appropriate. This isn't used as a +lookup. Instead, it is called from expand.c as an expansion condition test. + +The DN from a successful lookup is placed in $ldap_dn. This feature postdates +the provision of the SEARCH_LDAP_DN facility for returning just the DN as the +data. + +Arguments: + ldap_url the URL to be looked up + server server host name, when URL contains none + s_port server port, used when URL contains no name + search_type SEARCH_LDAP_MULTIPLE allows values from multiple entries + SEARCH_LDAP_SINGLE allows values from one entry only + SEARCH_LDAP_DN gets the DN from one entry + res set to point at the result (not used for ldapauth) + errmsg set to point a message if result is not OK + defer_break set TRUE if no more servers to be tried after a DEFER + user user name for authentication, or NULL + password password for authentication, or NULL + sizelimit max number of entries returned, or 0 for no limit + timelimit max time to wait, or 0 for no limit + tcplimit max time for network activity, e.g. connect, or 0 for OS default + deference the dereference option, which is one of + LDAP_DEREF_{NEVER,SEARCHING,FINDING,ALWAYS} + referrals the referral option, which is LDAP_OPT_ON or LDAP_OPT_OFF + +Returns: OK or FAIL or DEFER + FAIL is given only if a lookup was performed successfully, but + returned no data. +*/ + +static int +perform_ldap_search(const uschar *ldap_url, uschar *server, int s_port, + int search_type, uschar **res, uschar **errmsg, BOOL *defer_break, + uschar *user, uschar *password, int sizelimit, int timelimit, int tcplimit, + int dereference, void *referrals) +{ +LDAPURLDesc *ludp = NULL; +LDAPMessage *result = NULL; +BerElement *ber; +LDAP_CONNECTION *lcp; + +struct timeval timeout; +struct timeval *timeoutptr = NULL; + +uschar *attr; +uschar **attrp; +gstring * data = NULL; +uschar *dn = NULL; +uschar *host; +uschar **values; +uschar **firstval; +uschar porttext[16]; + +uschar *error1 = NULL; /* string representation of errcode (static) */ +uschar *error2 = NULL; /* error message from the server */ +uschar *matched = NULL; /* partially matched DN */ + +int attrs_requested = 0; +int error_yield = DEFER; +int msgid; +int rc, ldap_rc, ldap_parse_rc; +int port; +int rescount = 0; +BOOL attribute_found = FALSE; +BOOL ldapi = FALSE; + +DEBUG(D_lookup) + debug_printf("perform_ldap_search: ldap%s URL = \"%s\" server=%s port=%d " + "sizelimit=%d timelimit=%d tcplimit=%d\n", + search_type == SEARCH_LDAP_MULTIPLE ? "m" : + search_type == SEARCH_LDAP_DN ? "dn" : + search_type == SEARCH_LDAP_AUTH ? "auth" : "", + ldap_url, server, s_port, sizelimit, timelimit, tcplimit); + +/* Check if LDAP thinks the URL is a valid LDAP URL. We assume that if the LDAP +library that is in use doesn't recognize, say, "ldapi", it will barf here. */ + +if (!ldap_is_ldap_url(CS ldap_url)) + { + *errmsg = string_sprintf("ldap_is_ldap_url: not an LDAP url \"%s\"\n", + ldap_url); + goto RETURN_ERROR_BREAK; + } + +/* Parse the URL */ + +if ((rc = ldap_url_parse(CS ldap_url, &ludp)) != 0) + { + *errmsg = string_sprintf("ldap_url_parse: (error %d) parsing \"%s\"\n", rc, + ldap_url); + goto RETURN_ERROR_BREAK; + } + +/* If the host name is empty, take it from the separate argument, if one is +given. OpenLDAP 2.0.6 sets an unset hostname to "" rather than empty, but +expects NULL later in ldap_init() to mean "default", annoyingly. In OpenLDAP +2.0.11 this has changed (it uses NULL). */ + +if ((!ludp->lud_host || !ludp->lud_host[0]) && server) + { + host = server; + port = s_port; + } +else + { + host = US ludp->lud_host; + if (host && !host[0]) host = NULL; + port = ludp->lud_port; + } + +DEBUG(D_lookup) debug_printf("after ldap_url_parse: host=%s port=%d\n", + host, port); + +if (port == 0) port = LDAP_PORT; /* Default if none given */ +sprintf(CS porttext, ":%d", port); /* For messages */ + +/* If the "host name" is actually a path, we are going to connect using a Unix +socket, regardless of whether "ldapi" was actually specified or not. This means +that a Unix socket can be declared in eldap_default_servers, and "traditional" +LDAP queries using just "ldap" can be used ("ldaps" is similarly overridden). +The path may start with "/" or it may already be escaped as "%2F" if it was +actually declared that way in eldap_default_servers. (I did it that way the +first time.) If the host name is not a path, the use of "ldapi" causes an +error, except in the default case. (But lud_scheme doesn't seem to exist in +older libraries.) */ + +if (host) + { + if ((host[0] == '/' || Ustrncmp(host, "%2F", 3) == 0)) + { + ldapi = TRUE; + porttext[0] = 0; /* Remove port from messages */ + } + +#if defined LDAP_LIB_OPENLDAP2 + else if (strncmp(ludp->lud_scheme, "ldapi", 5) == 0) + { + *errmsg = string_sprintf("ldapi requires an absolute path (\"%s\" given)", + host); + goto RETURN_ERROR; + } +#endif + } + +/* Count the attributes; we need this later to tell us how to format results */ + +for (attrp = USS ludp->lud_attrs; attrp && *attrp; attrp++) + attrs_requested++; + +/* See if we can find a cached connection to this host. The port is not +relevant for ldapi. The host name pointer is set to NULL if no host was given +(implying the library default), rather than to the empty string. Note that in +this case, there is no difference between ldap and ldapi. */ + +for (lcp = ldap_connections; lcp; lcp = lcp->next) + { + if ((host == NULL) != (lcp->host == NULL) || + (host != NULL && strcmpic(lcp->host, host) != 0)) + continue; + if (ldapi || port == lcp->port) break; + } + +/* Use this network timeout in any requests. */ + +if (tcplimit > 0) + { + timeout.tv_sec = tcplimit; + timeout.tv_usec = 0; + timeoutptr = &timeout; + } + +/* If no cached connection found, we must open a connection to the server. If +the server name is actually an absolute path, we set ldapi=TRUE above. This +requests connection via a Unix socket. However, as far as I know, only OpenLDAP +supports the use of sockets, and the use of ldap_initialize(). */ + +if (!lcp) + { + LDAP *ld; + +#ifdef LDAP_OPT_X_TLS_NEWCTX + int am_server = 0; + LDAP *ldsetctx; +#else + LDAP *ldsetctx = NULL; +#endif + + + /* --------------------------- OpenLDAP ------------------------ */ + + /* There seems to be a preference under OpenLDAP for ldap_initialize() + instead of ldap_init(), though I have as yet been unable to find + documentation that says this. (OpenLDAP documentation is sparse to + non-existent). So we handle OpenLDAP differently here. Also, support for + ldapi seems to be OpenLDAP-only at present. */ + +#ifdef LDAP_LIB_OPENLDAP2 + + /* We now need an empty string for the default host. Get some store in which + to build a URL for ldap_initialize(). In the ldapi case, it can't be bigger + than (9 + 3*Ustrlen(shost)), whereas in the other cases it can't be bigger + than the host name + "ldaps:///" plus : and a port number, say 20 + the + length of the host name. What we get should accommodate both, easily. */ + + uschar *shost = (host == NULL)? US"" : host; + uschar *init_url = store_get(20 + 3 * Ustrlen(shost)); + uschar *init_ptr; + + /* Handle connection via Unix socket ("ldapi"). We build a basic LDAP URI to + contain the path name, with slashes escaped as %2F. */ + + if (ldapi) + { + int ch; + init_ptr = init_url + 8; + Ustrcpy(init_url, "ldapi://"); + while ((ch = *shost++)) + if (ch == '/') + { Ustrncpy(init_ptr, "%2F", 3); init_ptr += 3; } + else + *init_ptr++ = ch; + *init_ptr = 0; + } + + /* This is not an ldapi call. Just build a URI with the protocol type, host + name, and port. */ + + else + { + init_ptr = Ustrchr(ldap_url, '/'); + Ustrncpy(init_url, ldap_url, init_ptr - ldap_url); + init_ptr = init_url + (init_ptr - ldap_url); + sprintf(CS init_ptr, "//%s:%d/", shost, port); + } + + /* Call ldap_initialize() and check the result */ + + DEBUG(D_lookup) debug_printf("ldap_initialize with URL %s\n", init_url); + if ((rc = ldap_initialize(&ld, CS init_url)) != LDAP_SUCCESS) + { + *errmsg = string_sprintf("ldap_initialize: (error %d) URL \"%s\"\n", + rc, init_url); + goto RETURN_ERROR; + } + store_reset(init_url); /* Might as well save memory when we can */ + + + /* ------------------------- Not OpenLDAP ---------------------- */ + + /* For libraries other than OpenLDAP, use ldap_init(). */ + +#else /* LDAP_LIB_OPENLDAP2 */ + ld = ldap_init(CS host, port); +#endif /* LDAP_LIB_OPENLDAP2 */ + + /* -------------------------------------------------------------- */ + + + /* Handle failure to initialize */ + + if (!ld) + { + *errmsg = string_sprintf("failed to initialize for LDAP server %s%s - %s", + host, porttext, strerror(errno)); + goto RETURN_ERROR; + } + +#ifdef LDAP_OPT_X_TLS_NEWCTX + ldsetctx = ld; +#endif + + /* Set the TCP connect time limit if available. This is something that is + in Netscape SDK v4.1; I don't know about other libraries. */ + +#ifdef LDAP_X_OPT_CONNECT_TIMEOUT + if (tcplimit > 0) + { + int timeout1000 = tcplimit*1000; + ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT, (void *)&timeout1000); + } + else + { + int notimeout = LDAP_X_IO_TIMEOUT_NO_TIMEOUT; + ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT, (void *)¬imeout); + } +#endif + + /* Set the TCP connect timeout. This works with OpenLDAP 2.2.14. */ + +#ifdef LDAP_OPT_NETWORK_TIMEOUT + if (tcplimit > 0) + ldap_set_option(ld, LDAP_OPT_NETWORK_TIMEOUT, (void *)timeoutptr); +#endif + + /* I could not get TLS to work until I set the version to 3. That version + seems to be the default nowadays. The RFC is dated 1997, so I would hope + that all the LDAP libraries support it. Therefore, if eldap_version hasn't + been set, go for v3 if we can. */ + + if (eldap_version < 0) + { +#ifdef LDAP_VERSION3 + eldap_version = LDAP_VERSION3; +#else + eldap_version = 2; +#endif + } + +#ifdef LDAP_OPT_PROTOCOL_VERSION + ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, (void *)&eldap_version); +#endif + + DEBUG(D_lookup) debug_printf("initialized for LDAP (v%d) server %s%s\n", + eldap_version, host, porttext); + + /* If not using ldapi and TLS is available, set appropriate TLS options: hard + for "ldaps" and soft otherwise. */ + +#ifdef LDAP_OPT_X_TLS + if (!ldapi) + { + int tls_option; +# ifdef LDAP_OPT_X_TLS_REQUIRE_CERT + if (eldap_require_cert) + { + tls_option = + Ustrcmp(eldap_require_cert, "hard") == 0 ? LDAP_OPT_X_TLS_HARD + : Ustrcmp(eldap_require_cert, "demand") == 0 ? LDAP_OPT_X_TLS_DEMAND + : Ustrcmp(eldap_require_cert, "allow") == 0 ? LDAP_OPT_X_TLS_ALLOW + : Ustrcmp(eldap_require_cert, "try") == 0 ? LDAP_OPT_X_TLS_TRY + : LDAP_OPT_X_TLS_NEVER; + + DEBUG(D_lookup) + debug_printf("Require certificate overrides LDAP_OPT_X_TLS option (%d)\n", + tls_option); + } + else +# endif /* LDAP_OPT_X_TLS_REQUIRE_CERT */ + if (strncmp(ludp->lud_scheme, "ldaps", 5) == 0) + { + tls_option = LDAP_OPT_X_TLS_HARD; + DEBUG(D_lookup) + debug_printf("LDAP_OPT_X_TLS_HARD set due to ldaps:// URI\n"); + } + else + { + tls_option = LDAP_OPT_X_TLS_TRY; + DEBUG(D_lookup) + debug_printf("LDAP_OPT_X_TLS_TRY set due to ldap:// URI\n"); + } + ldap_set_option(ld, LDAP_OPT_X_TLS, (void *)&tls_option); + } +#endif /* LDAP_OPT_X_TLS */ + +#ifdef LDAP_OPT_X_TLS_CACERTFILE + if (eldap_ca_cert_file) + ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CACERTFILE, eldap_ca_cert_file); +#endif +#ifdef LDAP_OPT_X_TLS_CACERTDIR + if (eldap_ca_cert_dir) + ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CACERTDIR, eldap_ca_cert_dir); +#endif +#ifdef LDAP_OPT_X_TLS_CERTFILE + if (eldap_cert_file) + ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CERTFILE, eldap_cert_file); +#endif +#ifdef LDAP_OPT_X_TLS_KEYFILE + if (eldap_cert_key) + ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_KEYFILE, eldap_cert_key); +#endif +#ifdef LDAP_OPT_X_TLS_CIPHER_SUITE + if (eldap_cipher_suite) + ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CIPHER_SUITE, eldap_cipher_suite); +#endif +#ifdef LDAP_OPT_X_TLS_REQUIRE_CERT + if (eldap_require_cert) + { + int cert_option = + Ustrcmp(eldap_require_cert, "hard") == 0 ? LDAP_OPT_X_TLS_HARD + : Ustrcmp(eldap_require_cert, "demand") == 0 ? LDAP_OPT_X_TLS_DEMAND + : Ustrcmp(eldap_require_cert, "allow") == 0 ? LDAP_OPT_X_TLS_ALLOW + : Ustrcmp(eldap_require_cert, "try") == 0 ? LDAP_OPT_X_TLS_TRY + : LDAP_OPT_X_TLS_NEVER; + + /* This ldap handle is set at compile time based on client libs. Older + * versions want it to be global and newer versions can force a reload + * of the TLS context (to reload these settings we are changing from the + * default that loaded at instantiation). */ + rc = ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_REQUIRE_CERT, &cert_option); + if (rc) + DEBUG(D_lookup) + debug_printf("Unable to set TLS require cert_option(%d) globally: %s\n", + cert_option, ldap_err2string(rc)); + } +#endif +#ifdef LDAP_OPT_X_TLS_NEWCTX + if ((rc = ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_NEWCTX, &am_server))) + DEBUG(D_lookup) + debug_printf("Unable to reload TLS context %d: %s\n", + rc, ldap_err2string(rc)); + #endif + + /* Now add this connection to the chain of cached connections */ + + lcp = store_get(sizeof(LDAP_CONNECTION)); + lcp->host = (host == NULL)? NULL : string_copy(host); + lcp->bound = FALSE; + lcp->user = NULL; + lcp->password = NULL; + lcp->port = port; + lcp->ld = ld; + lcp->next = ldap_connections; + lcp->is_start_tls_called = FALSE; + ldap_connections = lcp; + } + +/* Found cached connection */ + +else + DEBUG(D_lookup) + debug_printf("re-using cached connection to LDAP server %s%s\n", + host, porttext); + +/* Bind with the user/password supplied, or an anonymous bind if these values +are NULL, unless a cached connection is already bound with the same values. */ + +if ( !lcp->bound + || !lcp->user && user + || lcp->user && !user + || lcp->user && user && Ustrcmp(lcp->user, user) != 0 + || !lcp->password && password + || lcp->password && !password + || lcp->password && password && Ustrcmp(lcp->password, password) != 0 + ) + { + DEBUG(D_lookup) debug_printf("%sbinding with user=%s password=%s\n", + lcp->bound ? "re-" : "", user, password); + + if (eldap_start_tls && !lcp->is_start_tls_called && !ldapi) + { +#if defined(LDAP_OPT_X_TLS) && !defined(LDAP_LIB_SOLARIS) + /* The Oracle LDAP libraries (LDAP_LIB_TYPE=SOLARIS) don't support this. + * Note: moreover, they appear to now define LDAP_OPT_X_TLS and still not + * export an ldap_start_tls_s symbol. + */ + if ( (rc = ldap_start_tls_s(lcp->ld, NULL, NULL)) != LDAP_SUCCESS) + { + *errmsg = string_sprintf("failed to initiate TLS processing on an " + "LDAP session to server %s%s - ldap_start_tls_s() returned %d:" + " %s", host, porttext, rc, ldap_err2string(rc)); + goto RETURN_ERROR; + } + lcp->is_start_tls_called = TRUE; +#else + DEBUG(D_lookup) debug_printf("TLS initiation not supported with this Exim" + " and your LDAP library.\n"); +#endif + } + if ((msgid = ldap_bind(lcp->ld, CS user, CS password, LDAP_AUTH_SIMPLE)) + == -1) + { + *errmsg = string_sprintf("failed to bind the LDAP connection to server " + "%s%s - ldap_bind() returned -1", host, porttext); + goto RETURN_ERROR; + } + + if ((rc = ldap_result(lcp->ld, msgid, 1, timeoutptr, &result)) <= 0) + { + *errmsg = string_sprintf("failed to bind the LDAP connection to server " + "%s%s - LDAP error: %s", host, porttext, + rc == -1 ? "result retrieval failed" : "timeout" ); + result = NULL; + goto RETURN_ERROR; + } + + rc = ldap_result2error(lcp->ld, result, 0); + + /* Invalid credentials when just checking credentials returns FAIL. This + stops any further servers being tried. */ + + if (search_type == SEARCH_LDAP_AUTH && rc == LDAP_INVALID_CREDENTIALS) + { + DEBUG(D_lookup) + debug_printf("Invalid credentials: ldapauth returns FAIL\n"); + error_yield = FAIL; + goto RETURN_ERROR_NOMSG; + } + + /* Otherwise we have a problem that doesn't stop further servers from being + tried. */ + + if (rc != LDAP_SUCCESS) + { + *errmsg = string_sprintf("failed to bind the LDAP connection to server " + "%s%s - LDAP error %d: %s", host, porttext, rc, ldap_err2string(rc)); + goto RETURN_ERROR; + } + + /* Successful bind */ + + lcp->bound = TRUE; + lcp->user = !user ? NULL : string_copy(user); + lcp->password = !password ? NULL : string_copy(password); + + ldap_msgfree(result); + result = NULL; + } + +/* If we are just checking credentials, return OK. */ + +if (search_type == SEARCH_LDAP_AUTH) + { + DEBUG(D_lookup) debug_printf("Bind succeeded: ldapauth returns OK\n"); + goto RETURN_OK; + } + +/* Before doing the search, set the time and size limits (if given). Here again +the different implementations of LDAP have chosen to do things differently. */ + +#if defined(LDAP_OPT_SIZELIMIT) +ldap_set_option(lcp->ld, LDAP_OPT_SIZELIMIT, (void *)&sizelimit); +ldap_set_option(lcp->ld, LDAP_OPT_TIMELIMIT, (void *)&timelimit); +#else +lcp->ld->ld_sizelimit = sizelimit; +lcp->ld->ld_timelimit = timelimit; +#endif + +/* Similarly for dereferencing aliases. Don't know if this is possible on +an LDAP library without LDAP_OPT_DEREF. */ + +#if defined(LDAP_OPT_DEREF) +ldap_set_option(lcp->ld, LDAP_OPT_DEREF, (void *)&dereference); +#endif + +/* Similarly for the referral setting; should the library follow referrals that +the LDAP server returns? The conditional is just in case someone uses a library +without it. */ + +#if defined(LDAP_OPT_REFERRALS) +ldap_set_option(lcp->ld, LDAP_OPT_REFERRALS, referrals); +#endif + +/* Start the search on the server. */ + +DEBUG(D_lookup) debug_printf("Start search\n"); + +msgid = ldap_search(lcp->ld, ludp->lud_dn, ludp->lud_scope, ludp->lud_filter, + ludp->lud_attrs, 0); + +if (msgid == -1) + { +#if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2 + int err; + ldap_get_option(lcp->ld, LDAP_OPT_ERROR_NUMBER, &err); + *errmsg = string_sprintf("ldap_search failed: %d, %s", err, + ldap_err2string(err)); +#else + *errmsg = string_sprintf("ldap_search failed"); +#endif + + goto RETURN_ERROR; + } + +/* Loop to pick up results as they come in, setting a timeout if one was +given. */ + +while ((rc = ldap_result(lcp->ld, msgid, 0, timeoutptr, &result)) == + LDAP_RES_SEARCH_ENTRY) + { + LDAPMessage *e; + int valuecount; /* We can see an attr spread across several + entries. If B is derived from A and we request + A and the directory contains both, A and B, + then we get two entries, one for A and one for B. + Here we just count the values per entry */ + + DEBUG(D_lookup) debug_printf("LDAP result loop\n"); + + for(e = ldap_first_entry(lcp->ld, result), valuecount = 0; + e; + e = ldap_next_entry(lcp->ld, e)) + { + uschar *new_dn; + BOOL insert_space = FALSE; + + DEBUG(D_lookup) debug_printf("LDAP entry loop\n"); + + rescount++; /* Count results */ + + /* Results for multiple entries values are separated by newlines. */ + + if (data) data = string_catn(data, US"\n", 1); + + /* Get the DN from the last result. */ + + if ((new_dn = US ldap_get_dn(lcp->ld, e))) + { + if (dn) + { +#if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2 + ldap_memfree(dn); +#else /* OPENLDAP 1, UMich, Solaris */ + free(dn); +#endif + } + /* Save for later */ + dn = new_dn; + } + + /* If the data we want is actually the DN rather than any attribute values, + (an "ldapdn" search) add it to the data string. If there are multiple + entries, the DNs will be concatenated, but we test for this case below, as + for SEARCH_LDAP_SINGLE, and give an error. */ + + if (search_type == SEARCH_LDAP_DN) /* Do not amalgamate these into one */ + { /* condition, because of the else */ + if (new_dn) /* below, that's for the first only */ + { + data = string_cat(data, new_dn); + (void) string_from_gstring(data); + attribute_found = TRUE; + } + } + + /* Otherwise, loop through the entry, grabbing attribute values. If there's + only one attribute being retrieved, no attribute name is given, and the + result is not quoted. Multiple values are separated by (comma). + If more than one attribute is being retrieved, the data is given as a + sequence of name=value pairs, separated by (space), with the value always in quotes. + If there are multiple values, they are given within the quotes, comma separated. */ + + else for (attr = US ldap_first_attribute(lcp->ld, e, &ber); + attr; attr = US ldap_next_attribute(lcp->ld, e, ber)) + { + DEBUG(D_lookup) debug_printf("LDAP attr loop\n"); + + /* In case of attrs_requested == 1 we just count the values, in all other cases + (0, >1) we count the values per attribute */ + if (attrs_requested != 1) valuecount = 0; + + if (attr[0] != 0) + { + /* Get array of values for this attribute. */ + + if ((firstval = values = USS ldap_get_values(lcp->ld, e, CS attr))) + { + if (attrs_requested != 1) + { + if (insert_space) + data = string_catn(data, US" ", 1); + else + insert_space = TRUE; + data = string_cat(data, attr); + data = string_catn(data, US"=\"", 2); + } + + while (*values) + { + uschar *value = *values; + int len = Ustrlen(value); + ++valuecount; + + DEBUG(D_lookup) debug_printf("LDAP value loop %s:%s\n", attr, value); + + /* In case we requested one attribute only but got several times + into that attr loop, we need to append the additional values. + (This may happen if you derive attributeTypes B and C from A and + then query for A.) In all other cases we detect the different + attribute and append only every non first value. */ + + if (data && valuecount > 1) + data = string_catn(data, US",", 1); + + /* For multiple attributes, the data is in quotes. We must escape + internal quotes, backslashes, newlines, and must double commas. */ + + if (attrs_requested != 1) + { + int j; + for (j = 0; j < len; j++) + { + if (value[j] == '\n') + data = string_catn(data, US"\\n", 2); + else if (value[j] == ',') + data = string_catn(data, US",,", 2); + else + { + if (value[j] == '\"' || value[j] == '\\') + data = string_catn(data, US"\\", 1); + data = string_catn(data, value+j, 1); + } + } + } + + /* For single attributes, just double commas */ + + else + { + int j; + for (j = 0; j < len; j++) + if (value[j] == ',') + data = string_catn(data, US",,", 2); + else + data = string_catn(data, value+j, 1); + } + + + /* Move on to the next value */ + + values++; + attribute_found = TRUE; + } + + /* Closing quote at the end of the data for a named attribute. */ + + if (attrs_requested != 1) + data = string_catn(data, US"\"", 1); + + /* Free the values */ + + ldap_value_free(CSS firstval); + } + } + +#if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2 + + /* Netscape and OpenLDAP2 LDAP's attrs are dynamically allocated and need + to be freed. UMich LDAP stores them in static storage and does not require + this. */ + + ldap_memfree(attr); +#endif + } /* End "for" loop for extracting attributes from an entry */ + } /* End "for" loop for extracting entries from a result */ + + /* Free the result */ + + ldap_msgfree(result); + result = NULL; + } /* End "while" loop for multiple results */ + +/* Terminate the dynamic string that we have built and reclaim unused store. +In the odd case of a single attribute with zero-length value, allocate +an empty string. */ + +if (!data) data = string_get(1); +(void) string_from_gstring(data); +gstring_reset_unused(data); + +/* Copy the last dn into eldap_dn */ + +if (dn) + { + eldap_dn = string_copy(dn); +#if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2 + ldap_memfree(dn); +#else /* OPENLDAP 1, UMich, Solaris */ + free(dn); +#endif + } + +DEBUG(D_lookup) debug_printf("search ended by ldap_result yielding %d\n",rc); + +if (rc == 0) + { + *errmsg = US"ldap_result timed out"; + goto RETURN_ERROR; + } + +/* A return code of -1 seems to mean "ldap_result failed internally or couldn't +provide you with a message". Other error states seem to exist where +ldap_result() didn't give us any message from the server at all, leaving result +set to NULL. Apparently, "the error parameters of the LDAP session handle will +be set accordingly". That's the best we can do to retrieve an error status; we +can't use functions like ldap_result2error because they parse a message from +the server, which we didn't get. + +Annoyingly, the different implementations of LDAP have gone for different +methods of handling error codes and generating error messages. */ + +if (rc == -1 || !result) + { + int err; + DEBUG(D_lookup) debug_printf("ldap_result failed\n"); + +#if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2 + ldap_get_option(lcp->ld, LDAP_OPT_ERROR_NUMBER, &err); + *errmsg = string_sprintf("ldap_result failed: %d, %s", + err, ldap_err2string(err)); + +#elif defined LDAP_LIB_NETSCAPE + /* Dubious (surely 'matched' is spurious here?) */ + (void)ldap_get_lderrno(lcp->ld, &matched, &error1); + *errmsg = string_sprintf("ldap_result failed: %s (%s)", error1, matched); + +#else /* UMich LDAP aka OpenLDAP 1.x */ + *errmsg = string_sprintf("ldap_result failed: %d, %s", + lcp->ld->ld_errno, ldap_err2string(lcp->ld->ld_errno)); +#endif + + goto RETURN_ERROR; + } + +/* A return code that isn't -1 doesn't necessarily mean there were no problems +with the search. The message must be an LDAP_RES_SEARCH_RESULT or +LDAP_RES_SEARCH_REFERENCE or else it's something we can't handle. Some versions +of LDAP do not define LDAP_RES_SEARCH_REFERENCE (LDAP v1 is one, it seems). So +we don't provide that functionality when we can't. :-) */ + +if (rc != LDAP_RES_SEARCH_RESULT +#ifdef LDAP_RES_SEARCH_REFERENCE + && rc != LDAP_RES_SEARCH_REFERENCE +#endif + ) + { + *errmsg = string_sprintf("ldap_result returned unexpected code %d", rc); + goto RETURN_ERROR; + } + +/* We have a result message from the server. This doesn't yet mean all is well. +We need to parse the message to find out exactly what's happened. */ + +#if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2 + ldap_rc = rc; + ldap_parse_rc = ldap_parse_result(lcp->ld, result, &rc, CSS &matched, + CSS &error2, NULL, NULL, 0); + DEBUG(D_lookup) debug_printf("ldap_parse_result: %d\n", ldap_parse_rc); + if (ldap_parse_rc < 0 && + (ldap_parse_rc != LDAP_NO_RESULTS_RETURNED + #ifdef LDAP_RES_SEARCH_REFERENCE + || ldap_rc != LDAP_RES_SEARCH_REFERENCE + #endif + )) + { + *errmsg = string_sprintf("ldap_parse_result failed %d", ldap_parse_rc); + goto RETURN_ERROR; + } + error1 = US ldap_err2string(rc); + +#elif defined LDAP_LIB_NETSCAPE + /* Dubious (it doesn't reference 'result' at all!) */ + rc = ldap_get_lderrno(lcp->ld, &matched, &error1); + +#else /* UMich LDAP aka OpenLDAP 1.x */ + rc = ldap_result2error(lcp->ld, result, 0); + error1 = ldap_err2string(rc); + error2 = lcp->ld->ld_error; + matched = lcp->ld->ld_matched; +#endif + +/* Process the status as follows: + + (1) If we get LDAP_SIZELIMIT_EXCEEDED, just carry on, to return the + truncated result list. + + (2) If we get LDAP_RES_SEARCH_REFERENCE, also just carry on. This was a + submitted patch that is reported to "do the right thing" with Solaris + LDAP libraries. (The problem it addresses apparently does not occur with + Open LDAP.) + + (3) The range of errors defined by LDAP_NAME_ERROR generally mean "that + object does not, or cannot, exist in the database". For those cases we + fail the lookup. + + (4) All other non-successes here are treated as some kind of problem with + the lookup, so return DEFER (which is the default in error_yield). +*/ + +DEBUG(D_lookup) debug_printf("ldap_parse_result yielded %d: %s\n", + rc, ldap_err2string(rc)); + +if (rc != LDAP_SUCCESS && rc != LDAP_SIZELIMIT_EXCEEDED + #ifdef LDAP_RES_SEARCH_REFERENCE + && rc != LDAP_RES_SEARCH_REFERENCE + #endif + ) + { + *errmsg = string_sprintf("LDAP search failed - error %d: %s%s%s%s%s", + rc, + error1 ? error1 : US"", + error2 && error2[0] ? US"/" : US"", + error2 ? error2 : US"", + matched && matched[0] ? US"/" : US"", + matched ? matched : US""); + +#if defined LDAP_NAME_ERROR + if (LDAP_NAME_ERROR(rc)) +#elif defined NAME_ERROR /* OPENLDAP1 calls it this */ + if (NAME_ERROR(rc)) +#else + if (rc == LDAP_NO_SUCH_OBJECT) +#endif + + { + DEBUG(D_lookup) debug_printf("lookup failure forced\n"); + error_yield = FAIL; + } + goto RETURN_ERROR; + } + +/* The search succeeded. Check if we have too many results */ + +if (search_type != SEARCH_LDAP_MULTIPLE && rescount > 1) + { + *errmsg = string_sprintf("LDAP search: more than one entry (%d) was returned " + "(filter not specific enough?)", rescount); + goto RETURN_ERROR_BREAK; + } + +/* Check if we have too few (zero) entries */ + +if (rescount < 1) + { + *errmsg = string_sprintf("LDAP search: no results"); + error_yield = FAIL; + goto RETURN_ERROR_BREAK; + } + +/* If an entry was found, but it had no attributes, we behave as if no entries +were found, that is, the lookup failed. */ + +if (!attribute_found) + { + *errmsg = US"LDAP search: found no attributes"; + error_yield = FAIL; + goto RETURN_ERROR; + } + +/* Otherwise, it's all worked */ + +DEBUG(D_lookup) debug_printf("LDAP search: returning: %s\n", data->s); +*res = data->s; + +RETURN_OK: +if (result) ldap_msgfree(result); +ldap_free_urldesc(ludp); +return OK; + +/* Error returns */ + +RETURN_ERROR_BREAK: +*defer_break = TRUE; + +RETURN_ERROR: +DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + +RETURN_ERROR_NOMSG: +if (result) ldap_msgfree(result); +if (ludp) ldap_free_urldesc(ludp); + +#if defined LDAP_LIB_OPENLDAP2 + if (error2) ldap_memfree(error2); + if (matched) ldap_memfree(matched); +#endif + +return error_yield; +} + + + +/************************************************* +* Internal search control function * +*************************************************/ + +/* This function is called from eldap_find(), eldapauth_find(), eldapdn_find(), +and eldapm_find() with a difference in the "search_type" argument. It controls +calls to perform_ldap_search() which actually does the work. We call that +repeatedly for certain types of defer in the case when the URL contains no host +name and eldap_default_servers is set to a list of servers to try. This gives +more control than just passing over a list of hosts to ldap_open() because it +handles other kinds of defer as well as just a failure to open. Note that the +URL is defined to contain either zero or one "hostport" only. + +Parameter data in addition to the URL can be passed as preceding text in the +string, as items of the form XXX=yyy. The URL itself can be detected because it +must begin "ldapx://", where x is empty, s, or i. + +Arguments: + ldap_url the URL to be looked up, optionally preceded by other parameter + settings + search_type SEARCH_LDAP_MULTIPLE allows values from multiple entries + SEARCH_LDAP_SINGLE allows values from one entry only + SEARCH_LDAP_DN gets the DN from one entry + res set to point at the result + errmsg set to point a message if result is not OK + +Returns: OK or FAIL or DEFER +*/ + +static int +control_ldap_search(const uschar *ldap_url, int search_type, uschar **res, + uschar **errmsg) +{ +BOOL defer_break = FALSE; +int timelimit = LDAP_NO_LIMIT; +int sizelimit = LDAP_NO_LIMIT; +int tcplimit = 0; +int sep = 0; +int dereference = LDAP_DEREF_NEVER; +void* referrals = LDAP_OPT_ON; +const uschar *url = ldap_url; +const uschar *p; +uschar *user = NULL; +uschar *password = NULL; +uschar *local_servers = NULL; +uschar *server; +const uschar *list; +uschar buffer[512]; + +while (isspace(*url)) url++; + +/* Until the string begins "ldap", search for the other parameter settings that +are recognized. They are of the form NAME=VALUE, with the value being +optionally double-quoted. There must still be a space after it, however. No +NAME has the value "ldap". */ + +while (strncmpic(url, US"ldap", 4) != 0) + { + const uschar *name = url; + while (*url != 0 && *url != '=') url++; + if (*url == '=') + { + int namelen; + uschar *value; + namelen = ++url - name; + value = string_dequote(&url); + if (isspace(*url)) + { + if (strncmpic(name, US"USER=", namelen) == 0) user = value; + else if (strncmpic(name, US"PASS=", namelen) == 0) password = value; + else if (strncmpic(name, US"SIZE=", namelen) == 0) sizelimit = Uatoi(value); + else if (strncmpic(name, US"TIME=", namelen) == 0) timelimit = Uatoi(value); + else if (strncmpic(name, US"CONNECT=", namelen) == 0) tcplimit = Uatoi(value); + else if (strncmpic(name, US"NETTIME=", namelen) == 0) tcplimit = Uatoi(value); + else if (strncmpic(name, US"SERVERS=", namelen) == 0) local_servers = value; + + /* Don't know if all LDAP libraries have LDAP_OPT_DEREF */ + + #ifdef LDAP_OPT_DEREF + else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0) + { + if (strcmpic(value, US"never") == 0) dereference = LDAP_DEREF_NEVER; + else if (strcmpic(value, US"searching") == 0) + dereference = LDAP_DEREF_SEARCHING; + else if (strcmpic(value, US"finding") == 0) + dereference = LDAP_DEREF_FINDING; + if (strcmpic(value, US"always") == 0) dereference = LDAP_DEREF_ALWAYS; + } + #else + else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0) + { + *errmsg = string_sprintf("LDAP_OP_DEREF not defined in this LDAP " + "library - cannot use \"dereference\""); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + #endif + + #ifdef LDAP_OPT_REFERRALS + else if (strncmpic(name, US"REFERRALS=", namelen) == 0) + { + if (strcmpic(value, US"follow") == 0) referrals = LDAP_OPT_ON; + else if (strcmpic(value, US"nofollow") == 0) referrals = LDAP_OPT_OFF; + else + { + *errmsg = string_sprintf("LDAP option REFERRALS is not \"follow\" " + "or \"nofollow\""); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + } + #else + else if (strncmpic(name, US"REFERRALS=", namelen) == 0) + { + *errmsg = string_sprintf("LDAP_OP_REFERRALS not defined in this LDAP " + "library - cannot use \"referrals\""); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + #endif + + else + { + *errmsg = + string_sprintf("unknown parameter \"%.*s\" precedes LDAP URL", + namelen, name); + DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); + return DEFER; + } + while (isspace(*url)) url++; + continue; + } + } + *errmsg = US"malformed parameter setting precedes LDAP URL"; + DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); + return DEFER; + } + +/* If user is set, de-URL-quote it. Some LDAP libraries do this for themselves, +but it seems that not all behave like this. The DN for the user is often the +result of ${quote_ldap_dn:...} quoting, which does apply URL quoting, because +that is needed when the DN is used as a base DN in a query. Sigh. This is all +far too complicated. */ + +if (user != NULL) + { + uschar *s; + uschar *t = user; + for (s = user; *s != 0; s++) + { + int c, d; + if (*s == '%' && isxdigit(c=s[1]) && isxdigit(d=s[2])) + { + c = tolower(c); + d = tolower(d); + *t++ = + (((c >= 'a')? (10 + c - 'a') : c - '0') << 4) | + ((d >= 'a')? (10 + d - 'a') : d - '0'); + s += 2; + } + else *t++ = *s; + } + *t = 0; + } + +DEBUG(D_lookup) + debug_printf("LDAP parameters: user=%s pass=%s size=%d time=%d connect=%d " + "dereference=%d referrals=%s\n", user, password, sizelimit, timelimit, + tcplimit, dereference, (referrals == LDAP_OPT_ON)? "on" : "off"); + +/* If the request is just to check authentication, some credentials must +be given. The password must not be empty because LDAP binds with an empty +password are considered anonymous, and will succeed on most installations. */ + +if (search_type == SEARCH_LDAP_AUTH) + { + if (user == NULL || password == NULL) + { + *errmsg = US"ldapauth lookups must specify the username and password"; + return DEFER; + } + if (password[0] == 0) + { + DEBUG(D_lookup) debug_printf("Empty password: ldapauth returns FAIL\n"); + return FAIL; + } + } + +/* Check for valid ldap url starters */ + +p = url + 4; +if (tolower(*p) == 's' || tolower(*p) == 'i') p++; +if (Ustrncmp(p, "://", 3) != 0) + { + *errmsg = string_sprintf("LDAP URL does not start with \"ldap://\", " + "\"ldaps://\", or \"ldapi://\" (it starts with \"%.16s...\")", url); + DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); + return DEFER; + } + +/* No default servers, or URL contains a server name: just one attempt */ + +if ((eldap_default_servers == NULL && local_servers == NULL) || p[3] != '/') + { + return perform_ldap_search(url, NULL, 0, search_type, res, errmsg, + &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference, + referrals); + } + +/* Loop through the default servers until OK or FAIL. Use local_servers list + * if defined in the lookup, otherwise use the global default list */ +list = (local_servers == NULL) ? eldap_default_servers : local_servers; +while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) + { + int rc; + int port = 0; + uschar *colon = Ustrchr(server, ':'); + if (colon != NULL) + { + *colon = 0; + port = Uatoi(colon+1); + } + rc = perform_ldap_search(url, server, port, search_type, res, errmsg, + &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference, + referrals); + if (rc != DEFER || defer_break) return rc; + } + +return DEFER; +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The different kinds of search +are handled by a common function, with a flag to differentiate between them. +The handle and filename arguments are not used. */ + +static int +eldap_find(void *handle, uschar *filename, const uschar *ldap_url, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_SINGLE, result, errmsg)); +} + +static int +eldapm_find(void *handle, uschar *filename, const uschar *ldap_url, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_MULTIPLE, result, errmsg)); +} + +static int +eldapdn_find(void *handle, uschar *filename, const uschar *ldap_url, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_DN, result, errmsg)); +} + +int +eldapauth_find(void *handle, uschar *filename, const uschar *ldap_url, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_AUTH, result, errmsg)); +} + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +eldap_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. +Make sure that eldap_dn does not refer to reclaimed or worse, freed store */ + +static void +eldap_tidy(void) +{ +LDAP_CONNECTION *lcp = NULL; +eldap_dn = NULL; + +while ((lcp = ldap_connections) != NULL) + { + DEBUG(D_lookup) debug_printf("unbind LDAP connection to %s:%d\n", lcp->host, + lcp->port); + if(lcp->bound == TRUE) + ldap_unbind(lcp->ld); + ldap_connections = lcp->next; + } +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* LDAP quoting is unbelievably messy. For a start, two different levels of +quoting have to be done: LDAP quoting, and URL quoting. The current +specification is the result of a suggestion by Brian Candler. It recognizes +two separate cases: + +(1) For text that appears in a search filter, the following escapes are + required (see RFC 2254): + + * -> \2A + ( -> \28 + ) -> \29 + \ -> \5C + NULL -> \00 + + Then the entire filter text must be URL-escaped. This kind of quoting is + implemented by ${quote_ldap:....}. Note that we can never have a NULL + in the input string, because that's a terminator. + +(2) For a DN that is part of a URL (i.e. the base DN), the characters + + , + " \ < > ; + + must be quoted by backslashing. See RFC 2253. Leading and trailing spaces + must be escaped, as must a leading #. Then the string must be URL-quoted. + This type of quoting is implemented by ${quote_ldap_dn:....}. + +For URL quoting, the only characters that need not be quoted are the +alphamerics and + + ! $ ' ( ) * + - . _ + +All the others must be hexified and preceded by %. This includes the +backslashes used for LDAP quoting. + +For a DN that is given in the USER parameter for authentication, we need the +same initial quoting as (2) but in this case, the result must NOT be +URL-escaped, because it isn't a URL. The way this is handled is by +de-URL-quoting the text when processing the USER parameter in +control_ldap_search() above. That means that the same quote operator can be +used. This has the additional advantage that spaces in the DN won't cause +parsing problems. For example: + + USER=cn=${quote_ldap_dn:$1},%20dc=example,%20dc=com + +should be safe if there are spaces in $1. + + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + only "dn" is recognized + +Returns: the processed string or NULL for a bad option +*/ + + + +/* The characters in this string, together with alphanumerics, never need +quoting in any way. */ + +#define ALWAYS_LITERAL "!$'-._" + +/* The special characters in this string do not need to be URL-quoted. The set +is a bit larger than the general literals. */ + +#define URL_NONQUOTE ALWAYS_LITERAL "()*+" + +/* The following macros define the characters that are quoted by quote_ldap and +quote_ldap_dn, respectively. */ + +#define LDAP_QUOTE "*()\\" +#define LDAP_DN_QUOTE ",+\"\\<>;" + + + +static uschar * +eldap_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +int len = 0; +BOOL dn = FALSE; +uschar *t = s; +uschar *quoted; + +/* Test for a DN quotation. */ + +if (opt != NULL) + { + if (Ustrcmp(opt, "dn") != 0) return NULL; /* No others recognized */ + dn = TRUE; + } + +/* Compute how much extra store we need for the string. This doesn't have to be +exact as long as it isn't an underestimate. The worst case is the addition of 5 +extra bytes for a single character. This occurs for certain characters in DNs, +where, for example, < turns into %5C%3C. For simplicity, we just add 5 for each +possibly escaped character. The really fast way would be just to test for +non-alphanumerics, but it is probably better to spot a few others that are +never escaped, because if there are no specials at all, we can avoid copying +the string. */ + +while ((c = *t++) != 0) + { + len++; + if (!isalnum(c) && Ustrchr(ALWAYS_LITERAL, c) == NULL) count += 5; + } +if (count == 0) return s; + +/* Get sufficient store to hold the quoted string */ + +t = quoted = store_get(len + count + 1); + +/* Handle plain quote_ldap */ + +if (!dn) + { + while ((c = *s++) != 0) + { + if (!isalnum(c)) + { + if (Ustrchr(LDAP_QUOTE, c) != NULL) + { + sprintf(CS t, "%%5C%02X", c); /* e.g. * => %5C2A */ + t += 5; + continue; + } + if (Ustrchr(URL_NONQUOTE, c) == NULL) /* e.g. ] => %5D */ + { + sprintf(CS t, "%%%02X", c); + t += 3; + continue; + } + } + *t++ = c; /* unquoted character */ + } + } + +/* Handle quote_ldap_dn */ + +else + { + uschar *ss = s + len; + + /* Find the last char before any trailing spaces */ + + while (ss > s && ss[-1] == ' ') ss--; + + /* Quote leading spaces and sharps */ + + for (; s < ss; s++) + { + if (*s != ' ' && *s != '#') break; + sprintf(CS t, "%%5C%%%02X", *s); + t += 6; + } + + /* Handle the rest of the string, up to the trailing spaces */ + + while (s < ss) + { + c = *s++; + if (!isalnum(c)) + { + if (Ustrchr(LDAP_DN_QUOTE, c) != NULL) + { + Ustrncpy(t, "%5C", 3); /* insert \ where needed */ + t += 3; /* fall through to check URL */ + } + if (Ustrchr(URL_NONQUOTE, c) == NULL) /* e.g. ] => %5D */ + { + sprintf(CS t, "%%%02X", c); + t += 3; + continue; + } + } + *t++ = c; /* unquoted character, or non-URL quoted after %5C */ + } + + /* Handle the trailing spaces */ + + while (*ss++ != 0) + { + Ustrncpy(t, "%5C%20", 6); + t += 6; + } + } + +/* Terminate the new string and return */ + +*t = 0; +return quoted; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +ldap_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: LDAP: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info ldap_lookup_info = { + US"ldap", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + eldap_open, /* open function */ + NULL, /* check function */ + eldap_find, /* find function */ + NULL, /* no close function */ + eldap_tidy, /* tidy function */ + eldap_quote, /* quoting function */ + ldap_version_report /* version reporting */ +}; + +static lookup_info ldapdn_lookup_info = { + US"ldapdn", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + eldap_open, /* sic */ /* open function */ + NULL, /* check function */ + eldapdn_find, /* find function */ + NULL, /* no close function */ + eldap_tidy, /* sic */ /* tidy function */ + eldap_quote, /* sic */ /* quoting function */ + NULL /* no version reporting (redundant) */ +}; + +static lookup_info ldapm_lookup_info = { + US"ldapm", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + eldap_open, /* sic */ /* open function */ + NULL, /* check function */ + eldapm_find, /* find function */ + NULL, /* no close function */ + eldap_tidy, /* sic */ /* tidy function */ + eldap_quote, /* sic */ /* quoting function */ + NULL /* no version reporting (redundant) */ +}; + +#ifdef DYNLOOKUP +#define ldap_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &ldap_lookup_info, &ldapdn_lookup_info, &ldapm_lookup_info }; +lookup_module_info ldap_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 3 }; + +/* End of lookups/ldap.c */ diff --git a/src/lookups/ldap.h b/src/lookups/ldap.h new file mode 100644 index 0000000..ddfda85 --- /dev/null +++ b/src/lookups/ldap.h @@ -0,0 +1,13 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for eldapauth_find */ + +extern int eldapauth_find(void *, uschar *, const uschar *, int, uschar **, + uschar **, BOOL *); + +/* End of lookups/ldap.h */ diff --git a/src/lookups/lf_check_file.c b/src/lookups/lf_check_file.c new file mode 100644 index 0000000..43198a8 --- /dev/null +++ b/src/lookups/lf_check_file.c @@ -0,0 +1,113 @@ +/************************************************* +* 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" +#include "lf_functions.h" + + + +/************************************************* +* Check a file's credentials * +*************************************************/ + +/* fstat can normally be expected to work on an open file, but there are some +NFS states where it may not. + +Arguments: + fd an open file descriptor or -1 + filename a file name if fd is -1 + s_type type of file (S_IFREG or S_IFDIR) + modemask a mask specifying mode bits that must *not* be set + owners NULL or a list of of allowable uids, count in the first item + owngroups NULL or a list of allowable gids, count in the first item + type name of lookup type for putting in error message + errmsg where to put an error message + +Returns: -1 stat() or fstat() failed + 0 OK + +1 something didn't match + +Side effect: sets errno to ERRNO_BADUGID, ERRNO_NOTREGULAR or ERRNO_BADMODE for + bad uid/gid, not a regular file, or bad mode; otherwise leaves it + to what fstat set it to. +*/ + +int +lf_check_file(int fd, uschar *filename, int s_type, int modemask, uid_t *owners, + gid_t *owngroups, const char *type, uschar **errmsg) +{ +int i; +struct stat statbuf; + +if ((fd >= 0 && fstat(fd, &statbuf) != 0) || + (fd < 0 && Ustat(filename, &statbuf) != 0)) + { + int save_errno = errno; + *errmsg = string_sprintf("%s: stat failed", filename); + errno = save_errno; + return -1; + } + +if ((statbuf.st_mode & S_IFMT) != s_type) + { + if (s_type == S_IFREG) + { + *errmsg = string_sprintf("%s is not a regular file (%s lookup)", + filename, type); + errno = ERRNO_NOTREGULAR; + } + else + { + *errmsg = string_sprintf("%s is not a directory (%s lookup)", + filename, type); + errno = ERRNO_NOTDIRECTORY; + } + return +1; + } + +if ((statbuf.st_mode & modemask) != 0) + { + *errmsg = string_sprintf("%s (%s lookup): file mode %.4o should not contain " + "%.4o", filename, type, statbuf.st_mode & 07777, + statbuf.st_mode & modemask); + errno = ERRNO_BADMODE; + return +1; + } + +if (owners != NULL) + { + BOOL uid_ok = FALSE; + for (i = 1; i <= (int)owners[0]; i++) + if (owners[i] == statbuf.st_uid) { uid_ok = TRUE; break; } + if (!uid_ok) + { + *errmsg = string_sprintf("%s (%s lookup): file has wrong owner", filename, + type); + errno = ERRNO_BADUGID; + return +1; + } + } + +if (owngroups != NULL) + { + BOOL gid_ok = FALSE; + for (i = 1; i <= (int)owngroups[0]; i++) + if (owngroups[i] == statbuf.st_gid) { gid_ok = TRUE; break; } + if (!gid_ok) + { + *errmsg = string_sprintf("%s (%s lookup): file has wrong group", filename, + type); + errno = ERRNO_BADUGID; + return +1; + } + } + +return 0; +} + +/* End of lf_check_file.c */ diff --git a/src/lookups/lf_functions.h b/src/lookups/lf_functions.h new file mode 100644 index 0000000..8fa6027 --- /dev/null +++ b/src/lookups/lf_functions.h @@ -0,0 +1,18 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the functions that are shared by the lookups */ + +extern int lf_check_file(int, uschar *, int, int, uid_t *, gid_t *, + const char *, uschar **); +extern gstring *lf_quote(uschar *, uschar *, int, gstring *); +extern int lf_sqlperform(const uschar *, const uschar *, const uschar *, + const uschar *, uschar **, + uschar **, uint *, int(*)(const uschar *, uschar *, uschar **, + uschar **, BOOL *, uint *)); + +/* End of lf_functions.h */ diff --git a/src/lookups/lf_quote.c b/src/lookups/lf_quote.c new file mode 100644 index 0000000..8916fdc --- /dev/null +++ b/src/lookups/lf_quote.c @@ -0,0 +1,64 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "lf_functions.h" + + +/************************************************* +* Add string to result, quoting if necessary * +*************************************************/ + +/* This function is called by some lookups that create name=value result +strings, to handle the quoting of the data. It adds "name=" to the result, +followed by appropriately quoted data, followed by a single space. + +Arguments: + name the field name + value the data value + vlength the data length + result the result expanding-string + +Returns: the result pointer (possibly updated) +*/ + +gstring * +lf_quote(uschar *name, uschar *value, int vlength, gstring * result) +{ +result = string_append(result, 2, name, US"="); + +/* NULL is handled as an empty string */ + +if (!value) + { + value = US""; + vlength = 0; + } + +/* Quote the value if it is empty, contains white space, or starts with a quote +character. */ + +if (value[0] == 0 || Ustrpbrk(value, " \t\n\r") != NULL || value[0] == '\"') + { + int j; + result = string_catn(result, US"\"", 1); + for (j = 0; j < vlength; j++) + { + if (value[j] == '\"' || value[j] == '\\') + result = string_catn(result, US"\\", 1); + result = string_catn(result, US value+j, 1); + } + result = string_catn(result, US"\"", 1); + } +else + result = string_catn(result, US value, vlength); + +return string_catn(result, US" ", 1); +} + +/* End of lf_quote.c */ diff --git a/src/lookups/lf_sqlperform.c b/src/lookups/lf_sqlperform.c new file mode 100644 index 0000000..9966307 --- /dev/null +++ b/src/lookups/lf_sqlperform.c @@ -0,0 +1,138 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "lf_functions.h" + + + +/************************************************* +* Call SQL server(s) to run an actual query * +*************************************************/ + +/* All the SQL lookups are of the same form, with a list of servers to try +until one can be accessed. It is now also possible to provide the server data +as part of the query. This function manages server selection and looping; each +lookup has its own function for actually performing the lookup. + +Arguments: + name the lookup name, e.g. "MySQL" + optionname the name of the servers option, e.g. "mysql_servers" + optserverlist the value of the servers option + query the query + result where to pass back the result + errmsg where to pass back an error message + do_cache to be set zero if data is changed + func the lookup function to call + +Returns: the return from the lookup function, or DEFER +*/ + +int +lf_sqlperform(const uschar *name, const uschar *optionname, + const uschar *optserverlist, const uschar *query, + uschar **result, uschar **errmsg, uint *do_cache, + int(*fn)(const uschar *, uschar *, uschar **, uschar **, BOOL *, uint *)) +{ +int sep, rc; +uschar *server; +const uschar *serverlist; +uschar buffer[512]; +BOOL defer_break = FALSE; + +DEBUG(D_lookup) debug_printf("%s query: %s\n", name, query); + +/* Handle queries that do not have server information at the start. */ + +if (Ustrncmp(query, "servers", 7) != 0) + { + sep = 0; + serverlist = optserverlist; + while ((server = string_nextinlist(&serverlist, &sep, buffer, + sizeof(buffer))) != NULL) + { + rc = (*fn)(query, server, result, errmsg, &defer_break, do_cache); + if (rc != DEFER || defer_break) return rc; + } + if (optserverlist == NULL) + *errmsg = string_sprintf("no %s servers defined (%s option)", name, + optionname); + } + +/* Handle queries that do have server information at the start. */ + +else + { + int qsep; + const uschar *s, *ss; + const uschar *qserverlist; + uschar *qserver; + uschar qbuffer[512]; + + s = query + 7; + while (isspace(*s)) s++; + if (*s++ != '=') + { + *errmsg = string_sprintf("missing = after \"servers\" in %s lookup", name); + return DEFER; + } + while (isspace(*s)) s++; + + ss = Ustrchr(s, ';'); + if (ss == NULL) + { + *errmsg = string_sprintf("missing ; after \"servers=\" in %s lookup", + name); + return DEFER; + } + + if (ss == s) + { + *errmsg = string_sprintf("\"servers=\" defines no servers in \"%s\"", + query); + return DEFER; + } + + qserverlist = string_sprintf("%.*s", (int)(ss - s), s); + qsep = 0; + + while ((qserver = string_nextinlist(&qserverlist, &qsep, qbuffer, + sizeof(qbuffer))) != NULL) + { + if (Ustrchr(qserver, '/') != NULL) + server = qserver; + else + { + int len = Ustrlen(qserver); + + sep = 0; + serverlist = optserverlist; + while ((server = string_nextinlist(&serverlist, &sep, buffer, + sizeof(buffer))) != NULL) + { + if (Ustrncmp(server, qserver, len) == 0 && server[len] == '/') + break; + } + + if (server == NULL) + { + *errmsg = string_sprintf("%s server \"%s\" not found in %s", name, + qserver, optionname); + return DEFER; + } + } + + rc = (*fn)(ss+1, server, result, errmsg, &defer_break, do_cache); + if (rc != DEFER || defer_break) return rc; + } + } + +return DEFER; +} + +/* End of lf_sqlperform.c */ diff --git a/src/lookups/lmdb.c b/src/lookups/lmdb.c new file mode 100644 index 0000000..efaa71f --- /dev/null +++ b/src/lookups/lmdb.c @@ -0,0 +1,160 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 2016 - 2018*/ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +#ifdef EXPERIMENTAL_LMDB + +#include <lmdb.h> + +typedef struct lmdbstrct +{ +MDB_txn *txn; +MDB_dbi db_dbi; +} Lmdbstrct; + + +/************************************************* +* Open entry point * +*************************************************/ + +static void * +lmdb_open(uschar * filename, uschar ** errmsg) +{ +MDB_env * db_env = NULL; +Lmdbstrct * lmdb_p; +int ret, save_errno; +const uschar * errstr; + +lmdb_p = store_get(sizeof(Lmdbstrct)); +lmdb_p->txn = NULL; + +if ((ret = mdb_env_create(&db_env))) + { + errstr = US"create environment"; + goto bad; + } + +if ((ret = mdb_env_open(db_env, CS filename, MDB_NOSUBDIR|MDB_RDONLY, 0660))) + { + errstr = string_sprintf("open environment with %s", filename); + goto bad; + } + +if ((ret = mdb_txn_begin(db_env, NULL, MDB_RDONLY, &lmdb_p->txn))) + { + errstr = US"start transaction"; + goto bad; + } + +if ((ret = mdb_open(lmdb_p->txn, NULL, 0, &lmdb_p->db_dbi))) + { + errstr = US"open database"; + goto bad; + } + +return lmdb_p; + +bad: + save_errno = errno; + if (lmdb_p->txn) mdb_txn_abort(lmdb_p->txn); + if (db_env) mdb_env_close(db_env); + *errmsg = string_sprintf("LMDB: Unable to %s: %s", errstr, mdb_strerror(ret)); + errno = save_errno; + return NULL; +} + + +/************************************************* +* Find entry point * +*************************************************/ + +static int +lmdb_find(void * handle, uschar * filename, + const uschar * keystring, int length, uschar ** result, uschar ** errmsg, + uint * do_cache) +{ +int ret; +MDB_val dbkey, data; +Lmdbstrct * lmdb_p = handle; + +dbkey.mv_data = CS keystring; +dbkey.mv_size = length; + +DEBUG(D_lookup) debug_printf("LMDB: lookup key: %s\n", CS keystring); + +if ((ret = mdb_get(lmdb_p->txn, lmdb_p->db_dbi, &dbkey, &data)) == 0) + { + *result = string_copyn(US data.mv_data, data.mv_size); + DEBUG(D_lookup) debug_printf("LMDB: lookup result: %s\n", *result); + return OK; + } +else if (ret == MDB_NOTFOUND) + { + *errmsg = US"LMDB: lookup, no data found"; + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return FAIL; + } +else + { + *errmsg = string_sprintf("LMDB: lookup error: %s", mdb_strerror(ret)); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } +} + + +/************************************************* +* Close entry point * +*************************************************/ + +static void +lmdb_close(void * handle) +{ +Lmdbstrct * lmdb_p = handle; +MDB_env * db_env = mdb_txn_env(lmdb_p->txn); +mdb_txn_abort(lmdb_p->txn); +mdb_env_close(db_env); +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +#include "../version.h" + +void +lmdb_version_report(FILE * f) +{ +fprintf(f, "Library version: LMDB: Compile: %d.%d.%d\n", + MDB_VERSION_MAJOR, MDB_VERSION_MINOR, MDB_VERSION_PATCH); +#ifdef DYNLOOKUP +fprintf(f, " Exim version %s\n", EXIM_VERSION_STR); +#endif +} + +static lookup_info lmdb_lookup_info = { + US"lmdb", /* lookup name */ + lookup_absfile, /* query-style lookup */ + lmdb_open, /* open function */ + NULL, /* no check function */ + lmdb_find, /* find function */ + lmdb_close, /* close function */ + NULL, /* tidy function */ + NULL, /* quoting function */ + lmdb_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +# define lmdb_lookup_module_info _lookup_module_info +#endif /* DYNLOOKUP */ + +static lookup_info *_lookup_list[] = { &lmdb_lookup_info }; +lookup_module_info lmdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +#endif /* EXPERIMENTAL_LMDB */ diff --git a/src/lookups/lsearch.c b/src/lookups/lsearch.c new file mode 100644 index 0000000..8b4459a --- /dev/null +++ b/src/lookups/lsearch.c @@ -0,0 +1,484 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + +/* Codes for the different kinds of lsearch that are supported */ + +enum { + LSEARCH_PLAIN, /* Literal keys */ + LSEARCH_WILD, /* Wild card keys, expanded */ + LSEARCH_NWILD, /* Wild card keys, not expanded */ + LSEARCH_IP /* IP addresses and networks */ +}; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +static void * +lsearch_open(uschar *filename, uschar **errmsg) +{ +FILE *f = Ufopen(filename, "rb"); +if (f == NULL) + { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s for linear search", filename); + errno = save_errno; + return NULL; + } +return f; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +static BOOL +lsearch_check(void *handle, uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +return lf_check_file(fileno((FILE *)handle), filename, S_IFREG, modemask, + owners, owngroups, "lsearch", errmsg) == 0; +} + + + +/************************************************* +* Internal function for the various lsearches * +*************************************************/ + +/* See local README for interface description, plus: + +Extra argument: + + type one of the values LSEARCH_PLAIN, LSEARCH_WILD, LSEARCH_NWILD, or + LSEARCH_IP + +There is some messy logic in here to cope with very long data lines that do not +fit into the fixed sized buffer. Most of the time this will never be exercised, +but people do occasionally do weird things. */ + +static int +internal_lsearch_find(void *handle, uschar *filename, const uschar *keystring, + int length, uschar **result, uschar **errmsg, int type) +{ +FILE *f = (FILE *)handle; +BOOL last_was_eol = TRUE; +BOOL this_is_eol = TRUE; +int old_pool = store_pool; +void *reset_point = NULL; +uschar buffer[4096]; + +/* Wildcard searches may use up some store, because of expansions. We don't +want them to fill up our search store. What we do is set the pool to the main +pool and get a point to reset to later. Wildcard searches could also issue +lookups, but internal_search_find will take care of that, and the cache will be +safely stored in the search pool again. */ + +if(type == LSEARCH_WILD || type == LSEARCH_NWILD) + { + store_pool = POOL_MAIN; + reset_point = store_get(0); + } + +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; + +rewind(f); +for (last_was_eol = TRUE; + Ufgets(buffer, sizeof(buffer), f) != NULL; + last_was_eol = this_is_eol) + { + int p = Ustrlen(buffer); + int linekeylength; + BOOL this_is_comment; + gstring * yield; + uschar *s = buffer; + + /* Check whether this the final segment of a line. If it follows an + incomplete part-line, skip it. */ + + this_is_eol = p > 0 && buffer[p-1] == '\n'; + if (!last_was_eol) continue; + + /* We now have the start of a physical line. If this is a final line segment, + remove trailing white space. */ + + if (this_is_eol) + { + while (p > 0 && isspace((uschar)buffer[p-1])) p--; + buffer[p] = 0; + } + + /* If the buffer is empty it might be (a) a complete empty line, or (b) the + start of a line that begins with so much white space that it doesn't all fit + in the buffer. In both cases we want to skip the entire physical line. + + If the buffer begins with # it is a comment line; if it begins with white + space it is a logical continuation; again, we want to skip the entire + physical line. */ + + if (buffer[0] == 0 || buffer[0] == '#' || isspace(buffer[0])) continue; + + /* We assume that they key will fit in the buffer. If the key starts with ", + read it as a quoted string. We don't use string_dequote() because that uses + new store for the result, and we may be doing this many times in a long file. + We know that the dequoted string must be shorter than the original, because + we are removing the quotes, and also any escape sequences always turn two or + more characters into one character. Therefore, we can store the new string in + the same buffer. */ + + if (*s == '\"') + { + uschar *t = s++; + while (*s != 0 && *s != '\"') + { + if (*s == '\\') *t++ = string_interpret_escape(CUSS &s); + else *t++ = *s; + s++; + } + if (*s != 0) s++; /* Past terminating " */ + linekeylength = t - buffer; + } + + /* Otherwise it is terminated by a colon or white space */ + + else + { + while (*s != 0 && *s != ':' && !isspace(*s)) s++; + linekeylength = s - buffer; + } + + /* The matching test depends on which kind of lsearch we are doing */ + + switch(type) + { + /* A plain lsearch treats each key as a literal */ + + case LSEARCH_PLAIN: + if (linekeylength != length || strncmpic(buffer, keystring, length) != 0) + continue; + break; /* Key matched */ + + /* A wild lsearch treats each key as a possible wildcarded string; no + expansion is done for nwildlsearch. */ + + case LSEARCH_WILD: + case LSEARCH_NWILD: + { + int rc; + int save = buffer[linekeylength]; + const uschar *list = buffer; + buffer[linekeylength] = 0; + rc = match_isinlist(keystring, + &list, + UCHAR_MAX+1, /* Single-item list */ + NULL, /* No anchor */ + NULL, /* No caching */ + MCL_STRING + ((type == LSEARCH_WILD)? 0:MCL_NOEXPAND), + TRUE, /* Caseless */ + NULL); + buffer[linekeylength] = save; + if (rc == FAIL) continue; + if (rc == DEFER) return DEFER; + } + + /* The key has matched. If the search involved a regular expression, it + might have caused numerical variables to be set. However, their values will + be in the wrong storage pool for external use. Copying them to the standard + pool is not feasible because of the caching of lookup results - a repeated + lookup will not match the regular expression again. Therefore, we flatten + all numeric variables at this point. */ + + expand_nmax = -1; + break; + + /* Compare an ip address against a list of network/ip addresses. We have to + allow for the "*" case specially. */ + + case LSEARCH_IP: + if (linekeylength == 1 && buffer[0] == '*') + { + if (length != 1 || keystring[0] != '*') continue; + } + else if (length == 1 && keystring[0] == '*') continue; + else + { + int maskoffset; + int save = buffer[linekeylength]; + buffer[linekeylength] = 0; + if (string_is_ip_address(buffer, &maskoffset) == 0 || + !host_is_in_net(keystring, buffer, maskoffset)) continue; + buffer[linekeylength] = save; + } + break; /* Key matched */ + } + + /* The key has matched. Skip spaces after the key, and allow an optional + colon after the spaces. This is an odd specification, but it's for + compatibility. */ + + while (isspace((uschar)*s)) s++; + if (*s == ':') + { + s++; + while (isspace((uschar)*s)) s++; + } + + /* Reset dynamic store, if we need to, and revert to the search pool */ + + if (reset_point) + { + store_reset(reset_point); + store_pool = old_pool; + } + + /* Now we want to build the result string to contain the data. There can be + two kinds of continuation: (a) the physical line may not all have fitted into + the buffer, and (b) there may be logical continuation lines, for which we + must convert all leading white space into a single blank. + + Initialize, and copy the first segment of data. */ + + this_is_comment = FALSE; + yield = string_get(100); + if (*s != 0) + yield = string_cat(yield, s); + + /* Now handle continuations */ + + for (last_was_eol = this_is_eol; + Ufgets(buffer, sizeof(buffer), f) != NULL; + last_was_eol = this_is_eol) + { + s = buffer; + p = Ustrlen(buffer); + this_is_eol = p > 0 && buffer[p-1] == '\n'; + + /* Remove trailing white space from a physical line end */ + + if (this_is_eol) + { + while (p > 0 && isspace((uschar)buffer[p-1])) p--; + buffer[p] = 0; + } + + /* If this is not a physical line continuation, skip it entirely if it's + empty or starts with #. Otherwise, break the loop if it doesn't start with + white space. Otherwise, replace leading white space with a single blank. */ + + if (last_was_eol) + { + this_is_comment = (this_is_comment || (buffer[0] == 0 || buffer[0] == '#')); + if (this_is_comment) continue; + if (!isspace((uschar)buffer[0])) break; + while (isspace((uschar)*s)) s++; + *(--s) = ' '; + } + if (this_is_comment) continue; + + /* Join a physical or logical line continuation onto the result string. */ + + yield = string_cat(yield, s); + } + + store_reset(yield->s + yield->ptr + 1); + *result = string_from_gstring(yield); + return OK; + } + +/* Reset dynamic store, if we need to */ + +if (reset_point) + { + store_reset(reset_point); + store_pool = old_pool; + } + +return FAIL; +} + + +/************************************************* +* Find entry point for lsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +lsearch_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_PLAIN); +} + + + +/************************************************* +* Find entry point for wildlsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +wildlsearch_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_WILD); +} + + + +/************************************************* +* Find entry point for nwildlsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +nwildlsearch_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_NWILD); +} + + + + +/************************************************* +* Find entry point for iplsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +iplsearch_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +if ((length == 1 && keystring[0] == '*') || + string_is_ip_address(keystring, NULL) != 0) + { + return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_IP); + } +else + { + *errmsg = string_sprintf("\"%s\" is not a valid iplsearch key (an IP " + "address, with optional CIDR mask, is wanted): " + "in a host list, use net-iplsearch as the search type", keystring); + return DEFER; + } +} + + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +static void +lsearch_close(void *handle) +{ +(void)fclose((FILE *)handle); +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +lsearch_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: lsearch: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info iplsearch_lookup_info = { + US"iplsearch", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + lsearch_open, /* open function */ + lsearch_check, /* check function */ + iplsearch_find, /* find function */ + lsearch_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + NULL /* no version reporting (redundant) */ +}; + +static lookup_info lsearch_lookup_info = { + US"lsearch", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + lsearch_open, /* open function */ + lsearch_check, /* check function */ + lsearch_find, /* find function */ + lsearch_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + lsearch_version_report /* version reporting */ +}; + +static lookup_info nwildlsearch_lookup_info = { + US"nwildlsearch", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + lsearch_open, /* open function */ + lsearch_check, /* check function */ + nwildlsearch_find, /* find function */ + lsearch_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + NULL /* no version reporting (redundant) */ +}; + +static lookup_info wildlsearch_lookup_info = { + US"wildlsearch", /* lookup name */ + lookup_absfile, /* uses absolute file name */ + lsearch_open, /* open function */ + lsearch_check, /* check function */ + wildlsearch_find, /* find function */ + lsearch_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + NULL /* no version reporting (redundant) */ +}; + +#ifdef DYNLOOKUP +#define lsearch_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &iplsearch_lookup_info, + &lsearch_lookup_info, + &nwildlsearch_lookup_info, + &wildlsearch_lookup_info }; +lookup_module_info lsearch_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 4 }; + +/* End of lookups/lsearch.c */ diff --git a/src/lookups/mysql.c b/src/lookups/mysql.c new file mode 100644 index 0000000..77027fc --- /dev/null +++ b/src/lookups/mysql.c @@ -0,0 +1,505 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Thanks to Paul Kelly for contributing the original code for these +functions. */ + + +#include "../exim.h" +#include "lf_functions.h" + +#include <mysql.h> /* The system header */ + +/* We define symbols for *_VERSION_ID (numeric), *_VERSION_STR (char*) +and *_BASE_STR (char*). It's a bit of guesswork. Especially for mariadb +with versions before 10.2, as they do not define there there specific symbols. +*/ + +/* Newer (>= 10.2) MariaDB */ +#if defined MARIADB_VERSION_ID +#define EXIM_MxSQL_VERSION_ID MARIADB_VERSION_ID + +/* MySQL defines MYSQL_VERSION_ID, and MariaDB does so */ +/* https://dev.mysql.com/doc/refman/5.7/en/c-api-server-client-versions.html */ +#elif defined LIBMYSQL_VERSION_ID +#define EXIM_MxSQL_VERSION_ID LIBMYSQL_VERSION_ID +#elif defined MYSQL_VERSION_ID +#define EXIM_MxSQL_VERSION_ID MYSQL_VERSION_ID + +#else +#define EXIM_MYSQL_VERSION_ID 0 +#endif + +/* Newer (>= 10.2) MariaDB */ +#ifdef MARIADB_CLIENT_VERSION_STR +#define EXIM_MxSQL_VERSION_STR MARIADB_CLIENT_VERSION_STR + +/* Mysql uses MYSQL_SERVER_VERSION */ +#elif defined LIBMYSQL_VERSION +#define EXIM_MxSQL_VERSION_STR LIBMYSQL_VERSION +#elif defined MYSQL_SERVER_VERSION +#define EXIM_MxSQL_VERSION_STR MYSQL_SERVER_VERSION + +#else +#define EXIM_MxSQL_VERSION_STR "unknown" +#endif + +#if defined MARIADB_BASE_VERSION +#define EXIM_MxSQL_BASE_STR MARIADB_BASE_VERSION + +#elif defined MARIADB_PACKAGE_VERSION +#define EXIM_MxSQL_BASE_STR "mariadb" + +#elif defined MYSQL_BASE_VERSION +#define EXIM_MxSQL_BASE_STR MYSQL_BASE_VERSION + +#else +#define EXIM_MxSQL_BASE_STR "n.A." +#endif + + +/* Structure and anchor for caching connections. */ + +typedef struct mysql_connection { + struct mysql_connection *next; + uschar *server; + MYSQL *handle; +} mysql_connection; + +static mysql_connection *mysql_connections = NULL; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +mysql_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void +mysql_tidy(void) +{ +mysql_connection *cn; +while ((cn = mysql_connections) != NULL) + { + mysql_connections = cn->next; + DEBUG(D_lookup) debug_printf("close MYSQL connection: %s\n", cn->server); + mysql_close(cn->handle); + } +} + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* 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 zero if data is changed + +The server string is of the form "host/dbname/user/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_mysql_search(const uschar *query, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break, uint *do_cache) +{ +MYSQL *mysql_handle = NULL; /* Keep compilers happy */ +MYSQL_RES *mysql_result = NULL; +MYSQL_ROW mysql_row_data; +MYSQL_FIELD *fields; + +int i; +int yield = DEFER; +unsigned int num_fields; +gstring * result = NULL; +mysql_connection *cn; +uschar *server_copy = NULL; +uschar *sdata[4]; + +/* Disaggregate the parameters from the server argument. The order is host, +database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 3; i > 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (pp == NULL) + { + *errmsg = string_sprintf("incomplete MySQL server data: %s", + (i == 3)? server : server_copy); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 3) server_copy = string_copy(server); /* sans password */ + } +sdata[0] = server; /* What's left at the start */ + +/* See if we have a cached connection to the server */ + +for (cn = mysql_connections; cn; cn = cn->next) + if (Ustrcmp(cn->server, server_copy) == 0) + { + mysql_handle = cn->handle; + break; + } + +/* If no cached connection, we must set one up. Mysql allows for a host name +and port to be specified. It also allows the name of a Unix socket to be used. +Unfortunately, this contains slashes, but its use is expected to be rare, so +the rather cumbersome syntax shouldn't inconvenience too many people. We use +this: host:port(socket)[group] where all the parts are optional. +The "group" parameter specifies an option group from a MySQL option file. */ + +if (!cn) + { + uschar *p; + uschar *socket = NULL; + int port = 0; + uschar *group = US"exim"; + + if ((p = Ustrchr(sdata[0], '['))) + { + *p++ = 0; + group = p; + while (*p && *p != ']') p++; + *p = 0; + } + + if ((p = Ustrchr(sdata[0], '('))) + { + *p++ = 0; + socket = p; + while (*p && *p != ')') p++; + *p = 0; + } + + if ((p = Ustrchr(sdata[0], ':'))) + { + *p++ = 0; + port = Uatoi(p); + } + + if (Ustrchr(sdata[0], '/')) + { + *errmsg = string_sprintf("unexpected slash in MySQL server hostname: %s", + sdata[0]); + *defer_break = TRUE; + return DEFER; + } + + /* If the database is the empty string, set it NULL - the query must then + define it. */ + + if (sdata[1][0] == 0) sdata[1] = NULL; + + DEBUG(D_lookup) + debug_printf("MYSQL new connection: host=%s port=%d socket=%s " + "database=%s user=%s\n", sdata[0], port, socket, sdata[1], sdata[2]); + + /* Get store for a new handle, initialize it, and connect to the server */ + + mysql_handle = store_get(sizeof(MYSQL)); + mysql_init(mysql_handle); + mysql_options(mysql_handle, MYSQL_READ_DEFAULT_GROUP, CS group); + if (mysql_real_connect(mysql_handle, + /* host user passwd database */ + CS sdata[0], CS sdata[2], CS sdata[3], CS sdata[1], + port, CS socket, CLIENT_MULTI_RESULTS) == NULL) + { + *errmsg = string_sprintf("MYSQL connection failed: %s", + mysql_error(mysql_handle)); + *defer_break = FALSE; + goto MYSQL_EXIT; + } + + /* Add the connection to the cache */ + + cn = store_get(sizeof(mysql_connection)); + cn->server = server_copy; + cn->handle = mysql_handle; + cn->next = mysql_connections; + mysql_connections = cn; + } + +/* Else use a previously cached connection */ + +else + { + DEBUG(D_lookup) + debug_printf("MYSQL using cached connection for %s\n", server_copy); + } + +/* Run the query */ + +if (mysql_query(mysql_handle, CS query) != 0) + { + *errmsg = string_sprintf("MYSQL: query failed: %s\n", + mysql_error(mysql_handle)); + *defer_break = FALSE; + goto MYSQL_EXIT; + } + +/* Pick up the result. If the query was not of the type that returns data, +namely INSERT, UPDATE, or DELETE, an error occurs here. However, this situation +can be detected by calling mysql_field_count(). If its result is zero, no data +was expected (this is all explained clearly in the MySQL manual). In this case, +we return the number of rows affected by the command. In this event, we do NOT +want to cache the result; also the whole cache for the handle must be cleaned +up. Setting do_cache zero requests this. */ + +if (!(mysql_result = mysql_use_result(mysql_handle))) + { + if ( mysql_field_count(mysql_handle) == 0 ) + { + DEBUG(D_lookup) debug_printf("MYSQL: query was not one that returns data\n"); + result = string_cat(result, + string_sprintf("%d", mysql_affected_rows(mysql_handle))); + *do_cache = 0; + goto MYSQL_EXIT; + } + *errmsg = string_sprintf("MYSQL: lookup result failed: %s\n", + mysql_error(mysql_handle)); + *defer_break = FALSE; + goto MYSQL_EXIT; + } + +/* Find the number of fields returned. If this is one, we don't add field +names to the data. Otherwise we do. */ + +num_fields = mysql_num_fields(mysql_result); + +/* Get the fields and construct the result string. If there is more than one +row, we insert '\n' between them. */ + +fields = mysql_fetch_fields(mysql_result); + +while ((mysql_row_data = mysql_fetch_row(mysql_result))) + { + unsigned long *lengths = mysql_fetch_lengths(mysql_result); + + if (result) + result = string_catn(result, US"\n", 1); + + if (num_fields != 1) + for (i = 0; i < num_fields; i++) + result = lf_quote(US fields[i].name, US mysql_row_data[i], lengths[i], + result); + + else if (mysql_row_data[0] != NULL) /* NULL value yields nothing */ + result = string_catn(result, US mysql_row_data[0], lengths[0]); + } + +/* more results? -1 = no, >0 = error, 0 = yes (keep looping) + This is needed because of the CLIENT_MULTI_RESULTS on mysql_real_connect(), + we don't expect any more results. */ + +while((i = mysql_next_result(mysql_handle)) >= 0) + { + if(i == 0) /* Just ignore more results */ + { + DEBUG(D_lookup) debug_printf("MYSQL: got unexpected more results\n"); + continue; + } + + *errmsg = string_sprintf( + "MYSQL: lookup result error when checking for more results: %s\n", + mysql_error(mysql_handle)); + goto MYSQL_EXIT; + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + +if (!result) + { + yield = FAIL; + *errmsg = US"MYSQL: no data found"; + } + +/* Get here by goto from various error checks and from the case where no data +was read (e.g. an update query). */ + +MYSQL_EXIT: + +/* Free mysal store for any result that was got; don't close the connection, as +it is cached. */ + +if (mysql_result) mysql_free_result(mysql_result); + +/* Non-NULL result indicates a successful result */ + +if (result) + { + *resultptr = string_from_gstring(result); + store_reset(result->s + (result->size = result->ptr + 1)); + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + 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 SQL lookups. */ + +static int +mysql_find(void *handle, uschar *filename, const uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +return lf_sqlperform(US"MySQL", US"mysql_servers", mysql_servers, query, + result, errmsg, do_cache, perform_mysql_search); +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only characters that need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent, and underscore and not escaped. They are only special in contexts +where they can be wild cards, and this isn't usually the case for data inserted +from messages, since that isn't likely to be treated as a pattern of any kind. +Sadly, MySQL doesn't seem to behave like other programs. If you use something +like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really +can't quote "on spec". + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +mysql_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options recognized */ + +while ((c = *t++) != 0) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +if (count == 0) return s; +t = quoted = store_get(Ustrlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) + { + *t++ = '\\'; + switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + } + } + else *t++ = c; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +mysql_version_report(FILE *f) +{ +fprintf(f, "Library version: MySQL: Compile: %lu %s [%s]\n" + " Runtime: %lu %s\n", + (long)EXIM_MxSQL_VERSION_ID, EXIM_MxSQL_VERSION_STR, EXIM_MxSQL_BASE_STR, + mysql_get_client_version(), mysql_get_client_info()); +#ifdef DYNLOOKUP +fprintf(f, " Exim version %s\n", EXIM_VERSION_STR); +#endif +} + +/* These are the lookup_info blocks for this driver */ + +static lookup_info mysql_lookup_info = { + US"mysql", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + mysql_open, /* open function */ + NULL, /* no check function */ + mysql_find, /* find function */ + NULL, /* no close function */ + mysql_tidy, /* tidy function */ + mysql_quote, /* quoting function */ + mysql_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define mysql_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &mysql_lookup_info }; +lookup_module_info mysql_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/mysql.c */ diff --git a/src/lookups/nis.c b/src/lookups/nis.c new file mode 100644 index 0000000..d3f0480 --- /dev/null +++ b/src/lookups/nis.c @@ -0,0 +1,137 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + +#include <rpcsvc/ypclnt.h> + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. This serves for both +the "nis" and "nis0" lookup types. */ + +static void * +nis_open(uschar *filename, uschar **errmsg) +{ +char *nis_domain; +if (yp_get_default_domain(&nis_domain) != 0) + { + *errmsg = string_sprintf("failed to get default NIS domain"); + return NULL; + } +return nis_domain; +} + + + +/************************************************* +* Find entry point for nis * +*************************************************/ + +/* See local README for interface description. A separate function is used +for nis0 because they are so short it isn't worth trying to use any common +code. */ + +static int +nis_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +int rc; +uschar *nis_data; +int nis_data_length; +do_cache = do_cache; /* Placate picky compilers */ +if ((rc = yp_match(CCS handle, CCS filename, CCS keystring, length, + CSS &nis_data, &nis_data_length)) == 0) + { + *result = string_copy(nis_data); + (*result)[nis_data_length] = 0; /* remove final '\n' */ + return OK; + } +return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER; +} + + + +/************************************************* +* Find entry point for nis0 * +*************************************************/ + +/* See local README for interface description. */ + +static int +nis0_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +int rc; +uschar *nis_data; +int nis_data_length; +do_cache = do_cache; /* Placate picky compilers */ +if ((rc = yp_match(CCS handle, CCS filename, CCS keystring, length + 1, + CSS &nis_data, &nis_data_length)) == 0) + { + *result = string_copy(nis_data); + (*result)[nis_data_length] = 0; /* remove final '\n' */ + return OK; + } +return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +nis_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: NIS: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info nis_lookup_info = { + US"nis", /* lookup name */ + 0, /* not abs file, not query style*/ + nis_open, /* open function */ + NULL, /* check function */ + nis_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + nis_version_report /* version reporting */ +}; + +static lookup_info nis0_lookup_info = { + US"nis0", /* lookup name */ + 0, /* not absfile, not query style */ + nis_open, /* sic */ /* open function */ + NULL, /* check function */ + nis0_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + NULL /* no version reporting (redundant) */ +}; + +#ifdef DYNLOOKUP +#define nis_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &nis_lookup_info, &nis0_lookup_info }; +lookup_module_info nis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 2 }; + +/* End of lookups/nis.c */ diff --git a/src/lookups/nisplus.c b/src/lookups/nisplus.c new file mode 100644 index 0000000..e2115a5 --- /dev/null +++ b/src/lookups/nisplus.c @@ -0,0 +1,293 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + +#include <rpcsvc/nis.h> + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +nisplus_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The format of queries for a +NIS+ search is + + [field=value,...],table-name +or + [field=value,...],table-name:result-field-name + +in other words, a normal NIS+ "indexed name", with an optional result field +name tagged on the end after a colon. If there is no result-field name, the +yield is the concatenation of all the fields, preceded by their names and an +equals sign. */ + +static int +nisplus_find(void *handle, uschar *filename, const uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +int i; +int error_error = FAIL; +const uschar * field_name = NULL; +nis_result *nrt = NULL; +nis_result *nre = NULL; +nis_object *tno, *eno; +struct entry_obj *eo; +struct table_obj *ta; +const uschar * p = query + length; +gstring * yield = NULL; + +do_cache = do_cache; /* Placate picky compilers */ + +/* Search backwards for a colon to see if a result field name +has been given. */ + +while (p > query && p[-1] != ':') p--; + +if (p > query) /* get the query without the result-field */ + { + uint len = p-1 - query; + field_name = p; + query = string_copyn(query, len); + p = query + len; + } +else + p = query + length; + +/* Now search backwards to find the comma that starts the +table name. */ + +while (p > query && p[-1] != ',') p--; +if (p <= query) + { + *errmsg = US"NIS+ query malformed"; + error_error = DEFER; + goto NISPLUS_EXIT; + } + +/* Look up the data for the table, in order to get the field names, +check that we got back a table, and set up pointers so the field +names can be scanned. */ + +nrt = nis_lookup(CS p, EXPAND_NAME | NO_CACHE); +if (nrt->status != NIS_SUCCESS) + { + *errmsg = string_sprintf("NIS+ error accessing %s table: %s", p, + nis_sperrno(nrt->status)); + if (nrt->status != NIS_NOTFOUND && nrt->status != NIS_NOSUCHTABLE) + error_error = DEFER; + goto NISPLUS_EXIT; + } +tno = nrt->objects.objects_val; +if (tno->zo_data.zo_type != TABLE_OBJ) + { + *errmsg = string_sprintf("NIS+ error: %s is not a table", p); + goto NISPLUS_EXIT; + } +ta = &tno->zo_data.objdata_u.ta_data; + +/* Now look up the entry in the table, check that we got precisely one +object and that it is a table entry. */ + +nre = nis_list(CS query, EXPAND_NAME, NULL, NULL); +if (nre->status != NIS_SUCCESS) + { + *errmsg = string_sprintf("NIS+ error accessing entry %s: %s", + query, nis_sperrno(nre->status)); + goto NISPLUS_EXIT; + } +if (nre->objects.objects_len > 1) + { + *errmsg = string_sprintf("NIS+ returned more than one object for %s", + query); + goto NISPLUS_EXIT; + } +else if (nre->objects.objects_len < 1) + { + *errmsg = string_sprintf("NIS+ returned no data for %s", query); + goto NISPLUS_EXIT; + } +eno = nre->objects.objects_val; +if (eno->zo_data.zo_type != ENTRY_OBJ) + { + *errmsg = string_sprintf("NIS+ error: %s is not an entry", query); + goto NISPLUS_EXIT; + } + +/* Scan the columns in the entry and in the table. If a result field +was given, look for that field; otherwise concatenate all the fields +with their names. */ + +eo = &(eno->zo_data.objdata_u.en_data); +for (i = 0; i < eo->en_cols.en_cols_len; i++) + { + table_col *tc = ta->ta_cols.ta_cols_val + i; + entry_col *ec = eo->en_cols.en_cols_val + i; + int len = ec->ec_value.ec_value_len; + uschar *value = US ec->ec_value.ec_value_val; + + /* The value may be NULL for a zero-length field. Turn this into an + empty string for consistency. Remove trailing whitespace and zero + bytes. */ + + if (value == NULL) value = US""; else + while (len > 0 && (value[len-1] == 0 || isspace(value[len-1]))) + len--; + + /* Concatenate all fields if no specific one selected */ + + if (!field_name) + { + yield = string_cat (yield, tc->tc_name); + yield = string_catn(yield, US"=", 1); + + /* Quote the value if it contains spaces or is empty */ + + if (value[0] == 0 || Ustrchr(value, ' ') != NULL) + { + int j; + yield = string_catn(yield, US"\"", 1); + for (j = 0; j < len; j++) + { + if (value[j] == '\"' || value[j] == '\\') + yield = string_catn(yield, US"\\", 1); + yield = string_catn(yield, value+j, 1); + } + yield = string_catn(yield, US"\"", 1); + } + else + yield = string_catn(yield, value, len); + + yield = string_catn(yield, US" ", 1); + } + + /* When the specified field is found, grab its data and finish */ + + else if (Ustrcmp(field_name, tc->tc_name) == 0) + { + yield = string_catn(yield, value, len); + goto NISPLUS_EXIT; + } + } + +/* Error if a field name was specified and we didn't find it; if no +field name, ensure the concatenated data is zero-terminated. */ + +if (field_name) + *errmsg = string_sprintf("NIS+ field %s not found for %s", field_name, + query); +else + store_reset(yield->s + yield->ptr + 1); + +/* Free result store before finishing. */ + +NISPLUS_EXIT: +if (nrt) nis_freeresult(nrt); +if (nre) nis_freeresult(nre); + +if (yield) + { + *result = string_from_gstring(yield); + return OK; + } + +return error_error; /* FAIL or DEFER */ +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only quoting that is necessary for NIS+ is to double any doublequote +characters. No options are recognized. + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +nisplus_quote(uschar *s, uschar *opt) +{ +int count = 0; +uschar *quoted; +uschar *t = s; + +if (opt != NULL) return NULL; /* No options recognized */ + +while (*t != 0) if (*t++ == '\"') count++; +if (count == 0) return s; + +t = quoted = store_get(Ustrlen(s) + count + 1); + +while (*s != 0) + { + *t++ = *s; + if (*s++ == '\"') *t++ = '\"'; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +nisplus_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: NIS+: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"nisplus", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + nisplus_open, /* open function */ + NULL, /* check function */ + nisplus_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + nisplus_quote, /* quoting function */ + nisplus_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define nisplus_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info nisplus_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/nisplus.c */ diff --git a/src/lookups/oracle.c b/src/lookups/oracle.c new file mode 100644 index 0000000..1b21e6a --- /dev/null +++ b/src/lookups/oracle.c @@ -0,0 +1,631 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Interface to an Oracle database. This code was originally supplied by +Paul Kelly, but I have hacked it around for various reasons, and tried to add +some comments from my position of Oracle ignorance. */ + + +#include "../exim.h" + + +/* The Oracle system headers */ + +#include <oratypes.h> +#include <ocidfn.h> +#include <ocikpr.h> + +#define PARSE_NO_DEFER 0 /* parse straight away */ +#define PARSE_V7_LNG 2 +#define MAX_ITEM_BUFFER_SIZE 1024 /* largest size of a cell of data */ +#define MAX_SELECT_LIST_SIZE 32 /* maximum number of columns (not rows!) */ + +/* Paul's comment on this was "change this to 512 for 64bit cpu", but I don't +understand why. The Oracle manual just asks for 256 bytes. + +That was years ago. Jin Choi suggested (March 2007) that this change should +be made in the source, as at worst it wastes 256 bytes, and it saves people +having to discover about this for themselves as more and more systems are +64-bit. So I have changed 256 to 512. */ + +#define HDA_SIZE 512 + +/* Internal/external datatype codes */ + +#define NUMBER_TYPE 2 +#define INT_TYPE 3 +#define FLOAT_TYPE 4 +#define STRING_TYPE 5 +#define ROWID_TYPE 11 +#define DATE_TYPE 12 + +/* ORACLE error codes used in demonstration programs */ + +#define VAR_NOT_IN_LIST 1007 +#define NO_DATA_FOUND 1403 + +typedef struct Ora_Describe { + sb4 dbsize; + sb2 dbtype; + sb1 buf[MAX_ITEM_BUFFER_SIZE]; + sb4 buflen; + sb4 dsize; + sb2 precision; + sb2 scale; + sb2 nullok; +} Ora_Describe; + +typedef struct Ora_Define { + ub1 buf[MAX_ITEM_BUFFER_SIZE]; + float flt_buf; + sword int_buf; + sb2 indp; + ub2 col_retlen, col_retcode; +} Ora_Define; + +/* Structure and anchor for caching connections. */ + +typedef struct oracle_connection { + struct oracle_connection *next; + uschar *server; + struct cda_def *handle; + void *hda_mem; +} oracle_connection; + +static oracle_connection *oracle_connections = NULL; + + + + + +/************************************************* +* Set up message after error * +*************************************************/ + +/* Sets up a message from a local string plus whatever Oracle gives. + +Arguments: + oracle_handle the handle of the connection + rc the return code + msg local text message +*/ + +static uschar * +oracle_error(struct cda_def *oracle_handle, int rc, uschar *msg) +{ +uschar tmp[1024]; +oerhms(oracle_handle, rc, tmp, sizeof(tmp)); +return string_sprintf("ORACLE %s: %s", msg, tmp); +} + + + +/************************************************* +* Describe and define the select list items * +*************************************************/ + +/* Figures out sizes, types, and numbers. + +Arguments: + cda the connection + def + desc descriptions put here + +Returns: number of fields +*/ + +static sword +describe_define(Cda_Def *cda, Ora_Define *def, Ora_Describe *desc) +{ +sword col, deflen, deftyp; +static ub1 *defptr; +static sword numwidth = 8; + +/* Describe the select-list items. */ + +for (col = 0; col < MAX_SELECT_LIST_SIZE; col++) + { + desc[col].buflen = MAX_ITEM_BUFFER_SIZE; + + if (odescr(cda, col + 1, &desc[col].dbsize, + &desc[col].dbtype, &desc[col].buf[0], + &desc[col].buflen, &desc[col].dsize, + &desc[col].precision, &desc[col].scale, + &desc[col].nullok) != 0) + { + /* Break on end of select list. */ + if (cda->rc == VAR_NOT_IN_LIST) break; else return -1; + } + + /* Adjust sizes and types for display, handling NUMBER with scale as float. */ + + if (desc[col].dbtype == NUMBER_TYPE) + { + desc[col].dbsize = numwidth; + if (desc[col].scale != 0) + { + defptr = (ub1 *)&def[col].flt_buf; + deflen = (sword) sizeof(float); + deftyp = FLOAT_TYPE; + desc[col].dbtype = FLOAT_TYPE; + } + else + { + defptr = (ub1 *)&def[col].int_buf; + deflen = (sword) sizeof(sword); + deftyp = INT_TYPE; + desc[col].dbtype = INT_TYPE; + } + } + else + { + if (desc[col].dbtype == DATE_TYPE) + desc[col].dbsize = 9; + if (desc[col].dbtype == ROWID_TYPE) + desc[col].dbsize = 18; + defptr = def[col].buf; + deflen = desc[col].dbsize > MAX_ITEM_BUFFER_SIZE ? + MAX_ITEM_BUFFER_SIZE : desc[col].dbsize + 1; + deftyp = STRING_TYPE; + desc[col].dbtype = STRING_TYPE; + } + + /* Define an output variable */ + + if (odefin(cda, col + 1, + defptr, deflen, deftyp, + -1, &def[col].indp, (text *) 0, -1, -1, + &def[col].col_retlen, + &def[col].col_retcode) != 0) + return -1; + } /* Loop for each column */ + +return col; +} + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +oracle_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void +oracle_tidy(void) +{ +oracle_connection *cn; +while ((cn = oracle_connections) != NULL) + { + oracle_connections = cn->next; + DEBUG(D_lookup) debug_printf("close ORACLE connection: %s\n", cn->server); + ologof(cn->handle); + } +} + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* 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 + +The server string is of the form "host/dbname/user/password", for compatibility +with MySQL and pgsql, but at present, the dbname is not used. This string is in +a nextinlist temporary buffer, so can be overwritten. + +Returns: OK, FAIL, or DEFER +*/ + +static int +perform_oracle_search(uschar *query, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break) +{ +Cda_Def *cda = NULL; +struct cda_def *oracle_handle = NULL; +Ora_Describe *desc = NULL; +Ora_Define *def = NULL; +void *hda = NULL; + +int i; +int yield = DEFER; +unsigned int num_fields = 0; +gstring * result = NULL; +oracle_connection *cn = NULL; +uschar *server_copy = NULL; +uschar *sdata[4]; + +/* Disaggregate the parameters from the server argument. The order is host, +database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 3; i > 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (pp == NULL) + { + *errmsg = string_sprintf("incomplete ORACLE server data: %s", server); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 3) server_copy = string_copy(server); /* sans password */ + } +sdata[0] = server; /* What's left at the start */ + +/* If the database is the empty string, set it NULL - the query must then +define it. */ + +if (sdata[1][0] == 0) sdata[1] = NULL; + +/* See if we have a cached connection to the server */ + +for (cn = oracle_connections; cn; cn = cn->next) + if (strcmp(cn->server, server_copy) == 0) + { + oracle_handle = cn->handle; + hda = cn->hda_mem; + break; + } + +/* If no cached connection, we must set one up */ + +if (!cn) + { + DEBUG(D_lookup) debug_printf("ORACLE new connection: host=%s database=%s " + "user=%s\n", sdata[0], sdata[1], sdata[2]); + + /* Get store for a new connection, initialize it, and connect to the server */ + + oracle_handle = store_get(sizeof(struct cda_def)); + hda = store_get(HDA_SIZE); + memset(hda,'\0',HDA_SIZE); + + /* + * Perform a default (blocking) login + * + * sdata[0] = tnsname (service name - typically host name) + * sdata[1] = dbname - not used at present + * sdata[2] = username + * sdata[3] = passwd + */ + + if(olog(oracle_handle, hda, sdata[2], -1, sdata[3], -1, sdata[0], -1, + (ub4)OCI_LM_DEF) != 0) + { + *errmsg = oracle_error(oracle_handle, oracle_handle->rc, + US"connection failed"); + *defer_break = FALSE; + goto ORACLE_EXIT_NO_VALS; + } + + /* Add the connection to the cache */ + + cn = store_get(sizeof(oracle_connection)); + cn->server = server_copy; + cn->handle = oracle_handle; + cn->next = oracle_connections; + cn->hda_mem = hda; + oracle_connections = cn; + } + +/* Else use a previously cached connection - we can write to the server string +to obliterate the password because it is in a nextinlist temporary buffer. */ + +else + { + DEBUG(D_lookup) + debug_printf("ORACLE using cached connection for %s\n", server_copy); + } + +/* We have a connection. Open a cursor and run the query */ + +cda = store_get(sizeof(Cda_Def)); + +if (oopen(cda, oracle_handle, (text *)0, -1, -1, (text *)0, -1) != 0) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "failed to open cursor"); + *defer_break = FALSE; + goto ORACLE_EXIT_NO_VALS; + } + +if (oparse(cda, (text *)query, (sb4) -1, + (sword)PARSE_NO_DEFER, (ub4)PARSE_V7_LNG) != 0) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "query failed"); + *defer_break = FALSE; + oclose(cda); + goto ORACLE_EXIT_NO_VALS; + } + +/* Find the number of fields returned and sort out their types. If the number +is one, we don't add field names to the data. Otherwise we do. */ + +def = store_get(sizeof(Ora_Define)*MAX_SELECT_LIST_SIZE); +desc = store_get(sizeof(Ora_Describe)*MAX_SELECT_LIST_SIZE); + +if ((num_fields = describe_define(cda,def,desc)) == -1) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "describe_define failed"); + *defer_break = FALSE; + goto ORACLE_EXIT; + } + +if (oexec(cda)!=0) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "oexec failed"); + *defer_break = FALSE; + goto ORACLE_EXIT; + } + +/* Get the fields and construct the result string. If there is more than one +row, we insert '\n' between them. */ + +while (cda->rc != NO_DATA_FOUND) /* Loop for each row */ + { + ofetch(cda); + if(cda->rc == NO_DATA_FOUND) break; + + if (result) result = string_catn(result, "\n", 1); + + /* Single field - just add on the data */ + + if (num_fields == 1) + result = string_catn(result, def[0].buf, def[0].col_retlen); + + /* Multiple fields - precede by file name, removing {lead,trail}ing WS */ + + else for (i = 0; i < num_fields; i++) + { + int slen; + uschar *s = US desc[i].buf; + + while (*s != 0 && isspace(*s)) s++; + slen = Ustrlen(s); + while (slen > 0 && isspace(s[slen-1])) slen--; + result = string_catn(result, s, slen); + result = string_catn(result, US"=", 1); + + /* int and float type won't ever need escaping. Otherwise, quote the value + if it contains spaces or is empty. */ + + if (desc[i].dbtype != INT_TYPE && desc[i].dbtype != FLOAT_TYPE && + (def[i].buf[0] == 0 || strchr(def[i].buf, ' ') != NULL)) + { + int j; + result = string_catn(result, "\"", 1); + for (j = 0; j < def[i].col_retlen; j++) + { + if (def[i].buf[j] == '\"' || def[i].buf[j] == '\\') + result = string_catn(result, "\\", 1); + result = string_catn(result, def[i].buf+j, 1); + } + result = string_catn(result, "\"", 1); + } + + else switch(desc[i].dbtype) + { + case INT_TYPE: + result = string_cat(result, string_sprintf("%d", def[i].int_buf)); + break; + + case FLOAT_TYPE: + result = string_cat(result, string_sprintf("%f", def[i].flt_buf)); + break; + + case STRING_TYPE: + result = string_catn(result, def[i].buf, def[i].col_retlen); + break; + + default: + *errmsg = string_sprintf("ORACLE: unknown field type %d", desc[i].dbtype); + *defer_break = FALSE; + result = NULL; + goto ORACLE_EXIT; + } + + result = string_catn(result, " ", 1); + } + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + +if (!result) + { + yield = FAIL; + *errmsg = "ORACLE: no data found"; + } +else + store_reset(result->s + result->ptr + 1); + +/* Get here by goto from various error checks. */ + +ORACLE_EXIT: + +/* Close the cursor; don't close the connection, as it is cached. */ + +oclose(cda); + +ORACLE_EXIT_NO_VALS: + +/* Non-NULL result indicates a successful result */ + +if (result) + { + *resultptr = string_from_gstring(result); + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return yield; /* FAIL or DEFER */ + } +} + + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The handle and filename +arguments are not used. Loop through a list of servers while the query is +deferred with a retryable error. */ + +static int +oracle_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +int sep = 0; +uschar *server; +uschar *list = oracle_servers; +uschar buffer[512]; + +do_cache = do_cache; /* Placate picky compilers */ + +DEBUG(D_lookup) debug_printf("ORACLE query: %s\n", query); + +while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) + { + BOOL defer_break; + int rc = perform_oracle_search(query, server, result, errmsg, &defer_break); + if (rc != DEFER || defer_break) return rc; + } + +if (oracle_servers == NULL) + *errmsg = "no ORACLE servers defined (oracle_servers option)"; + +return DEFER; +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only characters that need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent and underscore are not escaped. They are only special in contexts where +they can be wild cards, and this isn't usually the case for data inserted from +messages, since that isn't likely to be treated as a pattern of any kind. + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +oracle_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options are recognized */ + +while ((c = *t++) != 0) + if (strchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +if (count == 0) return s; +t = quoted = store_get((int)strlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (strchr("\n\t\r\b\'\"\\", c) != NULL) + { + *t++ = '\\'; + switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + } + } + else *t++ = c; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +oracle_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: Oracle: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"oracle", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + oracle_open, /* open function */ + NULL, /* check function */ + oracle_find, /* find function */ + NULL, /* no close function */ + oracle_tidy, /* tidy function */ + oracle_quote, /* quoting function */ + oracle_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define oracle_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info oracle_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/oracle.c */ diff --git a/src/lookups/passwd.c b/src/lookups/passwd.c new file mode 100644 index 0000000..315677f --- /dev/null +++ b/src/lookups/passwd.c @@ -0,0 +1,90 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +static void * +passwd_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +return (void *)(-1); /* Just return something non-null */ +} + + + + +/************************************************* +* Find entry point for passwd * +*************************************************/ + +/* See local README for interface description */ + +static int +passwd_find(void *handle, uschar *filename, const uschar *keystring, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +struct passwd *pw; + +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; +errmsg = errmsg; +do_cache = do_cache; + +if (!route_finduser(keystring, &pw, NULL)) return FAIL; +*result = string_sprintf("*:%d:%d:%s:%s:%s", (int)pw->pw_uid, (int)pw->pw_gid, + pw->pw_gecos, pw->pw_dir, pw->pw_shell); +return OK; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +passwd_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: passwd: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + +static lookup_info _lookup_info = { + US"passwd", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + passwd_open, /* open function */ + NULL, /* no check function */ + passwd_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + passwd_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define passwd_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info passwd_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/passwd.c */ diff --git a/src/lookups/pgsql.c b/src/lookups/pgsql.c new file mode 100644 index 0000000..697285a --- /dev/null +++ b/src/lookups/pgsql.c @@ -0,0 +1,510 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Thanks to Petr Cech for contributing the original code for these +functions. Thanks to Joachim Wieland for the initial patch for the Unix domain +socket extension. */ + +#include "../exim.h" +#include "lf_functions.h" + +#include <libpq-fe.h> /* The system header */ + +/* Structure and anchor for caching connections. */ + +typedef struct pgsql_connection { + struct pgsql_connection *next; + uschar *server; + PGconn *handle; +} pgsql_connection; + +static pgsql_connection *pgsql_connections = NULL; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +pgsql_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void +pgsql_tidy(void) +{ +pgsql_connection *cn; +while ((cn = pgsql_connections) != NULL) + { + pgsql_connections = cn->next; + DEBUG(D_lookup) debug_printf("close PGSQL connection: %s\n", cn->server); + PQfinish(cn->handle); + } +} + + +/************************************************* +* Notice processor function for pgsql * +*************************************************/ + +/* This function is passed to pgsql below, and called for any PostgreSQL +"notices". By default they are written to stderr, which is undesirable. + +Arguments: + arg an opaque user cookie (not used) + message the notice + +Returns: nothing +*/ + +static void +notice_processor(void *arg, const char *message) +{ +arg = arg; /* Keep compiler happy */ +DEBUG(D_lookup) debug_printf("PGSQL: %s\n", message); +} + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* This function is called from the find entry point to do the search for a +single server. The server string is of the form "server/dbname/user/password". + +PostgreSQL supports connections through Unix domain sockets. This is usually +faster and costs less cpu time than a TCP/IP connection. However it can only be +used if the mail server runs on the same machine as the database server. A +configuration line for PostgreSQL via Unix domain sockets looks like this: + +hide pgsql_servers = (/tmp/.s.PGSQL.5432)/db/user/password[:<nextserver>] + +We enclose the path name in parentheses so that its slashes aren't visually +confused with the delimiters for the other pgsql_server settings. + +For TCP/IP connections, the server is a host name and optional port (with a +colon separator). + +NOTE: + 1) All three '/' must be present. + 2) If host is omitted the local unix socket is used. + +Arguments: + query the query string + server the server string; this is in dynamic memory and can be updated + resultptr where to store the result + errmsg where to point an error message + defer_break set TRUE if no more servers are to be tried after DEFER + do_cache set FALSE if data is changed + +Returns: OK, FAIL, or DEFER +*/ + +static int +perform_pgsql_search(const uschar *query, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break, uint *do_cache) +{ +PGconn *pg_conn = NULL; +PGresult *pg_result = NULL; + +int i; +gstring * result = NULL; +int yield = DEFER; +unsigned int num_fields, num_tuples; +pgsql_connection *cn; +uschar *server_copy = NULL; +uschar *sdata[3]; + +/* Disaggregate the parameters from the server argument. The order is host or +path, database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 2; i >= 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (!pp) + { + *errmsg = string_sprintf("incomplete pgSQL 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 */ + } + +/* The total server string has now been truncated so that what is left at the +start is the identification of the server (host or path). See if we have a +cached connection to the server. */ + +for (cn = pgsql_connections; cn; cn = cn->next) + if (Ustrcmp(cn->server, server_copy) == 0) + { + pg_conn = cn->handle; + break; + } + +/* If there is no cached connection, we must set one up. */ + +if (!cn) + { + uschar *port = US""; + + /* For a Unix domain socket connection, the path is in parentheses */ + + if (*server == '(') + { + uschar *last_slash, *last_dot, *p; + + p = ++server; + while (*p && *p != ')') p++; + *p = 0; + + last_slash = Ustrrchr(server, '/'); + last_dot = Ustrrchr(server, '.'); + + DEBUG(D_lookup) debug_printf("PGSQL new connection: socket=%s " + "database=%s user=%s\n", server, sdata[0], sdata[1]); + + /* A valid socket name looks like this: /var/run/postgresql/.s.PGSQL.5432 + We have to call PQsetdbLogin with '/var/run/postgresql' as the hostname + argument and put '5432' into the port variable. */ + + if (!last_slash || !last_dot) + { + *errmsg = string_sprintf("PGSQL invalid filename for socket: %s", server); + *defer_break = TRUE; + return DEFER; + } + + /* Terminate the path name and set up the port: we'll have something like + server = "/var/run/postgresql" and port = "5432". */ + + *last_slash = 0; + port = last_dot + 1; + } + + /* Host connection; sort out the port */ + + else + { + uschar *p; + if ((p = Ustrchr(server, ':'))) + { + *p++ = 0; + port = p; + } + + if (Ustrchr(server, '/')) + { + *errmsg = string_sprintf("unexpected slash in pgSQL server hostname: %s", + server); + *defer_break = TRUE; + return DEFER; + } + + DEBUG(D_lookup) debug_printf("PGSQL new connection: host=%s port=%s " + "database=%s user=%s\n", server, port, sdata[0], sdata[1]); + } + + /* If the database is the empty string, set it NULL - the query must then + define it. */ + + if (sdata[0][0] == 0) sdata[0] = NULL; + + /* Get store for a new handle, initialize it, and connect to the server */ + + pg_conn=PQsetdbLogin( + /* host port options tty database user passwd */ + CS server, CS port, NULL, NULL, CS sdata[0], CS sdata[1], CS sdata[2]); + + if(PQstatus(pg_conn) == CONNECTION_BAD) + { + store_reset(server_copy); + *errmsg = string_sprintf("PGSQL connection failed: %s", + PQerrorMessage(pg_conn)); + PQfinish(pg_conn); + goto PGSQL_EXIT; + } + + /* Set the client encoding to SQL_ASCII, which means that the server will + not try to interpret the query as being in any fancy encoding such as UTF-8 + or other multibyte code that might cause problems with escaping. */ + + PQsetClientEncoding(pg_conn, "SQL_ASCII"); + + /* Set the notice processor to prevent notices from being written to stderr + (which is what the default does). Our function (above) just produces debug + output. */ + + PQsetNoticeProcessor(pg_conn, notice_processor, NULL); + + /* Add the connection to the cache */ + + cn = store_get(sizeof(pgsql_connection)); + cn->server = server_copy; + cn->handle = pg_conn; + cn->next = pgsql_connections; + pgsql_connections = cn; + } + +/* Else use a previously cached connection */ + +else + { + DEBUG(D_lookup) debug_printf("PGSQL using cached connection for %s\n", + server_copy); + } + +/* Run the query */ + +pg_result = PQexec(pg_conn, CS query); +switch(PQresultStatus(pg_result)) + { + case PGRES_EMPTY_QUERY: + case PGRES_COMMAND_OK: + /* The command was successful but did not return any data since it was + not SELECT but either an INSERT, UPDATE or DELETE statement. Tell the + high level code to not cache this query, and clean the current cache for + this handle by setting *do_cache zero. */ + + result = string_cat(result, US PQcmdTuples(pg_result)); + *do_cache = 0; + DEBUG(D_lookup) debug_printf("PGSQL: command does not return any data " + "but was successful. Rows affected: %s\n", string_from_gstring(result)); + break; + + case PGRES_TUPLES_OK: + break; + + default: + /* This was the original code: + *errmsg = string_sprintf("PGSQL: query failed: %s\n", + PQresultErrorMessage(pg_result)); + This was suggested by a user: + */ + + *errmsg = string_sprintf("PGSQL: query failed: %s (%s) (%s)\n", + PQresultErrorMessage(pg_result), + PQresStatus(PQresultStatus(pg_result)), query); + goto PGSQL_EXIT; + } + +/* Result is in pg_result. Find the number of fields returned. If this is one, +we don't add field names to the data. Otherwise we do. If the query did not +return anything we skip the for loop; this also applies to the case +PGRES_COMMAND_OK. */ + +num_fields = PQnfields(pg_result); +num_tuples = PQntuples(pg_result); + +/* Get the fields and construct the result string. If there is more than one +row, we insert '\n' between them. */ + +for (i = 0; i < num_tuples; i++) + { + if (result) + result = string_catn(result, US"\n", 1); + + if (num_fields == 1) + result = string_catn(result, + US PQgetvalue(pg_result, i, 0), PQgetlength(pg_result, i, 0)); + else + { + int j; + for (j = 0; j < num_fields; j++) + { + uschar *tmp = US PQgetvalue(pg_result, i, j); + result = lf_quote(US PQfname(pg_result, j), tmp, Ustrlen(tmp), result); + } + } + } + +/* If result is NULL then no data has been found and so we return FAIL. */ + +if (!result) + { + yield = FAIL; + *errmsg = US"PGSQL: no data found"; + } + +/* Get here by goto from various error checks. */ + +PGSQL_EXIT: + +/* Free store for any result that was got; don't close the connection, as +it is cached. */ + +if (pg_result) PQclear(pg_result); + +/* Non-NULL result indicates a successful result */ + +if (result) + { + store_reset(result->s + result->ptr + 1); + *resultptr = string_from_gstring(result); + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + 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 SQL lookups. */ + +static int +pgsql_find(void *handle, uschar *filename, const uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +return lf_sqlperform(US"PostgreSQL", US"pgsql_servers", pgsql_servers, query, + result, errmsg, do_cache, perform_pgsql_search); +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The characters that always need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. + +The original code quoted single quotes as \' which is documented as valid in +the O'Reilly book "Practical PostgreSQL" (first edition) as an alternative to +the SQL standard '' way of representing a single quote as data. However, in +June 2006 there was some security issue with using \' and so this has been +changed. + +[Note: There is a function called PQescapeStringConn() that quotes strings. +This cannot be used because it needs a PGconn argument (the connection handle). +Why, I don't know. Seems odd for just string escaping...] + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +pgsql_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options recognized */ + +while ((c = *t++) != 0) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +if (count == 0) return s; +t = quoted = store_get(Ustrlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (c == '\'') + { + *t++ = '\''; + *t++ = '\''; + } + else if (Ustrchr("\n\t\r\b\"\\", c) != NULL) + { + *t++ = '\\'; + switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + } + } + else *t++ = c; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +pgsql_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: PostgreSQL: Exim version %s\n", EXIM_VERSION_STR); +#endif + +/* Version reporting: there appears to be no available information about +the client library in libpq-fe.h; once you have a connection object, you +can access the server version and the chosen protocol version, but those +aren't really what we want. It might make sense to debug_printf those +when the connection is established though? */ +} + + +static lookup_info _lookup_info = { + US"pgsql", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + pgsql_open, /* open function */ + NULL, /* no check function */ + pgsql_find, /* find function */ + NULL, /* no close function */ + pgsql_tidy, /* tidy function */ + pgsql_quote, /* quoting function */ + pgsql_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define pgsql_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info pgsql_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/pgsql.c */ diff --git a/src/lookups/redis.c b/src/lookups/redis.c new file mode 100644 index 0000000..a4b672a --- /dev/null +++ b/src/lookups/redis.c @@ -0,0 +1,471 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +#ifdef LOOKUP_REDIS + +#include "lf_functions.h" + +#include <hiredis/hiredis.h> + +#ifndef nele +# define nele(arr) (sizeof(arr) / sizeof(*arr)) +#endif + +/* 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)) + { + 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(const uschar *command, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break, uint *do_cache) +{ +redisContext *redis_handle = NULL; /* Keep compilers happy */ +redisReply *redis_reply = NULL; +redisReply *entry = NULL; +redisReply *tentry = NULL; +redis_connection *cn; +int yield = DEFER; +int i, j; +gstring * result = NULL; +uschar *server_copy = NULL; +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) + { + *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; cn = cn->next) + if (Ustrcmp(cn->server, server_copy) == 0) + { + redis_handle = cn->handle; + break; + } + +if (!cn) + { + uschar *p; + uschar *socket = NULL; + int port = 0; + /* int redis_err = REDIS_OK; */ + + if ((p = Ustrchr(sdata[0], '('))) + { + *p++ = 0; + socket = p; + while (*p != 0 && *p != ')') p++; + *p = 0; + } + + if ((p = Ustrchr(sdata[0], ':'))) + { + *p++ = 0; + port = Uatoi(p); + } + else + port = Uatoi("6379"); + + if (Ustrchr(server, '/')) + { + *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 ? */ + redis_handle = + socket ? redisConnectUnix(CCS socket) : redisConnect(CCS server, port); + if (!redis_handle) + { + *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]) + if (!(redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2]))) + { + *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]) + { + if (!(redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1]))) + { + *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr); + *defer_break = FALSE; + goto REDIS_EXIT; + } + DEBUG(D_lookup) debug_printf("REDIS: Selecting database=%s\n", sdata[1]); + } + +/* split string on whitespace into argv */ + { + uschar * argv[32]; + int i; + const uschar * s = command; + int siz, ptr; + uschar c; + + while (isspace(*s)) s++; + + for (i = 0; *s && i < nele(argv); i++) + { + gstring * g; + + for (g = NULL; (c = *s) && !isspace(c); s++) + if (c != '\\' || *++s) /* backslash protects next char */ + g = string_catn(g, s, 1); + argv[i] = string_from_gstring(g); + + DEBUG(D_lookup) debug_printf("REDIS: argv[%d] '%s'\n", i, argv[i]); + while (isspace(*s)) s++; + } + + /* Run the command. We use the argv form rather than plain as that parses + into args by whitespace yet has no escaping mechanism. */ + + redis_reply = redisCommandArgv(redis_handle, i, (const char **) argv, NULL); + if (!redis_reply) + { + *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); + + /* trap MOVED cluster responses and follow them */ + if (Ustrncmp(redis_reply->str, "MOVED", 5) == 0) + { + DEBUG(D_lookup) + debug_printf("REDIS: cluster redirect %s\n", redis_reply->str); + /* follow redirect + This is cheating, we simply set defer_break = FALSE to move on to + the next server in the redis_servers list */ + *defer_break = FALSE; + return DEFER; + } else { + *defer_break = TRUE; + } + *do_cache = 0; + goto REDIS_EXIT; + /* NOTREACHED */ + + case REDIS_REPLY_NIL: + DEBUG(D_lookup) + debug_printf("REDIS: query was not one that returned any data\n"); + result = string_catn(result, US"", 1); + *do_cache = 0; + goto REDIS_EXIT; + /* NOTREACHED */ + + case REDIS_REPLY_INTEGER: + result = string_cat(result, redis_reply->integer != 0 ? US"true" : US"false"); + break; + + case REDIS_REPLY_STRING: + case REDIS_REPLY_STATUS: + result = string_catn(result, 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) + result = string_catn(result, US"\n", 1); + + switch (entry->type) + { + case REDIS_REPLY_INTEGER: + result = string_fmt_append(result, "%d", entry->integer); + break; + case REDIS_REPLY_STRING: + result = string_catn(result, US entry->str, entry->len); + break; + case REDIS_REPLY_ARRAY: + for (j = 0; j < entry->elements; j++) + { + tentry = entry->element[j]; + + if (result) + result = string_catn(result, US"\n", 1); + + switch (tentry->type) + { + case REDIS_REPLY_INTEGER: + result = string_fmt_append(result, "%d", tentry->integer); + break; + case REDIS_REPLY_STRING: + result = string_catn(result, 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) + store_reset(result->s + result->ptr + 1); +else + { + yield = FAIL; + *errmsg = US"REDIS: no data found"; + } + +REDIS_EXIT: + +/* Free store for any result that was got; don't close the connection, +as it is cached. */ + +if (redis_reply) freeReplyObject(redis_reply); + +/* Non-NULL result indicates a successful result */ + +if (result) + { + *resultptr = string_from_gstring(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)), + const uschar *command, int length, uschar **result, uschar **errmsg, + uint *do_cache) +{ +return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command, + result, errmsg, do_cache, perform_redis_search); +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* Prefix any whitespace, or backslash, with a backslash. +This is not a Redis thing but instead to let the argv splitting +we do to split on whitespace, yet provide means for getting +whitespace into an argument. + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +redis_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt) return NULL; /* No options recognized */ + +while ((c = *t++) != 0) + if (isspace(c) || c == '\\') count++; + +if (count == 0) return s; +t = quoted = store_get(Ustrlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (isspace(c) || c == '\\') *t++ = '\\'; + *t++ = c; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* 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 */ + redis_quote, /* 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 /* LOOKUP_REDIS */ +/* End of lookups/redis.c */ diff --git a/src/lookups/spf.c b/src/lookups/spf.c new file mode 100644 index 0000000..b32a73e --- /dev/null +++ b/src/lookups/spf.c @@ -0,0 +1,121 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * Exim - SPF lookup module using libspf2 + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 2005 Chris Webb, Arachsys Internet Services Ltd + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * Copyright (c) The Exim Maintainers 2016 + */ + +#include "../exim.h" + +#ifndef SUPPORT_SPF +static void dummy(int x); +static void dummy2(int x) { dummy(x-1); } +static void dummy(int x) { dummy2(x-1); } +#else + +#include "lf_functions.h" +#ifndef HAVE_NS_TYPE +#define HAVE_NS_TYPE +#endif +#include <spf2/spf.h> +#include <spf2/spf_dns_resolv.h> +#include <spf2/spf_dns_cache.h> + +static void * +spf_open(uschar *filename, uschar **errmsg) +{ + SPF_server_t *spf_server = NULL; + spf_server = SPF_server_new(SPF_DNS_CACHE, 0); + if (spf_server == NULL) { + *errmsg = US"SPF_server_new() failed"; + return NULL; + } + return (void *) spf_server; +} + +static void +spf_close(void *handle) +{ + SPF_server_t *spf_server = handle; + if (spf_server) SPF_server_free(spf_server); +} + +static int +spf_find(void *handle, uschar *filename, const uschar *keystring, int key_len, + uschar **result, uschar **errmsg, uint *do_cache) +{ + SPF_server_t *spf_server = handle; + SPF_request_t *spf_request = NULL; + SPF_response_t *spf_response = NULL; + + spf_request = SPF_request_new(spf_server); + if (spf_request == NULL) { + *errmsg = US"SPF_request_new() failed"; + return FAIL; + } + + if (SPF_request_set_ipv4_str(spf_request, CS filename)) { + *errmsg = string_sprintf("invalid IP address '%s'", filename); + return FAIL; + } + if (SPF_request_set_env_from(spf_request, CS keystring)) { + *errmsg = string_sprintf("invalid envelope from address '%s'", keystring); + return FAIL; + } + + SPF_request_query_mailfrom(spf_request, &spf_response); + *result = string_copy(US SPF_strresult(SPF_response_result(spf_response))); + SPF_response_free(spf_response); + SPF_request_free(spf_request); + return OK; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +spf_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: SPF: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"spf", /* lookup name */ + 0, /* not absfile, not query style */ + spf_open, /* open function */ + NULL, /* no check function */ + spf_find, /* find function */ + spf_close, /* close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + spf_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define spf_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info spf_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +#endif /* SUPPORT_SPF */ diff --git a/src/lookups/sqlite.c b/src/lookups/sqlite.c new file mode 100644 index 0000000..1619429 --- /dev/null +++ b/src/lookups/sqlite.c @@ -0,0 +1,187 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + +#include <sqlite3.h> + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +sqlite_open(uschar *filename, uschar **errmsg) +{ +sqlite3 *db = NULL; +int ret; + +ret = sqlite3_open(CS filename, &db); +if (ret != 0) + { + *errmsg = (void *)sqlite3_errmsg(db); + debug_printf("Error opening database: %s\n", *errmsg); + } + +sqlite3_busy_timeout(db, 1000 * sqlite_lock_timeout); +return db; +} + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +static int +sqlite_callback(void *arg, int argc, char **argv, char **azColName) +{ +gstring * res = *(gstring **)arg; +int i; + +/* For second and subsequent results, insert \n */ + +if (res) + res = string_catn(res, US"\n", 1); + +if (argc > 1) + { + /* For multiple fields, include the field name too */ + for (i = 0; i < argc; i++) + { + uschar *value = US((argv[i] != NULL)? argv[i]:"<NULL>"); + res = lf_quote(US azColName[i], value, Ustrlen(value), res); + } + } + +else + res = string_cat(res, argv[0] ? US argv[0] : US "<NULL>"); + +*(gstring **)arg = res; +return 0; +} + + +static int +sqlite_find(void *handle, uschar *filename, const uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +int ret; +gstring * res = NULL; + +ret = sqlite3_exec(handle, CS query, sqlite_callback, &res, (char **)errmsg); +if (ret != SQLITE_OK) + { + debug_printf("sqlite3_exec failed: %s\n", *errmsg); + return FAIL; + } + +if (!res) *do_cache = 0; + +*result = string_from_gstring(res); +return OK; +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void sqlite_close(void *handle) +{ +sqlite3_close(handle); +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* From what I have found so far, the only character that needs to be quoted +for sqlite is the single quote, and it is quoted by doubling. + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +sqlite_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options recognized */ + +while ((c = *t++) != 0) if (c == '\'') count++; + +if (count == 0) return s; +t = quoted = store_get(Ustrlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (c == '\'') *t++ = '\''; + *t++ = c; + } + +*t = 0; +return quoted; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +sqlite_version_report(FILE *f) +{ +fprintf(f, "Library version: SQLite: Compile: %s\n" + " Runtime: %s\n", + SQLITE_VERSION, sqlite3_libversion()); +#ifdef DYNLOOKUP +fprintf(f, " Exim version %s\n", EXIM_VERSION_STR); +#endif +} + +static lookup_info _lookup_info = { + US"sqlite", /* lookup name */ + lookup_absfilequery, /* query-style lookup, starts with file name */ + sqlite_open, /* open function */ + NULL, /* no check function */ + sqlite_find, /* find function */ + sqlite_close, /* close function */ + NULL, /* no tidy function */ + sqlite_quote, /* quoting function */ + sqlite_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define sqlite_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info sqlite_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/sqlite.c */ diff --git a/src/lookups/testdb.c b/src/lookups/testdb.c new file mode 100644 index 0000000..401f7c8 --- /dev/null +++ b/src/lookups/testdb.c @@ -0,0 +1,103 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + + +/* These are not real lookup functions; they are just a way of testing the +rest of Exim by providing an easy way of specifying particular yields from +the find function. */ + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +testdb_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +static int +testdb_find(void *handle, uschar *filename, const uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; + +if (Ustrcmp(query, "fail") == 0) + { + *errmsg = US"testdb lookup forced FAIL"; + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return FAIL; + } +if (Ustrcmp(query, "defer") == 0) + { + *errmsg = US"testdb lookup forced DEFER"; + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + +if (Ustrcmp(query, "nocache") == 0) *do_cache = 0; + +*result = string_copy(query); +return OK; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +testdb_version_report(FILE *f) +{ +#ifdef DYNLOOKUP +fprintf(f, "Library version: TestDB: Exim version %s\n", EXIM_VERSION_STR); +#endif +} + + +static lookup_info _lookup_info = { + US"testdb", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + testdb_open, /* open function */ + NULL, /* check function */ + testdb_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + testdb_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define testdb_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info testdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/testdb.c */ diff --git a/src/lookups/whoson.c b/src/lookups/whoson.c new file mode 100644 index 0000000..8f065e6 --- /dev/null +++ b/src/lookups/whoson.c @@ -0,0 +1,101 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This code originally came from Robert Wal. */ + +#include "../exim.h" + + +#include <whoson.h> /* Public header */ + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +static void * +whoson_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +return (void *)(1); /* Just return something non-null */ +} + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +static int +whoson_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, uint *do_cache) +{ +uschar buffer[80]; +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; +errmsg = errmsg; +do_cache = do_cache; + +switch (wso_query(CS query, CS buffer, sizeof(buffer))) + { + case 0: + *result = string_copy(buffer); /* IP in database; return name of user */ + return OK; + + case +1: + return FAIL; /* IP not in database */ + + default: + *errmsg = string_sprintf("WHOSON: failed to complete: %s", buffer); + return DEFER; + } +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +void +whoson_version_report(FILE *f) +{ +fprintf(f, "Library version: Whoson: Runtime: %s\n", wso_version()); +#ifdef DYNLOOKUP +fprintf(f, " Exim version %s\n", EXIM_VERSION_STR); +#endif +} + +static lookup_info _lookup_info = { + US"whoson", /* lookup name */ + lookup_querystyle, /* query-style lookup */ + whoson_open, /* open function */ + NULL, /* check function */ + whoson_find, /* find function */ + NULL, /* no close function */ + NULL, /* no tidy function */ + NULL, /* no quoting function */ + whoson_version_report /* version reporting */ +}; + +#ifdef DYNLOOKUP +#define whoson_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &_lookup_info }; +lookup_module_info whoson_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/whoson.c */ |