diff options
Diffstat (limited to 'src/lookups')
-rw-r--r-- | src/lookups/Makefile | 76 | ||||
-rw-r--r-- | src/lookups/README | 181 | ||||
-rw-r--r-- | src/lookups/cdb.c | 490 | ||||
-rw-r--r-- | src/lookups/dbmdb.c | 284 | ||||
-rw-r--r-- | src/lookups/dnsdb.c | 614 | ||||
-rw-r--r-- | src/lookups/dsearch.c | 190 | ||||
-rw-r--r-- | src/lookups/ibase.c | 561 | ||||
-rw-r--r-- | src/lookups/json.c | 188 | ||||
-rw-r--r-- | src/lookups/ldap.c | 1613 | ||||
-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 | 20 | ||||
-rw-r--r-- | src/lookups/lf_quote.c | 63 | ||||
-rw-r--r-- | src/lookups/lf_sqlperform.c | 175 | ||||
-rw-r--r-- | src/lookups/lmdb.c | 162 | ||||
-rw-r--r-- | src/lookups/lsearch.c | 487 | ||||
-rw-r--r-- | src/lookups/mysql.c | 503 | ||||
-rw-r--r-- | src/lookups/nis.c | 141 | ||||
-rw-r--r-- | src/lookups/nisplus.c | 294 | ||||
-rw-r--r-- | src/lookups/oracle.c | 628 | ||||
-rw-r--r-- | src/lookups/passwd.c | 85 | ||||
-rw-r--r-- | src/lookups/pgsql.c | 506 | ||||
-rw-r--r-- | src/lookups/readsock.c | 341 | ||||
-rw-r--r-- | src/lookups/redis.c | 471 | ||||
-rw-r--r-- | src/lookups/spf.c | 159 | ||||
-rw-r--r-- | src/lookups/sqlite.c | 200 | ||||
-rw-r--r-- | src/lookups/testdb.c | 100 | ||||
-rw-r--r-- | src/lookups/whoson.c | 98 |
28 files changed, 8756 insertions, 0 deletions
diff --git a/src/lookups/Makefile b/src/lookups/Makefile new file mode 100644 index 0000000..19585bf --- /dev/null +++ b/src/lookups/Makefile @@ -0,0 +1,76 @@ +# Make file for building Exim's lookup modules. +# This is called from the main make file, after cd'ing +# to the lookups subdirectory. +# +# Copyright (c) The Exim Maintainers 2021 + +# 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: $(HDRS) lf_check_file.c lf_functions.h +lf_quote.o: $(HDRS) lf_quote.c lf_functions.h +lf_sqlperform.o: $(HDRS) lf_sqlperform.c lf_functions.h + +cdb.o: $(HDRS) cdb.c +dbmdb.o: $(HDRS) dbmdb.c +dnsdb.o: $(HDRS) dnsdb.c +dsearch.o: $(HDRS) dsearch.c +ibase.o: $(HDRS) ibase.c +ldap.o: $(HDRS) ldap.c +lmdb.o: $(HDRS) lmdb.c +json.o: $(HDRS) json.c +lsearch.o: $(HDRS) lsearch.c +mysql.o: $(HDRS) mysql.c +nis.o: $(HDRS) nis.c +nisplus.o: $(HDRS) nisplus.c +oracle.o: $(HDRS) oracle.c +passwd.o: $(HDRS) passwd.c +pgsql.o: $(HDRS) pgsql.c +readsock.o: $(HDRS) readsock.c +redis.o: $(HDRS) redis.c +spf.o: $(HDRS) spf.c +sqlite.o: $(HDRS) sqlite.c +testdb.o: $(HDRS) testdb.c +whoson.o: $(HDRS) whoson.c + +cdb.so: $(HDRS) cdb.c +dbmdb.so: $(HDRS) dbmdb.c +dnsdb.so: $(HDRS) dnsdb.c +dsearch.so: $(HDRS) dsearch.c +ibase.so: $(HDRS) ibase.c +json.so: $(HDRS) json.c +ldap.so: $(HDRS) ldap.c +lmdb.so: $(HDRS) lmdb.c +lsearch.so: $(HDRS) lsearch.c +mysql.so: $(HDRS) mysql.c +nis.so: $(HDRS) nis.c +nisplus.so: $(HDRS) nisplus.c +oracle.so: $(HDRS) oracle.c +passwd.so: $(HDRS) passwd.c +pgsql.so: $(HDRS) pgsql.c +redis.so: $(HDRS) redis.c +spf.so: $(HDRS) spf.c +sqlite.so: $(HDRS) sqlite.c +testdb.so: $(HDRS) testdb.c +whoson.so: $(HDRS) whoson.c + +# End diff --git a/src/lookups/README b/src/lookups/README new file mode 100644 index 0000000..2e87eda --- /dev/null +++ b/src/lookups/README @@ -0,0 +1,181 @@ +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. Traditionally, lookup_querystyle does not use this (just returning a +dummy value, and doing the "open" work in the xxx_find() routine); but this is +not enforced by the framework. + +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. + uschar *opts options, a comma-separated list of tagged values for + modifying the search operation + +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_version_report() +-------------------- + +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..966078f --- /dev/null +++ b/src/lookups/cdb.c @@ -0,0 +1,490 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * Exim - CDB database lookup module + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) The Exim Maintainers 2020 - 2022 + * 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(const uschar * filename, uschar ** errmsg) +{ +int fileno; +struct cdb_state *cdbp; +struct stat statbuf; +void * mapbuf; + +if ((fileno = Uopen(filename, O_RDONLY, 0)) < 0) + { + *errmsg = string_open_failed("%s for cdb lookup", filename); + return NULL; + } + +if (fstat(fileno, &statbuf) != 0) + { + *errmsg = string_open_failed("fstat(%s) failed - cannot do cdb lookup", + filename); + return NULL; + } + +/* If this is a valid file, then it *must* be at least +CDB_HASH_TABLE bytes long */ + +if (statbuf.st_size < CDB_HASH_TABLE) + { + *errmsg = string_open_failed("%s too short for cdb lookup", filename); + return NULL; + } + +/* Having got a file open we need the structure to put things in */ +cdbp = store_get(sizeof(struct cdb_state), GET_UNTAINTED); +/* 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 +if ((mapbuf = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fileno, 0)) + != 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); + } + +/* 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_indent("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, GET_UNTAINTED); + +/* 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("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, const 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, const uschar * filename, const uschar * keystring, + int key_len, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +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; + +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_indent("%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 (int 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. Assume it is not + tainted, lacking any way of telling. */ + + *result = store_get(item_dat_len + 1, GET_UNTAINTED); + 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 (int 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 */ + rmark reset_point = store_mark(); + uschar * item_key = store_get(key_len, GET_TAINTED); /* keys liable to be tainted */ + + 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(reset_point); + + /* 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, GET_UNTAINTED); + /* 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(reset_point); + } + } + 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" + +gstring * +cdb_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: CDB: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +lookup_info cdb_lookup_info = { + .name = US"cdb", /* lookup name */ + .type = lookup_absfile, /* absolute file name */ + .open = cdb_open, /* open function */ + .check = cdb_check, /* check function */ + .find = cdb_find, /* find function */ + .close = cdb_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..32514af --- /dev/null +++ b/src/lookups/dbmdb.c @@ -0,0 +1,284 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** errmsg) +{ +uschar * dirname = string_copy(filename); +uschar * s; +EXIM_DB * yield = NULL; + +if ((s = Ustrrchr(dirname, '/'))) *s = '\0'; +if (!(yield = exim_dbopen(filename, dirname, O_RDONLY, 0))) + *errmsg = string_open_failed("%s as a %s file", filename, EXIM_DBTYPE); +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, const uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +int rc; + +#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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +EXIM_DB *d = (EXIM_DB *)handle; +EXIM_DATUM key, data; + +exim_datum_init(&key); /* Some DBM libraries require datums to */ +exim_datum_init(&data); /* be cleared before use. */ +length++; +exim_datum_data_set(&key, + memcpy(store_get(length, keystring), keystring, length)); /* key can have embedded NUL */ +exim_datum_size_set(&key, length); + +if (exim_dbget(d, &key, &data)) + { + *result = string_copyn(exim_datum_data_get(&data), exim_datum_size_get(&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 */ + +static int +dbmnz_find(void * handle, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return dbmdb_find(handle, filename, keystring, length-1, result, errmsg, + do_cache, opts); +} + + + +/************************************************* +* 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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +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, keystring); + +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_indent("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, opts); +} + + + +/************************************************* +* 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" + +gstring * +dbm_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: DBM: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +lookup_info dbm_lookup_info = { + .name = US"dbm", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = dbmdb_open, /* open function */ + .check = dbmdb_check, /* check function */ + .find = dbmdb_find, /* find function */ + .close = dbmdb_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = dbm_version_report /* version reporting */ +}; + +lookup_info dbmz_lookup_info = { + .name = US"dbmnz", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = dbmdb_open, /* sic */ /* open function */ + .check = dbmdb_check, /* sic */ /* check function */ + .find = dbmnz_find, /* find function */ + .close = dbmdb_close, /* sic */ /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = NULL /* no version reporting (redundant) */ +}; + +lookup_info dbmjz_lookup_info = { + .name = US"dbmjz", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = dbmdb_open, /* sic */ /* open function */ + .check = dbmdb_check, /* sic */ /* check function */ + .find = dbmjz_find, /* find function */ + .close = dbmdb_close, /* sic */ /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..355be1b --- /dev/null +++ b/src/lookups/dnsdb.c @@ -0,0 +1,614 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar **errmsg) +{ +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' (default), and 'never'. 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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +int rc; +int sep = 0; +int defer_mode = PASS; +int dnssec_mode = PASS; +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; + +dns_answer * dnsa = store_get_dns_answer(); +dns_scan dnss; + +/* 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); + +/* 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"; + rc = DEFER; + goto out; + } + } + 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"; + rc = DEFER; + goto out; + } + } + 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"; + rc = DEFER; + goto out; + } + 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"; + rc = DEFER; + goto out; + } + dns_retry = retries; + } + else + break; + + while (isspace(*keystring)) keystring++; + if (*keystring++ != ',') + { + *errmsg = US"dnsdb modifier syntax error"; + rc = DEFER; + goto out; + } + 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"; + rc = DEFER; + goto out; + } + + 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))) + { + 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) + domain = dns_build_reverse(domain); + + do + { + DEBUG(D_lookup) debug_printf_indent("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 */ + rc = DEFER; /* always defer */ + goto out; + } + if (defer_mode == PASS) failrc = DEFER; /* defer only if all do */ + continue; /* treat defer as fail */ + } + + + /* Search the returned records */ + + for (dns_record * 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) + { + for (dns_address * 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 */ + +gstring_release_unused(yield); + +/* 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) + rc = failrc; +else + { + *result = string_from_gstring(yield); + rc = OK; + } + +out: + +store_free_dns_answer(dnsa); +return rc; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +gstring * +dnsdb_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: DNSDB: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"dnsdb", /* lookup name */ + .type = lookup_querystyle, /* query style */ + .open = dnsdb_open, /* open function */ + .check = NULL, /* check function */ + .find = dnsdb_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..a769102 --- /dev/null +++ b/src/lookups/dsearch.c @@ -0,0 +1,190 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * dirname, uschar ** errmsg) +{ +DIR * dp = exim_opendir(dirname); +if (!dp) + { + *errmsg = string_open_failed("%s for directory search", dirname); + 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. */ + +static BOOL +dsearch_check(void * handle, const uschar * filename, int modemask, + uid_t * owners, gid_t * owngroups, uschar ** errmsg) +{ +handle = handle; +if (*filename == '/') + return lf_check_file(-1, filename, S_IFDIR, modemask, owners, owngroups, + "dsearch", errmsg) == 0; +*errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", filename); +return FALSE; +} + + +/************************************************* +* Find entry point * +*************************************************/ + +#define RET_FULL BIT(0) +#define FILTER_TYPE BIT(1) +#define FILTER_ALL BIT(1) +#define FILTER_FILE BIT(2) +#define FILTER_DIR BIT(3) +#define FILTER_SUBDIR BIT(4) + +/* 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. */ + +static int +dsearch_find(void * handle, const uschar * dirname, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +struct stat statbuf; +int save_errno; +uschar * filename; +unsigned flags = 0; + +if (Ustrchr(keystring, '/') != 0) + { + *errmsg = string_sprintf("key for dsearch lookup contains a slash: %s", + keystring); + return DEFER; + } + +if (opts) + { + int sep = ','; + uschar * ele; + + while ((ele = string_nextinlist(&opts, &sep, NULL, 0))) + if (Ustrcmp(ele, "ret=full") == 0) + flags |= RET_FULL; + else if (Ustrncmp(ele, "filter=", 7) == 0) + { + ele += 7; + if (Ustrcmp(ele, "file") == 0) + flags |= FILTER_TYPE | FILTER_FILE; + else if (Ustrcmp(ele, "dir") == 0) + flags |= FILTER_TYPE | FILTER_DIR; + else if (Ustrcmp(ele, "subdir") == 0) + flags |= FILTER_TYPE | FILTER_SUBDIR; /* like dir but not "." or ".." */ + } + } + +filename = string_sprintf("%s/%s", dirname, keystring); +if ( Ulstat(filename, &statbuf) >= 0 + && ( !(flags & FILTER_TYPE) + || (flags & FILTER_FILE && S_ISREG(statbuf.st_mode)) + || ( flags & (FILTER_DIR | FILTER_SUBDIR) + && S_ISDIR(statbuf.st_mode) + && ( flags & FILTER_DIR + || keystring[0] != '.' + || keystring[1] && keystring[1] != '.' + ) ) ) ) + { + /* Since the filename exists in the filesystem, we can return a + non-tainted result. */ + *result = string_copy_taint(flags & RET_FULL ? filename : keystring, GET_UNTAINTED); + return OK; + } + +if (errno == ENOENT || errno == 0) return FAIL; + +save_errno = errno; +*errmsg = string_sprintf("%s: lstat: %s", filename, strerror(errno)); +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" + +gstring * +dsearch_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: dsearch: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"dsearch", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = dsearch_open, /* open function */ + .check = dsearch_check, /* check function */ + .find = dsearch_find, /* find function */ + .close = dsearch_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..c4fff71 --- /dev/null +++ b/src/lookups/ibase.c @@ -0,0 +1,561 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const 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_indent("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; +int i; +rmark reset_point; + +char buffer[256]; +ISC_STATUS status[20], *statusp = status; + +gstring * result; +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 (int 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_indent("Interbase using cached connection for %s\n", + server_copy); + } +else + { + cn = store_get(sizeof(ibase_connection), GET_UNTAINTED); + 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; + 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 (char * p = sdata[1]; *p;) + *dpb++ = *p++; + *dpb++ = isc_dpb_password; + *dpb++ = strlen(sdata[2]); + for (char * p = sdata[2]; *p;) + *dpb++ = *p++; + dpb_length = dpb - buffer; + + DEBUG(D_lookup) + debug_printf_indent("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; + } + +/* Lacking any information, assume that the data is untainted */ +reset_point = store_mark(); +out_sqlda = store_get(XSQLDA_LENGTH(1), GET_UNTAINTED); +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); + reset_point = store_reset(reset_point); + 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), GET_UNTAINTED); + if (isc_dsql_describe + (status, &stmth, out_sqlda->version, new_sqlda)) + { + isc_interprete(buffer, &statusp); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + reset_point = store_reset(reset_point); + 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, GET_UNTAINTED); + break; + case SQL_TEXT: + var->sqldata = CS store_get(sizeof(char) * var->sqllen, GET_UNTAINTED); + break; + case SQL_SHORT: + var->sqldata = CS store_get(sizeof(short), GET_UNTAINTED); + break; + case SQL_LONG: + var->sqldata = CS store_get(sizeof(ISC_LONG), GET_UNTAINTED); + break; +#ifdef SQL_INT64 + case SQL_INT64: + var->sqldata = CS store_get(sizeof(ISC_INT64), GET_UNTAINTED); + break; +#endif + case SQL_FLOAT: + var->sqldata = CS store_get(sizeof(float), GET_UNTAINTED); + break; + case SQL_DOUBLE: + var->sqldata = CS store_get(sizeof(double), GET_UNTAINTED); + break; +#ifdef SQL_TIMESTAMP + case SQL_DATE: + var->sqldata = CS store_get(sizeof(ISC_QUAD), GET_UNTAINTED); + break; +#else + case SQL_TIMESTAMP: + var->sqldata = CS store_get(sizeof(ISC_TIMESTAMP), GET_UNTAINTED); + break; + case SQL_TYPE_DATE: + var->sqldata = CS store_get(sizeof(ISC_DATE), GET_UNTAINTED); + break; + case SQL_TYPE_TIME: + var->sqldata = CS store_get(sizeof(ISC_TIME), GET_UNTAINTED); + break; + #endif + } + if (var->sqltype & 1) + var->sqlind = (short *) store_get(sizeof(short), GET_UNTAINTED); + } + +/* 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 (int 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) + { + result = string_catn(result, US "\"", 1); + for (int 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 + gstring_release_unused(result); + + +/* 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_indent("%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, const uschar * filename, uschar * query, int length, + uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts) +{ +int sep = 0; +uschar *server; +uschar *list = ibase_servers; + +DEBUG(D_lookup) debug_printf_indent("Interbase query: %s\n", query); + +while ((server = string_nextinlist(&list, &sep, NULL, 0))) + { + 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) + *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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +ibase_quote(uschar * s, uschar * opt, unsigned idx) +{ +int c; +int count = 0; +uschar * t = s, * quoted; + +if (opt) + return NULL; /* No options recognized */ + +while ((c = *t++)) + if (c == '\'') count++; + +t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx); + +while ((c = *s++)) + if (c == '\'') { *t++ = '\''; *t++ = '\''; } + else *t++ = c; + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +gstring * +ibase_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: ibase: Exim version %s\n", EXIM_VERSION_STR)); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"ibase", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = ibase_open, /* open function */ + .check NULL, /* no check function */ + .find = ibase_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = ibase_tidy, /* tidy function */ + .quote = ibase_quote, /* quoting function */ + .version_report = 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/json.c b/src/lookups/json.c new file mode 100644 index 0000000..c9abf8c --- /dev/null +++ b/src/lookups/json.c @@ -0,0 +1,188 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2021 - 2022 */ +/* Copyright (c) Jeremy Harris 2019 - 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include <jansson.h> + + + +/* All use of allocations will be done against the POOL_SEARCH memory, +which is freed once by search_tidyup(). Make the free call a dummy. +This burns some 300kB in handling a 37kB JSON file, for the benefit of +a fast free. The alternative of staying with malloc is nearly as bad, +eyeballing the activity there are 20% the number of free vs. alloc +calls (before the big bunch at the end). + +Assume that the file is trusted, so no tainting */ + +static void * +json_malloc(size_t nbytes) +{ +void * p = store_get((int)nbytes, GET_UNTAINTED); +/* debug_printf("%s %d: %p\n", __FUNCTION__, (int)nbytes, p); */ +return p; +} +static void +json_free(void * p) +{ +/* debug_printf("%s: %p\n", __FUNCTION__, p); */ +} + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +static void * +json_open(const uschar * filename, uschar ** errmsg) +{ +FILE * f; + +json_set_alloc_funcs(json_malloc, json_free); + +if (!(f = Ufopen(filename, "rb"))) + *errmsg = string_open_failed("%s for json search", filename); +return f; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +static BOOL +json_check(void *handle, const 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, "json", errmsg) == 0; +} + + + +/************************************************* +* Find entry point for lsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +json_find(void * handle, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +FILE * f = handle; +json_t * j, * j0; +json_error_t jerr; +uschar * key; +int sep = 0; + +rewind(f); +if (!(j = json_loadf(f, 0, &jerr))) + { + *errmsg = string_sprintf("json error on open: %.*s\n", + JSON_ERROR_TEXT_LENGTH, jerr.text); + return FAIL; + } +j0 = j; + +for (int k = 1; (key = string_nextinlist(&keystring, &sep, NULL, 0)); k++) + { + BOOL numeric = TRUE; + for (uschar * s = key; *s; s++) if (!isdigit(*s)) { numeric = FALSE; break; } + + if (!(j = numeric + ? json_array_get(j, (size_t) strtoul(CS key, NULL, 10)) + : json_object_get(j, CCS key) + ) ) + { + DEBUG(D_lookup) debug_printf_indent("%s, for key %d: '%s'\n", + numeric + ? US"bad index, or not json array" + : US"no such key, or not json object", + k, key); + json_decref(j0); + return FAIL; + } + } + +switch (json_typeof(j)) + { + case JSON_STRING: + *result = string_copyn(CUS json_string_value(j), json_string_length(j)); + break; + case JSON_INTEGER: + *result = string_sprintf("%" JSON_INTEGER_FORMAT, json_integer_value(j)); + break; + case JSON_REAL: + *result = string_sprintf("%f", json_real_value(j)); + break; + case JSON_TRUE: *result = US"true"; break; + case JSON_FALSE: *result = US"false"; break; + case JSON_NULL: *result = NULL; break; + default: *result = US json_dumps(j, 0); break; + } +json_decref(j0); +return OK; +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +static void +json_close(void *handle) +{ +(void)fclose((FILE *)handle); +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +gstring * +json_version_report(gstring * g) +{ +return string_fmt_append(g, "Library version: json: Jansonn version %s\n", JANSSON_VERSION); +} + + +static lookup_info json_lookup_info = { + .name = US"json", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = json_open, /* open function */ + .check = json_check, /* check function */ + .find = json_find, /* find function */ + .close = json_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = json_version_report /* version reporting */ +}; + + +#ifdef DYNLOOKUP +#define json_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &json_lookup_info }; +lookup_module_info json_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/json.c */ diff --git a/src/lookups/ldap.c b/src/lookups/ldap.c new file mode 100644 index 0000000..9751fa3 --- /dev/null +++ b/src/lookups/ldap.c @@ -0,0 +1,1613 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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; + +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_indent("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_indent("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 (uschar ** 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 ? host : US""; + rmark reset_point = store_mark(); + gstring * g; + + /* Handle connection via Unix socket ("ldapi"). We build a basic LDAP URI to + contain the path name, with slashes escaped as %2F. */ + + if (ldapi) + { + g = string_catn(NULL, US"ldapi://", 8); + for (uschar ch; (ch = *shost); shost++) + g = ch == '/' ? string_catn(g, US"%2F", 3) : string_catn(g, shost, 1); + } + + /* This is not an ldapi call. Just build a URI with the protocol type, host + name, and port. */ + + else + { + uschar * init_ptr = Ustrchr(ldap_url, '/'); + g = string_catn(NULL, ldap_url, init_ptr - ldap_url); + g = string_fmt_append(g, "//%s:%d/", shost, port); + } + string_from_gstring(g); + + /* Call ldap_initialize() and check the result */ + + DEBUG(D_lookup) debug_printf_indent("ldap_initialize with URL %s\n", g->s); + if ((rc = ldap_initialize(&ld, CS g->s)) != LDAP_SUCCESS) + { + *errmsg = string_sprintf("ldap_initialize: (error %d) URL \"%s\"\n", + rc, g->s); + goto RETURN_ERROR; + } + store_reset(reset_point); /* 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_indent("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_indent( + "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_indent("LDAP_OPT_X_TLS_HARD set due to ldaps:// URI\n"); + } + else + { + tls_option = LDAP_OPT_X_TLS_TRY; + DEBUG(D_lookup) + debug_printf_indent("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_indent("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_indent("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), GET_UNTAINTED); + lcp->host = host ? string_copy(host) : NULL; + 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_indent("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_indent("%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_indent("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_indent("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_indent("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_indent("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_indent("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_indent("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 (uschar * attr = US ldap_first_attribute(lcp->ld, e, &ber); + attr; attr = US ldap_next_attribute(lcp->ld, e, ber)) + { + DEBUG(D_lookup) debug_printf_indent("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_indent("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) + for (int 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 + for (int 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_release_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_indent("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_indent("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_indent("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_indent("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_indent("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 = US"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_indent("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_indent("%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; +const uschar *list; + +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 && *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_indent("%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 = US"LDAP option REFERRALS is not \"follow\" or \"nofollow\""; + DEBUG(D_lookup) debug_printf_indent("%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_indent("%s\n", *errmsg); + return DEFER; + } + #endif + + else + { + *errmsg = + string_sprintf("unknown parameter \"%.*s\" precedes LDAP URL", + namelen, name); + DEBUG(D_lookup) debug_printf_indent("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_indent("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) + { + uschar *t = user; + for (uschar * 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_indent("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 || !password) + { + *errmsg = US"ldapauth lookups must specify the username and password"; + return DEFER; + } + if (!*password) + { + DEBUG(D_lookup) debug_printf_indent("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_indent("LDAP query error: %s\n", *errmsg); + return DEFER; + } + +/* No default servers, or URL contains a server name: just one attempt */ + +if (!eldap_default_servers && !local_servers || 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 servers until OK or FAIL. Use local_servers list +if defined in the lookup, otherwise use the global default list */ + +list = local_servers ? local_servers : eldap_default_servers; +for (uschar * server; server = string_nextinlist(&list, &sep, NULL, 0); ) + { + int rc, port = 0; + uschar *colon = Ustrchr(server, ':'); + if (colon) + { + *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, const uschar * filename, const uschar * ldap_url, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return(control_ldap_search(ldap_url, SEARCH_LDAP_SINGLE, result, errmsg)); +} + +static int +eldapm_find(void * handle, const uschar * filename, const uschar * ldap_url, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return(control_ldap_search(ldap_url, SEARCH_LDAP_MULTIPLE, result, errmsg)); +} + +static int +eldapdn_find(void * handle, const uschar * filename, const uschar * ldap_url, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return(control_ldap_search(ldap_url, SEARCH_LDAP_DN, result, errmsg)); +} + +int +eldapauth_find(void * handle, const uschar * filename, const uschar * ldap_url, + int length, uschar ** result, uschar ** errmsg, uint * 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(const 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) +{ +eldap_dn = NULL; + +for (LDAP_CONNECTION *lcp; lcp = ldap_connections; ldap_connections = lcp->next) + { + DEBUG(D_lookup) debug_printf_indent("unbind LDAP connection to %s:%d\n", + lcp->host, lcp->port); + if(lcp->bound) ldap_unbind(lcp->ld); + } +} + + + +/************************************************* +* 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 + idx lookup type index + +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, unsigned idx) +{ +int c, count = 0, len = 0; +BOOL dn = FALSE; +uschar * t = s, * quoted; + +/* Test for a DN quotation. */ + +if (opt) + { + 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. +XXX No longer true; we always copy, to support quoted-enforcement */ + +while ((c = *t++)) + { + 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_quoted(len + count + 1, s, idx); + +/* Handle plain quote_ldap */ + +if (!dn) + { + while ((c = *s++)) + { + 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, US"%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, US"%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" + +gstring * +ldap_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: LDAP: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info ldap_lookup_info = { + .name = US"ldap", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = eldap_open, /* open function */ + .check = NULL, /* check function */ + .find = eldap_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = eldap_tidy, /* tidy function */ + .quote = eldap_quote, /* quoting function */ + .version_report = ldap_version_report /* version reporting */ +}; + +static lookup_info ldapdn_lookup_info = { + .name = US"ldapdn", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = eldap_open, /* sic */ /* open function */ + .check = NULL, /* check function */ + .find = eldapdn_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = eldap_tidy, /* sic */ /* tidy function */ + .quote = eldap_quote, /* sic */ /* quoting function */ + .version_report = NULL /* no version reporting (redundant) */ +}; + +static lookup_info ldapm_lookup_info = { + .name = US"ldapm", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = eldap_open, /* sic */ /* open function */ + .check = NULL, /* check function */ + .find = eldapm_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = eldap_tidy, /* sic */ /* tidy function */ + .quote = eldap_quote, /* sic */ /* quoting function */ + .version_report = 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..7f0f128 --- /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 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* 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, const uschar * filename, int s_type, int modemask, + uid_t * owners, gid_t * owngroups, const char * type, uschar ** errmsg) +{ +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 (int 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 (int 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..fd9eb30 --- /dev/null +++ b/src/lookups/lf_functions.h @@ -0,0 +1,20 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* 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, const 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 *, const uschar *, + int(*)(const uschar *, uschar *, uschar **, + uschar **, BOOL *, uint *, const uschar *)); + +/* 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..6f4143d --- /dev/null +++ b/src/lookups/lf_quote.c @@ -0,0 +1,63 @@ +/************************************************* +* 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] == '\"') + { + result = string_catn(result, US"\"", 1); + for (int 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..ce6f163 --- /dev/null +++ b/src/lookups/lf_sqlperform.c @@ -0,0 +1,175 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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, const uschar * opts, + int(*fn)(const uschar *, uschar *, uschar **, uschar **, BOOL *, uint *, const uschar *)) +{ +int rc; +uschar *server; +BOOL defer_break = FALSE; + +DEBUG(D_lookup) debug_printf_indent("%s query: \"%s\" opts '%s'\n", name, query, opts); + +/* Handle queries that do have server information at the start. */ + +if (Ustrncmp(query, "servers", 7) == 0) + { + int qsep = 0; + const uschar *s, *ss; + const uschar *qserverlist; + uschar *qserver; + + s = query + 7; + skip_whitespace(&s); + if (*s++ != '=') + { + *errmsg = string_sprintf("missing = after \"servers\" in %s lookup", name); + return DEFER; + } + skip_whitespace(&s); + + ss = Ustrchr(s, ';'); + if (!ss) + { + *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); + + while ((qserver = string_nextinlist(&qserverlist, &qsep, NULL, 0))) + { + if (Ustrchr(qserver, '/')) + server = qserver; + else + { + int len = Ustrlen(qserver); + const uschar * serverlist = optserverlist; + + for (int sep = 0; server = string_nextinlist(&serverlist, &sep, NULL, 0);) + if (Ustrncmp(server, qserver, len) == 0 && server[len] == '/') + break; + + if (!server) + { + *errmsg = string_sprintf("%s server \"%s\" not found in %s", name, + qserver, optionname); + return DEFER; + } + } + + if (is_tainted(server)) + { + *errmsg = string_sprintf("%s server \"%s\" is tainted", name, server); + return DEFER; + } + + rc = (*fn)(ss+1, server, result, errmsg, &defer_break, do_cache, opts); + if (rc != DEFER || defer_break) return rc; + } + } + +/* Handle queries that do not have server information at the start. */ + +else + { + const uschar * serverlist = NULL; + + /* If options are present, scan for a server definition. Default to + the "optserverlist" srgument. */ + + if (opts) + { + uschar * ele; + for (int sep = ','; ele = string_nextinlist(&opts, &sep, NULL, 0); ) + if (Ustrncmp(ele, "servers=", 8) == 0) + { serverlist = ele + 8; break; } + } + + if (!serverlist) + serverlist = optserverlist; + if (!serverlist) + *errmsg = string_sprintf("no %s servers defined (%s option)", name, + optionname); + else + for (int d = 0; (server = string_nextinlist(&serverlist, &d, NULL, 0)); ) + { + /* If not a full spec assume from options; scan main list for matching + hostname */ + + if (!Ustrchr(server, '/')) + { + int len = Ustrlen(server); + const uschar * slist = optserverlist; + uschar * ele; + for (int sep = 0; ele = string_nextinlist(&slist, &sep, NULL, 0); ) + if (Ustrncmp(ele, server, len) == 0 && ele[len] == '/') + break; + if (!ele) + { + *errmsg = string_sprintf("%s server \"%s\" not found in %s", name, + server, optionname); + return DEFER; + } + server = ele; + } + + if (is_tainted(server)) + { + *errmsg = string_sprintf("%s server \"%s\" is tainted", name, server); + return DEFER; + } + + rc = (*fn)(query, server, result, errmsg, &defer_break, do_cache, opts); + 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..a32c7f7 --- /dev/null +++ b/src/lookups/lmdb.c @@ -0,0 +1,162 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 2016 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +#ifdef LOOKUP_LMDB + +#include <lmdb.h> + +typedef struct lmdbstrct +{ +MDB_txn *txn; +MDB_dbi db_dbi; +} Lmdbstrct; + + +/************************************************* +* Open entry point * +*************************************************/ + +static void * +lmdb_open(const 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), GET_UNTAINTED); +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, const uschar * filename, + const uschar * keystring, int length, uschar ** result, uschar ** errmsg, + uint * do_cache, const uschar * opts) +{ +int ret; +MDB_val dbkey, data; +Lmdbstrct * lmdb_p = handle; + +dbkey.mv_data = CS keystring; +dbkey.mv_size = length; + +DEBUG(D_lookup) debug_printf_indent("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_indent("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_indent("%s\n", *errmsg); + return FAIL; + } +else + { + *errmsg = string_sprintf("LMDB: lookup error: %s", mdb_strerror(ret)); + DEBUG(D_lookup) debug_printf_indent("%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" + +gstring * +lmdb_version_report(gstring * g) +{ +g = string_fmt_append(g, "Library version: LMDB: Compile: %d.%d.%d\n", + MDB_VERSION_MAJOR, MDB_VERSION_MINOR, MDB_VERSION_PATCH); +#ifdef DYNLOOKUP +g = string_fmt_append(g, " Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + +static lookup_info lmdb_lookup_info = { + .name = US"lmdb", /* lookup name */ + .type = lookup_absfile, /* query-style lookup */ + .open = lmdb_open, /* open function */ + .check = NULL, /* no check function */ + .find = lmdb_find, /* find function */ + .close = lmdb_close, /* close function */ + .tidy = NULL, /* tidy function */ + .quote = NULL, /* quoting function */ + .version_report = 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 /* LOOKUP_LMDB */ diff --git a/src/lookups/lsearch.c b/src/lookups/lsearch.c new file mode 100644 index 0000000..dcfdec9 --- /dev/null +++ b/src/lookups/lsearch.c @@ -0,0 +1,487 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** errmsg) +{ +FILE * f = Ufopen(filename, "rb"); +if (!f) + *errmsg = string_open_failed("%s for linear search", filename); +return f; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +static BOOL +lsearch_check(void *handle, const 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, const uschar * filename, + const uschar * keystring, int length, uschar ** result, uschar ** errmsg, + int type, const uschar * opts) +{ +FILE *f = handle; +BOOL ret_full = FALSE; +int old_pool = store_pool; +rmark reset_point = NULL; +uschar buffer[4096]; + +if (opts) + { + int sep = ','; + uschar * ele; + + while ((ele = string_nextinlist(&opts, &sep, NULL, 0))) + if (Ustrcmp(ele, "ret=full") == 0) + { ret_full = TRUE; break; } + } + +/* 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_mark(); + } + +rewind(f); +for (BOOL this_is_eol, 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 && *s != '\"') + { + *t++ = *s == '\\' ? string_interpret_escape(CUSS &s) : *s; + s++; + } + linekeylength = t - buffer; + if (*s) s++; /* Past terminating " */ + if (ret_full) + memmove(t, s, Ustrlen(s)+1); /* copy the rest of line also */ + } + + /* Otherwise it is terminated by a colon or white space */ + + else + { + while (*s && *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 drop + 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. */ + + if (!ret_full) + if (Uskip_whitespace(&s) == ':') + { + s++; + Uskip_whitespace(&s); + } + + /* Reset dynamic store, if we need to, and revert to the search pool */ + + if (reset_point) + { + 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 (ret_full) + yield = string_cat(yield, buffer); + else if (*s) + 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); + } + + gstring_release_unused(yield); + *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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_PLAIN, opts); +} + + + +/************************************************* +* Find entry point for wildlsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +wildlsearch_find(void * handle, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_WILD, opts); +} + + + +/************************************************* +* Find entry point for nwildlsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +nwildlsearch_find(void * handle, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_NWILD, opts); +} + + + + +/************************************************* +* Find entry point for iplsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +iplsearch_find(void * handle, uschar const * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +if ((length == 1 && keystring[0] == '*') || + string_is_ip_address(keystring, NULL) != 0) + return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_IP, opts); + +*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" + +gstring * +lsearch_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: lsearch: Exim version %s\n", EXIM_VERSION_STR)); +#endif +return g; +} + + +static lookup_info iplsearch_lookup_info = { + .name = US"iplsearch", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = lsearch_open, /* open function */ + .check = lsearch_check, /* check function */ + .find = iplsearch_find, /* find function */ + .close = lsearch_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = NULL /* no version reporting (redundant) */ +}; + +static lookup_info lsearch_lookup_info = { + .name = US"lsearch", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = lsearch_open, /* open function */ + .check = lsearch_check, /* check function */ + .find = lsearch_find, /* find function */ + .close = lsearch_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = lsearch_version_report /* version reporting */ +}; + +static lookup_info nwildlsearch_lookup_info = { + .name = US"nwildlsearch", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = lsearch_open, /* open function */ + .check = lsearch_check, /* check function */ + .find = nwildlsearch_find, /* find function */ + .close = lsearch_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = NULL /* no version reporting (redundant) */ +}; + +static lookup_info wildlsearch_lookup_info = { + .name = US"wildlsearch", /* lookup name */ + .type = lookup_absfile, /* uses absolute file name */ + .open = lsearch_open, /* open function */ + .check = lsearch_check, /* check function */ + .find = wildlsearch_find, /* find function */ + .close = lsearch_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..78b8c2b --- /dev/null +++ b/src/lookups/mysql.c @@ -0,0 +1,503 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const 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_indent("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 + opts options + +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, const uschar * opts) +{ +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 (int i = 3; i > 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (!pp) + { + *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_indent("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), GET_UNTAINTED); + 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), GET_UNTAINTED); + 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_indent("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_indent("MYSQL: query was not one that returns data\n"); + result = string_cat(result, + string_sprintf("%lld", 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 (int 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 = lengths[0] == 0 && !result + ? string_get(1) /* for 0-len string result ensure non-null gstring */ + : 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) + { + *errmsg = string_sprintf( + "MYSQL: lookup result error when checking for more results: %s\n", + mysql_error(mysql_handle)); + goto MYSQL_EXIT; + } + else /* just ignore more results */ + DEBUG(D_lookup) debug_printf_indent("MYSQL: got unexpected more results\n"); + +/* 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); + gstring_release_unused(result); + return OK; + } +else + { + DEBUG(D_lookup) debug_printf_indent("%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, const uschar * filename, const uschar * query, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return lf_sqlperform(US"MySQL", US"mysql_servers", mysql_servers, query, + result, errmsg, do_cache, opts, 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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +mysql_quote(uschar * s, uschar * opt, unsigned idx) +{ +int c, count = 0; +uschar * t = s, * quoted; + +if (opt) return NULL; /* No options recognized */ + +while ((c = *t++)) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +/* Old code: if (count == 0) return s; +Now always allocate and copy, to track the quoted status. */ + +t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx); + +while ((c = *s++)) + { + 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" + +gstring * +mysql_version_report(gstring * g) +{ +g = string_fmt_append(g, + "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 +g = string_fmt_append(g, + " Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + +/* These are the lookup_info blocks for this driver */ + +static lookup_info mysql_lookup_info = { + .name = US"mysql", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = mysql_open, /* open function */ + .check = NULL, /* no check function */ + .find = mysql_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = mysql_tidy, /* tidy function */ + .quote = mysql_quote, /* quoting function */ + .version_report = 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..0024f44 --- /dev/null +++ b/src/lookups/nis.c @@ -0,0 +1,141 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** errmsg) +{ +char *nis_domain; +if (yp_get_default_domain(&nis_domain) != 0) + { + *errmsg = US"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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +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" + +gstring * +nis_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: NIS: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info nis_lookup_info = { + .name = US"nis", /* lookup name */ + .type = 0, /* not abs file, not query style*/ + .open = nis_open, /* open function */ + .check = NULL, /* check function */ + .find = nis_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = nis_version_report /* version reporting */ +}; + +static lookup_info nis0_lookup_info = { + .name = US"nis0", /* lookup name */ + .type = 0, /* not absfile, not query style */ + .open = nis_open, /* sic */ /* open function */ + .check = NULL, /* check function */ + .find = nis0_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..d9f3f7d --- /dev/null +++ b/src/lookups/nisplus.c @@ -0,0 +1,294 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const 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, const uschar * filename, const uschar * query, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +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 (int 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) 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, US 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) + { + yield = string_catn(yield, US"\"", 1); + for (int 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 + gstring_release_unused(yield); + +/* 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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +nisplus_quote(uschar * s, uschar * opt, unsigned idx) +{ +int count = 0; +uschar * quoted, * t = s; + +if (opt) return NULL; /* No options recognized */ + +while (*t) if (*t++ == '\"') count++; + +t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx); + +while (*s) + { + *t++ = *s; + if (*s++ == '\"') *t++ = '\"'; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +gstring * +nisplus_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: NIS+: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"nisplus", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = nisplus_open, /* open function */ + .check = NULL, /* check function */ + .find = nisplus_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = nisplus_quote, /* quoting function */ + .version_report = 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..d32b5e4 --- /dev/null +++ b/src/lookups/oracle.c @@ -0,0 +1,628 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const 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_indent("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 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 (int 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_indent("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), GET_UNTAINTED); + hda = store_get(HDA_SIZE, GET_UNTAINTED); + 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), GET_UNTAINTED); + 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_indent("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), GET_UNTAINTED); + +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, GET_UNTAINTED); +desc = store_get(sizeof(Ora_Describe)*MAX_SELECT_LIST_SIZE, GET_UNTAINTED); + +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 (int 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)) + { + result = string_catn(result, "\"", 1); + for (int 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 + gstring_release_unused(result); + +/* 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_indent("%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, const uschar * filename, uschar * query, int length, + uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts) +{ +int sep = 0; +uschar *server; +uschar *list = oracle_servers; + +do_cache = do_cache; /* Placate picky compilers */ + +DEBUG(D_lookup) debug_printf_indent("ORACLE query: %s\n", query); + +while ((server = string_nextinlist(&list, &sep, NULL, 0))) + { + BOOL defer_break; + int rc = perform_oracle_search(query, server, result, errmsg, &defer_break); + if (rc != DEFER || defer_break) return rc; + } + +if (!oracle_servers) + *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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +oracle_quote(uschar * s, uschar * opt, unsigned idx) +{ +int c, count = 0; +uschar * t = s, * quoted; + +if (opt) return NULL; /* No options are recognized */ + +while ((c = *t++)) + if (strchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +t = quoted = store_get_quoted((int)Ustrlen(s) + count + 1, s, idx); + +while ((c = *s++)) + { + 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" + +gstring * +oracle_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: Oracle: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"oracle", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = oracle_open, /* open function */ + .check = NULL, /* check function */ + .find = oracle_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = oracle_tidy, /* tidy function */ + .quote = oracle_quote, /* quoting function */ + .version_report = 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..eaf78b2 --- /dev/null +++ b/src/lookups/passwd.c @@ -0,0 +1,85 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** 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, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +struct passwd *pw; + +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" + +gstring * +passwd_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: passwd: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + +static lookup_info _lookup_info = { + .name = US"passwd", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = passwd_open, /* open function */ + .check = NULL, /* no check function */ + .find = passwd_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..4bb693a --- /dev/null +++ b/src/lookups/pgsql.c @@ -0,0 +1,506 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const 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_indent("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_indent("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 + opts options list + +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, const uschar * opts) +{ +PGconn *pg_conn = NULL; +PGresult *pg_result = NULL; + +gstring * result = NULL; +int yield = DEFER; +unsigned int num_fields, num_tuples; +pgsql_connection *cn; +rmark reset_point = store_mark(); +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 (int 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_indent("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_indent("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) + { + reset_point = store_reset(reset_point); + *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), GET_UNTAINTED); + 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_indent("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_indent("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 (int 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 + for (int 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) result = string_get(1); + } + +/* 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) + { + gstring_release_unused(result); + *resultptr = string_from_gstring(result); + return OK; + } +else + { + DEBUG(D_lookup) debug_printf_indent("%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, const uschar * filename, const uschar * query, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +return lf_sqlperform(US"PostgreSQL", US"pgsql_servers", pgsql_servers, query, + result, errmsg, do_cache, opts, 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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +pgsql_quote(uschar * s, uschar * opt, unsigned idx) +{ +int count = 0, c; +uschar * t = s, * quoted; + +if (opt) return NULL; /* No options recognized */ + +while ((c = *t++)) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx); + +while ((c = *s++)) + { + 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" + +gstring * +pgsql_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "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? */ + +return g; +} + + +static lookup_info _lookup_info = { + .name = US"pgsql", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = pgsql_open, /* open function */ + .check = NULL, /* no check function */ + .find = pgsql_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = pgsql_tidy, /* tidy function */ + .quote = pgsql_quote, /* quoting function */ + .version_report = 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/readsock.c b/src/lookups/readsock.c new file mode 100644 index 0000000..22179c9 --- /dev/null +++ b/src/lookups/readsock.c @@ -0,0 +1,341 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2021 - 2022 */ +/* Copyright (c) Jeremy Harris 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" + + +static int +internal_readsock_open(client_conn_ctx * cctx, const uschar * sspec, + int timeout, BOOL do_tls, uschar ** errmsg) +{ +const uschar * server_name; +host_item host; + +if (Ustrncmp(sspec, "inet:", 5) == 0) + { + int port; + uschar * port_name; + + DEBUG(D_lookup) + debug_printf_indent(" new inet socket needed for readsocket\n"); + + server_name = sspec + 5; + port_name = Ustrrchr(server_name, ':'); + + /* Sort out the port */ + + if (!port_name) + { + /* expand_string_message results in an EXPAND_FAIL, from our + only caller. Lack of it gets a SOCK_FAIL; we feed back via errmsg + for that, which gets copied to search_error_message. */ + + expand_string_message = + string_sprintf("missing port for readsocket %s", sspec); + return FAIL; + } + *port_name++ = 0; /* Terminate server name */ + + if (isdigit(*port_name)) + { + uschar *end; + port = Ustrtol(port_name, &end, 0); + if (end != port_name + Ustrlen(port_name)) + { + expand_string_message = + string_sprintf("invalid port number %s", port_name); + return FAIL; + } + } + else + { + struct servent *service_info = getservbyname(CS port_name, "tcp"); + if (!service_info) + { + expand_string_message = string_sprintf("unknown port \"%s\"", + port_name); + return FAIL; + } + port = ntohs(service_info->s_port); + } + + /* Not having the request-string here in the open routine means + that we cannot do TFO; a pity */ + + cctx->sock = ip_connectedsocket(SOCK_STREAM, server_name, port, port, + timeout, &host, errmsg, NULL); + callout_address = NULL; + if (cctx->sock < 0) + return FAIL; + } + +else + { + struct sockaddr_un sockun; /* don't call this "sun" ! */ + int rc; + + DEBUG(D_lookup) + debug_printf_indent(" new unix socket needed for readsocket\n"); + + if ((cctx->sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) + { + *errmsg = string_sprintf("failed to create socket: %s", strerror(errno)); + return FAIL; + } + + sockun.sun_family = AF_UNIX; + sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1), + sspec); + server_name = US sockun.sun_path; + + sigalrm_seen = FALSE; + ALARM(timeout); + rc = connect(cctx->sock, (struct sockaddr *) &sockun, sizeof(sockun)); + ALARM_CLR(0); + if (sigalrm_seen) + { + *errmsg = US "socket connect timed out"; + goto bad; + } + if (rc < 0) + { + *errmsg = string_sprintf("failed to connect to socket " + "%s: %s", sspec, strerror(errno)); + goto bad; + } + host.name = server_name; + host.address = US""; + } + +#ifndef DISABLE_TLS +if (do_tls) + { + union sockaddr_46 interface_sock; + EXIM_SOCKLEN_T size = sizeof(interface_sock); + smtp_connect_args conn_args = {.host = &host }; + tls_support tls_dummy = { .sni = NULL }; + uschar * errstr; + + if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0) + conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL); + else + { + *errmsg = string_sprintf("getsockname failed: %s", strerror(errno)); + goto bad; + } + + if (!tls_client_start(cctx, &conn_args, NULL, &tls_dummy, &errstr)) + { + *errmsg = string_sprintf("TLS connect failed: %s", errstr); + goto bad; + } + } +#endif + +DEBUG(D_expand|D_lookup) debug_printf_indent(" connected to socket %s\n", sspec); +return OK; + +bad: + close(cctx->sock); + return FAIL; +} + +/* All use of allocations will be done against the POOL_SEARCH memory, +which is freed once by search_tidyup(). */ + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ +/* We just create a placeholder record with a closed socket, so +that connection cacheing at the framework layer works. */ + +static void * +readsock_open(const uschar * filename, uschar ** errmsg) +{ +client_conn_ctx * cctx = store_get(sizeof(*cctx), GET_UNTAINTED); +cctx->sock = -1; +cctx->tls_ctx = NULL; +DEBUG(D_lookup) debug_printf_indent("readsock: allocated context\n"); +return cctx; +} + + + + + +/************************************************* +* Find entry point for lsearch * +*************************************************/ + +/* See local README for interface description */ + +static int +readsock_find(void * handle, const uschar * filename, const uschar * keystring, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +client_conn_ctx * cctx = handle; +int sep = ','; +struct { + BOOL do_shutdown:1; + BOOL do_tls:1; + BOOL cache:1; +} lf = {.do_shutdown = TRUE}; +uschar * eol = NULL; +int timeout = 5; +gstring * yield; +int ret = DEFER; + +DEBUG(D_lookup) + debug_printf_indent("readsock: file=\"%s\" key=\"%s\" len=%d opts=\"%s\"\n", + filename, keystring, length, opts); + +/* Parse options */ + +if (opts) for (uschar * s; s = string_nextinlist(&opts, &sep, NULL, 0); ) + if (Ustrncmp(s, "timeout=", 8) == 0) + timeout = readconf_readtime(s + 8, 0, FALSE); + else if (Ustrncmp(s, "shutdown=", 9) == 0) + lf.do_shutdown = Ustrcmp(s + 9, "no") != 0; +#ifndef DISABLE_TLS + else if (Ustrncmp(s, "tls=", 4) == 0 && Ustrcmp(s + 4, US"no") != 0) + lf.do_tls = TRUE; +#endif + else if (Ustrncmp(s, "eol=", 4) == 0) + eol = string_unprinting(s + 4); + else if (Ustrcmp(s, "cache=yes") == 0) + lf.cache = TRUE; + else if (Ustrcmp(s, "send=no") == 0) + length = 0; + +if (!filename) return FAIL; /* Server spec is required */ + +/* Open the socket, if not cached */ + +if (cctx->sock == -1) + if (internal_readsock_open(cctx, filename, timeout, lf.do_tls, errmsg) != OK) + return ret; + +testharness_pause_ms(100); /* Allow sequencing of test actions */ + +/* Write the request string, if not empty or already done */ + +if (length) + { + if (( +#ifndef DISABLE_TLS + cctx->tls_ctx ? tls_write(cctx->tls_ctx, keystring, length, FALSE) : +#endif + write(cctx->sock, keystring, length)) != length) + { + *errmsg = string_sprintf("request write to socket " + "failed: %s", strerror(errno)); + goto out; + } + } + +/* Shut down the sending side of the socket. This helps some servers to +recognise that it is their turn to do some work. Just in case some +system doesn't have this function, make it conditional. */ + +#ifdef SHUT_WR +if (!cctx->tls_ctx && lf.do_shutdown) + shutdown(cctx->sock, SHUT_WR); +#endif + +testharness_pause_ms(100); + +/* Now we need to read from the socket, under a timeout. The function +that reads a file can be used. If we're using a stdio buffered read, +and might need later write ops on the socket, the stdio must be in +writable mode or the underlying socket goes non-writable. */ + +sigalrm_seen = FALSE; +#ifdef DISABLE_TLS +if (TRUE) +#else +if (!cctx->tls_ctx) +#endif + { + FILE * fp = fdopen(cctx->sock, "rb"); + if (!fp) + { + log_write(0, LOG_MAIN|LOG_PANIC, "readsock fdopen: %s\n", strerror(errno)); + goto out; + } + ALARM(timeout); + yield = cat_file(fp, NULL, eol); + } +else + { + ALARM(timeout); + yield = cat_file_tls(cctx->tls_ctx, NULL, eol); + } + +ALARM_CLR(0); + +if (sigalrm_seen) + { *errmsg = US "socket read timed out"; goto out; } + +*result = yield ? string_from_gstring(yield) : US""; +ret = OK; +if (!lf.cache) *do_cache = 0; + +out: + +(void) close(cctx->sock); +cctx->sock = -1; +return ret; +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +static void +readsock_close(void * handle) +{ +client_conn_ctx * cctx = handle; +if (cctx->sock < 0) return; +#ifndef DISABLE_TLS +if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TRUE); +#endif +close(cctx->sock); +cctx->sock = -1; +} + + + +static lookup_info readsock_lookup_info = { + .name = US"readsock", /* lookup name */ + .type = lookup_querystyle, + .open = readsock_open, /* open function */ + .check = NULL, + .find = readsock_find, /* find function */ + .close = readsock_close, + .tidy = NULL, + .quote = NULL, /* no quoting function */ + .version_report = NULL +}; + + +#ifdef DYNLOOKUP +#define readsock_lookup_module_info _lookup_module_info +#endif + +static lookup_info *_lookup_list[] = { &readsock_lookup_info }; +lookup_module_info readsock_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 }; + +/* End of lookups/readsock.c */ diff --git a/src/lookups/redis.c b/src/lookups/redis.c new file mode 100644 index 0000000..9c8559c --- /dev/null +++ b/src/lookups/redis.c @@ -0,0 +1,471 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const 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_indent("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 + opts options + + 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, const uschar * opts) +{ +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 (int 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_indent("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 = US"REDIS connection failed"; + *defer_break = FALSE; + goto REDIS_EXIT; + } + + /* Add the connection to the cache */ + cn = store_get(sizeof(redis_connection), GET_UNTAINTED); + cn->server = server_copy; + cn->handle = redis_handle; + cn->next = redis_connections; + redis_connections = cn; + } +else + { + DEBUG(D_lookup) + debug_printf_indent("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_indent("REDIS: Selecting database=%s\n", sdata[1]); + } + +/* split string on whitespace into argv */ + { + uschar * argv[32]; + const uschar * s = command; + int siz, ptr, i; + 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_indent("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. */ + + if (!(redis_reply = redisCommandArgv(redis_handle, i, CCSS argv, NULL))) + { + *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr); + *defer_break = FALSE; + goto REDIS_EXIT; + } + } + +switch (redis_reply->type) + { + case REDIS_REPLY_ERROR: + *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str); + + /* trap MOVED cluster responses and follow them */ + if (Ustrncmp(redis_reply->str, "MOVED", 5) == 0) + { + DEBUG(D_lookup) + debug_printf_indent("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_indent("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 (int 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 (int 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_indent("REDIS: result has nesting of arrays which" + " is not supported. Ignoring!\n"); + break; + default: + DEBUG(D_lookup) debug_printf_indent( + "REDIS: result has unsupported type. Ignoring!\n"); + break; + } + } + break; + default: + DEBUG(D_lookup) debug_printf_indent("REDIS: query returned unsupported type\n"); + break; + } + } + break; + } + + +if (result) + gstring_release_unused(result); +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_indent("%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)), + const uschar * filename __attribute__((unused)), + const uschar * command, int length, uschar ** result, uschar ** errmsg, + uint * do_cache, const uschar * opts) +{ +return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command, + result, errmsg, do_cache, opts, 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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +redis_quote(uschar * s, uschar * opt, unsigned idx) +{ +int c, count = 0; +uschar * t = s, * quoted; + +if (opt) return NULL; /* No options recognized */ + +while ((c = *t++)) + if (isspace(c) || c == '\\') count++; + +t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx); + +while ((c = *s++)) + { + if (isspace(c) || c == '\\') *t++ = '\\'; + *t++ = c; + } + +*t = 0; +return quoted; +} + + +/************************************************* +* Version reporting entry point * +*************************************************/ +#include "../version.h" + +gstring * +redis_version_report(gstring * g) +{ +g = string_fmt_append(g, + "Library version: REDIS: Compile: %d [%d]\n", HIREDIS_MAJOR, HIREDIS_MINOR); +#ifdef DYNLOOKUP +g = string_fmt_append(g, + " Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + + +/* These are the lookup_info blocks for this driver */ +static lookup_info redis_lookup_info = { + .name = US"redis", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = redis_open, /* open function */ + .check = NULL, /* no check function */ + .find = redis_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = redis_tidy, /* tidy function */ + .quote = redis_quote, /* quoting function */ + .version_report = 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..78d954c --- /dev/null +++ b/src/lookups/spf.c @@ -0,0 +1,159 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Exim - SPF lookup module using libspf2 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Copyright (c) The Exim Maintainers 2020 - 2022 +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. +*/ + +#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" +#if !defined(HAVE_NS_TYPE) && defined(NS_INADDRSZ) +# define HAVE_NS_TYPE +#endif +#include <spf2/spf.h> +#include <spf2/spf_dns_resolv.h> +#include <spf2/spf_dns_cache.h> + +extern SPF_dns_server_t * SPF_dns_exim_new(int); + + +static void * +spf_open(const uschar * filename, uschar ** errmsg) +{ +SPF_dns_server_t * dc; +SPF_server_t *spf_server = NULL; +int debug = 0; + +DEBUG(D_lookup) debug = 1; + +if ((dc = SPF_dns_exim_new(debug))) + if ((dc = SPF_dns_cache_new(dc, NULL, debug, 8))) + spf_server = SPF_server_new_dns(dc, debug); + +if (!spf_server) + { + *errmsg = US"SPF_dns_exim_nnew() 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, const uschar * filename, const uschar * keystring, + int key_len, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +SPF_server_t *spf_server = handle; +SPF_request_t *spf_request; +SPF_response_t *spf_response = NULL; + +if (!(spf_request = SPF_request_new(spf_server))) + { + *errmsg = US"SPF_request_new() failed"; + return FAIL; + } + +#if HAVE_IPV6 +switch (string_is_ip_address(filename, NULL)) +#else +switch (4) +#endif + { + case 4: + if (!SPF_request_set_ipv4_str(spf_request, CS filename)) + break; + *errmsg = string_sprintf("invalid IPv4 address '%s'", filename); + return FAIL; +#if HAVE_IPV6 + + case 6: + if (!SPF_request_set_ipv6_str(spf_request, CS filename)) + break; + *errmsg = string_sprintf("invalid IPv6 address '%s'", filename); + return FAIL; + + default: + *errmsg = string_sprintf("invalid IP address '%s'", filename); + return FAIL; +#endif + } + +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))); + +DEBUG(D_lookup) spf_response_debug(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" + +gstring * +spf_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: SPF: Exim version %s\n", EXIM_VERSION_STR)); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"spf", /* lookup name */ + .type = 0, /* not absfile, not query style */ + .open = spf_open, /* open function */ + .check = NULL, /* no check function */ + .find = spf_find, /* find function */ + .close = spf_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..9080ae7 --- /dev/null +++ b/src/lookups/sqlite.c @@ -0,0 +1,200 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** errmsg) +{ +sqlite3 *db = NULL; +int ret; + +if (!filename || !*filename) + { + DEBUG(D_lookup) debug_printf_indent("Using sqlite_dbfile: %s\n", sqlite_dbfile); + filename = sqlite_dbfile; + } +if (!filename || *filename != '/') + *errmsg = US"absolute file name expected for \"sqlite\" lookup"; +else if ((ret = sqlite3_open(CCS filename, &db)) != 0) + { + *errmsg = (void *)sqlite3_errmsg(db); + sqlite3_close(db); + db = NULL; + DEBUG(D_lookup) debug_printf_indent("Error opening database: %s\n", *errmsg); + } + +if (db) + 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; + +/* 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 (int i = 0; i < argc; i++) + { + uschar * value = US(argv[i] ? 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>"); + +/* always return a non-null gstring, even for a zero-length string result */ +*(gstring **)arg = res ? res : string_get(1); +return 0; +} + + +static int +sqlite_find(void * handle, const uschar * filename, const uschar * query, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +int ret; +gstring * res = NULL; + +ret = sqlite3_exec(handle, CS query, sqlite_callback, &res, CSS errmsg); +if (ret != SQLITE_OK) + { + debug_printf_indent("sqlite3_exec failed: %s\n", *errmsg); + return FAIL; + } + +if (!res) *do_cache = 0; /* on fail, wipe cache */ + +*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 + idx lookup type index + +Returns: the processed string or NULL for a bad option +*/ + +static uschar * +sqlite_quote(uschar * s, uschar * opt, unsigned idx) +{ +int c, count = 0; +uschar * t = s, * quoted; + +if (opt) return NULL; /* No options recognized */ + +while ((c = *t++)) if (c == '\'') count++; +count += t - s; + +t = quoted = store_get_quoted(count + 1, s, idx); + +while ((c = *s++)) + { + if (c == '\'') *t++ = '\''; + *t++ = c; + } + +*t = 0; +return quoted; +} + + + +/************************************************* +* Version reporting entry point * +*************************************************/ + +/* See local README for interface description. */ + +#include "../version.h" + +gstring * +sqlite_version_report(gstring * g) +{ +g = string_fmt_append(g, + "Library version: SQLite: Compile: %s\n" + " Runtime: %s\n", + SQLITE_VERSION, sqlite3_libversion()); +#ifdef DYNLOOKUP +g = string_fmt_append(g, + " Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + +static lookup_info _lookup_info = { + .name = US"sqlite", /* lookup name */ + .type = lookup_absfilequery, /* query-style lookup, starts with file name */ + .open = sqlite_open, /* open function */ + .check = NULL, /* no check function */ + .find = sqlite_find, /* find function */ + .close = sqlite_close, /* close function */ + .tidy = NULL, /* no tidy function */ + .quote = sqlite_quote, /* quoting function */ + .version_report = 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..4824161 --- /dev/null +++ b/src/lookups/testdb.c @@ -0,0 +1,100 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +static int +testdb_find(void * handle, const uschar * filename, const uschar * query, + int length, uschar ** result, uschar ** errmsg, uint * do_cache, + const uschar * opts) +{ +if (Ustrcmp(query, "fail") == 0) + { + *errmsg = US"testdb lookup forced FAIL"; + DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg); + return FAIL; + } +if (Ustrcmp(query, "defer") == 0) + { + *errmsg = US"testdb lookup forced DEFER"; + DEBUG(D_lookup) debug_printf_indent("%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" + +gstring * +testdb_version_report(gstring * g) +{ +#ifdef DYNLOOKUP +g = string_fmt_append(g, "Library version: TestDB: Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + + +static lookup_info _lookup_info = { + .name = US"testdb", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = testdb_open, /* open function */ + .check = NULL, /* check function */ + .find = testdb_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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..990703f --- /dev/null +++ b/src/lookups/whoson.c @@ -0,0 +1,98 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* 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(const uschar * filename, uschar ** errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +static int +whoson_find(void * handle, const uschar * filename, uschar * query, int length, + uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts) +{ +uschar buffer[80]; + +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" + +gstring * +whoson_version_report(gstring * g) +{ +g = string_fmt_append(g, + "Library version: Whoson: Runtime: %s\n", wso_version()); +#ifdef DYNLOOKUP +g = string_fmt_append(g, + " Exim version %s\n", EXIM_VERSION_STR); +#endif +return g; +} + +static lookup_info _lookup_info = { + .name = US"whoson", /* lookup name */ + .type = lookup_querystyle, /* query-style lookup */ + .open = whoson_open, /* open function */ + .check = NULL, /* check function */ + .find = whoson_find, /* find function */ + .close = NULL, /* no close function */ + .tidy = NULL, /* no tidy function */ + .quote = NULL, /* no quoting function */ + .version_report = 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 */ |