diff options
Diffstat (limited to 'src/dbfn.c')
-rw-r--r-- | src/dbfn.c | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/src/dbfn.c b/src/dbfn.c new file mode 100644 index 0000000..ea94b7f --- /dev/null +++ b/src/dbfn.c @@ -0,0 +1,681 @@ +/************************************************* +* 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" + +/* We have buffers holding path names for database files. +PATH_MAX could be used here, but would be wasting memory, as we deal +with database files like $spooldirectory/db/<name> */ +#define PATHLEN 256 + + +/* Functions for accessing Exim's hints database, which consists of a number of +different DBM files. This module does not contain code for reading DBM files +for (e.g.) alias expansion. That is all contained within the general search +functions. As Exim now has support for several DBM interfaces, all the relevant +functions are called as macros. + +All the data in Exim's database is in the nature of *hints*. Therefore it +doesn't matter if it gets destroyed by accident. These functions are not +supposed to implement a "safe" database. + +Keys are passed in as C strings, and the terminating zero *is* used when +building the dbm files. This just makes life easier when scanning the files +sequentially. + +Synchronization is required on the database files, and this is achieved by +means of locking on independent lock files. (Earlier attempts to lock on the +DBM files themselves were never completely successful.) Since callers may in +general want to do more than one read or write while holding the lock, there +are separate open and close functions. However, the calling modules should +arrange to hold the locks for the bare minimum of time. */ + + + +/************************************************* +* Open and lock a database file * +*************************************************/ + +/* Used for accessing Exim's hints databases. + +Arguments: + name The single-component name of one of Exim's database files. + flags Either O_RDONLY or O_RDWR, indicating the type of open required; + O_RDWR implies "create if necessary" + dbblock Points to an open_db block to be filled in. + lof If TRUE, write to the log for actual open failures (locking failures + are always logged). + panic If TRUE, panic on failure to create the db directory + +Returns: NULL if the open failed, or the locking failed. After locking + failures, errno is zero. + + On success, dbblock is returned. This contains the dbm pointer and + the fd of the locked lock file. + +There are some calls that use O_RDWR|O_CREAT for the flags. Having discovered +this in December 2005, I'm not sure if this is correct or not, but for the +moment I haven't changed them. +*/ + +open_db * +dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof, BOOL panic) +{ +int rc, save_errno; +BOOL read_only = flags == O_RDONLY; +flock_t lock_data; +uschar dirname[PATHLEN], filename[PATHLEN]; + +DEBUG(D_hints_lookup) acl_level++; + +/* The first thing to do is to open a separate file on which to lock. This +ensures that Exim has exclusive use of the database before it even tries to +open it. Early versions tried to lock on the open database itself, but that +gave rise to mysterious problems from time to time - it was suspected that some +DB libraries "do things" on their open() calls which break the interlocking. +The lock file is never written to, but we open it for writing so we can get a +write lock if required. If it does not exist, we create it. This is done +separately so we know when we have done it, because when running as root we +need to change the ownership - see the bottom of this function. We also try to +make the directory as well, just in case. We won't be doing this many times +unnecessarily, because usually the lock file will be there. If the directory +exists, there is no error. */ + +snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory); +snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name); + +priv_drop_temp(exim_uid, exim_gid); +if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0) + { + (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, panic); + dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE); + } +priv_restore(); + +if (dbblock->lockfd < 0) + { + log_write(0, LOG_MAIN, "%s", + string_open_failed("database lock file %s", filename)); + errno = 0; /* Indicates locking failure */ + DEBUG(D_hints_lookup) acl_level--; + return NULL; + } + +/* Now we must get a lock on the opened lock file; do this with a blocking +lock that times out. */ + +lock_data.l_type = read_only? F_RDLCK : F_WRLCK; +lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0; + +DEBUG(D_hints_lookup|D_retry|D_route|D_deliver) + debug_printf_indent("locking %s\n", filename); + +sigalrm_seen = FALSE; +ALARM(EXIMDB_LOCK_TIMEOUT); +rc = fcntl(dbblock->lockfd, F_SETLKW, &lock_data); +ALARM_CLR(0); + +if (sigalrm_seen) errno = ETIMEDOUT; +if (rc < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to get %s lock for %s: %s", + read_only ? "read" : "write", filename, + errno == ETIMEDOUT ? "timed out" : strerror(errno)); + (void)close(dbblock->lockfd); + errno = 0; /* Indicates locking failure */ + DEBUG(D_hints_lookup) acl_level--; + return NULL; + } + +DEBUG(D_hints_lookup) debug_printf_indent("locked %s\n", filename); + +/* At this point we have an opened and locked separate lock file, that is, +exclusive access to the database, so we can go ahead and open it. If we are +expected to create it, don't do so at first, again so that we can detect +whether we need to change its ownership (see comments about the lock file +above.) There have been regular reports of crashes while opening hints +databases - often this is caused by non-matching db.h and the library. To make +it easy to pin this down, there are now debug statements on either side of the +open call. */ + +snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name); + +priv_drop_temp(exim_uid, exim_gid); +dbblock->dbptr = exim_dbopen(filename, dirname, flags, EXIMDB_MODE); +if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR) + { + DEBUG(D_hints_lookup) + debug_printf_indent("%s appears not to exist: trying to create\n", filename); + dbblock->dbptr = exim_dbopen(filename, dirname, flags|O_CREAT, EXIMDB_MODE); + } +save_errno = errno; +priv_restore(); + +/* If the open has failed, return NULL, leaving errno set. If lof is TRUE, +log the event - also for debugging - but debug only if the file just doesn't +exist. */ + +if (!dbblock->dbptr) + { + errno = save_errno; + if (lof && save_errno != ENOENT) + log_write(0, LOG_MAIN, "%s", string_open_failed("DB file %s", + filename)); + else + DEBUG(D_hints_lookup) + debug_printf_indent("%s\n", CS string_open_failed("DB file %s", + filename)); + (void)close(dbblock->lockfd); + errno = save_errno; + DEBUG(D_hints_lookup) acl_level--; + return NULL; + } + +DEBUG(D_hints_lookup) + debug_printf_indent("opened hints database %s: flags=%s\n", filename, + flags == O_RDONLY ? "O_RDONLY" + : flags == O_RDWR ? "O_RDWR" + : flags == (O_RDWR|O_CREAT) ? "O_RDWR|O_CREAT" + : "??"); + +/* Pass back the block containing the opened database handle and the open fd +for the lock. */ + +return dbblock; +} + + + + +/************************************************* +* Unlock and close a database file * +*************************************************/ + +/* Closing a file automatically unlocks it, so after closing the database, just +close the lock file. + +Argument: a pointer to an open database block +Returns: nothing +*/ + +void +dbfn_close(open_db *dbblock) +{ +exim_dbclose(dbblock->dbptr); +(void)close(dbblock->lockfd); +DEBUG(D_hints_lookup) + { debug_printf_indent("closed hints database and lockfile\n"); acl_level--; } +} + + + + +/************************************************* +* Read from database file * +*************************************************/ + +/* Passing back the pointer unchanged is useless, because there is +no guarantee of alignment. Since all the records used by Exim need +to be properly aligned to pick out the timestamps, etc., we might as +well do the copying centrally here. + +Most calls don't need the length, so there is a macro called dbfn_read which +has two arguments; it calls this function adding NULL as the third. + +Arguments: + dbblock a pointer to an open database block + key the key of the record to be read + length a pointer to an int into which to return the length, if not NULL + +Returns: a pointer to the retrieved record, or + NULL if the record is not found +*/ + +void * +dbfn_read_with_length(open_db *dbblock, const uschar *key, int *length) +{ +void *yield; +EXIM_DATUM key_datum, result_datum; +int klen = Ustrlen(key) + 1; +uschar * key_copy = store_get(klen, key); + +memcpy(key_copy, key, klen); + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%s\n", key); + +exim_datum_init(&key_datum); /* Some DBM libraries require the datum */ +exim_datum_init(&result_datum); /* to be cleared before use. */ +exim_datum_data_set(&key_datum, key_copy); +exim_datum_size_set(&key_datum, klen); + +if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum)) return NULL; + +/* Assume the data store could have been tainted. Properly, we should +store the taint status with the data. */ + +yield = store_get(exim_datum_size_get(&result_datum), GET_TAINTED); +memcpy(yield, exim_datum_data_get(&result_datum), exim_datum_size_get(&result_datum)); +if (length) *length = exim_datum_size_get(&result_datum); + +exim_datum_free(&result_datum); /* Some DBM libs require freeing */ +return yield; +} + + +/* Read a record. If the length is not as expected then delete it, write +an error log line, delete the record and return NULL. +Use this for fixed-size records (so not retry or wait records). + +Arguments: + dbblock a pointer to an open database block + key the key of the record to be read + length the expected record length + +Returns: a pointer to the retrieved record, or + NULL if the record is not found/bad +*/ + +void * +dbfn_read_enforce_length(open_db * dbblock, const uschar * key, size_t length) +{ +int rlen; +void * yield = dbfn_read_with_length(dbblock, key, &rlen); + +if (yield) + { + if (rlen == length) return yield; + log_write(0, LOG_MAIN|LOG_PANIC, "Bad db record size for '%s'", key); + dbfn_delete(dbblock, key); + } +return NULL; +} + + +/************************************************* +* Write to database file * +*************************************************/ + +/* +Arguments: + dbblock a pointer to an open database block + key the key of the record to be written + ptr a pointer to the record to be written + length the length of the record to be written + +Returns: the yield of the underlying dbm or db "write" function. If this + is dbm, the value is zero for OK. +*/ + +int +dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length) +{ +EXIM_DATUM key_datum, value_datum; +dbdata_generic *gptr = (dbdata_generic *)ptr; +int klen = Ustrlen(key) + 1; +uschar * key_copy = store_get(klen, key); + +memcpy(key_copy, key, klen); +gptr->time_stamp = time(NULL); + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_write: key=%s\n", key); + +exim_datum_init(&key_datum); /* Some DBM libraries require the datum */ +exim_datum_init(&value_datum); /* to be cleared before use. */ +exim_datum_data_set(&key_datum, key_copy); +exim_datum_size_set(&key_datum, klen); +exim_datum_data_set(&value_datum, ptr); +exim_datum_size_set(&value_datum, length); +return exim_dbput(dbblock->dbptr, &key_datum, &value_datum); +} + + + +/************************************************* +* Delete record from database file * +*************************************************/ + +/* +Arguments: + dbblock a pointer to an open database block + key the key of the record to be deleted + +Returns: the yield of the underlying dbm or db "delete" function. +*/ + +int +dbfn_delete(open_db *dbblock, const uschar *key) +{ +int klen = Ustrlen(key) + 1; +uschar * key_copy = store_get(klen, key); +EXIM_DATUM key_datum; + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_delete: key=%s\n", key); + +memcpy(key_copy, key, klen); +exim_datum_init(&key_datum); /* Some DBM libraries require clearing */ +exim_datum_data_set(&key_datum, key_copy); +exim_datum_size_set(&key_datum, klen); +return exim_dbdel(dbblock->dbptr, &key_datum); +} + + + +/************************************************* +* Scan the keys of a database file * +*************************************************/ + +/* +Arguments: + dbblock a pointer to an open database block + start TRUE if starting a new scan + FALSE if continuing with the current scan + cursor a pointer to a pointer to a cursor anchor, for those dbm libraries + that use the notion of a cursor + +Returns: the next record from the file, or + NULL if there are no more +*/ + +uschar * +dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor) +{ +EXIM_DATUM key_datum, value_datum; +uschar *yield; + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_scan\n"); + +/* Some dbm require an initialization */ + +if (start) *cursor = exim_dbcreate_cursor(dbblock->dbptr); + +exim_datum_init(&key_datum); /* Some DBM libraries require the datum */ +exim_datum_init(&value_datum); /* to be cleared before use. */ + +yield = exim_dbscan(dbblock->dbptr, &key_datum, &value_datum, start, *cursor) + ? US exim_datum_data_get(&key_datum) : NULL; + +/* Some dbm require a termination */ + +if (!yield) exim_dbdelete_cursor(*cursor); +return yield; +} + + + +/************************************************* +************************************************** +* Stand-alone test program * +************************************************** +*************************************************/ + +#ifdef STAND_ALONE + +int +main(int argc, char **cargv) +{ +open_db dbblock[8]; +int max_db = sizeof(dbblock)/sizeof(open_db); +int current = -1; +int showtime = 0; +int i; +dbdata_wait *dbwait = NULL; +uschar **argv = USS cargv; +uschar buffer[256]; +uschar structbuffer[1024]; + +if (argc != 2) + { + printf("Usage: test_dbfn directory\n"); + printf("The subdirectory called \"db\" in the given directory is used for\n"); + printf("the files used in this test program.\n"); + return 1; + } + +/* Initialize */ + +spool_directory = argv[1]; +debug_selector = D_all - D_memory; +debug_file = stderr; +big_buffer = malloc(big_buffer_size); + +for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL; + +printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE); +printf("DBM library: "); + +#ifdef DB_VERSION_STRING +printf("Berkeley DB: %s\n", DB_VERSION_STRING); +#elif defined(BTREEVERSION) && defined(HASHVERSION) + #ifdef USE_DB + printf("probably Berkeley DB version 1.8x (native mode)\n"); + #else + printf("probably Berkeley DB version 1.8x (compatibility mode)\n"); + #endif +#elif defined(_DBM_RDONLY) || defined(dbm_dirfno) +printf("probably ndbm\n"); +#elif defined(USE_TDB) +printf("using tdb\n"); +#else + #ifdef USE_GDBM + printf("probably GDBM (native mode)\n"); + #else + printf("probably GDBM (compatibility mode)\n"); + #endif +#endif + +/* Test the functions */ + +printf("\nTest the functions\n> "); + +while (Ufgets(buffer, 256, stdin) != NULL) + { + int len = Ustrlen(buffer); + int count = 1; + clock_t start = 1; + clock_t stop = 0; + uschar *cmd = buffer; + while (len > 0 && isspace((uschar)buffer[len-1])) len--; + buffer[len] = 0; + + if (isdigit((uschar)*cmd)) + { + count = Uatoi(cmd); + while (isdigit((uschar)*cmd)) cmd++; + while (isspace((uschar)*cmd)) cmd++; + } + + if (Ustrncmp(cmd, "open", 4) == 0) + { + int i; + open_db *odb; + uschar *s = cmd + 4; + while (isspace((uschar)*s)) s++; + + for (i = 0; i < max_db; i++) + if (dbblock[i].dbptr == NULL) break; + + if (i >= max_db) + { + printf("Too many open databases\n> "); + continue; + } + + start = clock(); + odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE, TRUE); + stop = clock(); + + if (odb) + { + current = i; + printf("opened %d\n", current); + } + /* Other error cases will have written messages */ + else if (errno == ENOENT) + { + printf("open failed: %s%s\n", strerror(errno), + #ifdef USE_DB + " (or other Berkeley DB error)" + #else + "" + #endif + ); + } + } + + else if (Ustrncmp(cmd, "write", 5) == 0) + { + int rc = 0; + uschar *key = cmd + 5; + uschar *data; + + if (current < 0) + { + printf("No current database\n"); + continue; + } + + while (isspace((uschar)*key)) key++; + data = key; + while (*data != 0 && !isspace((uschar)*data)) data++; + *data++ = 0; + while (isspace((uschar)*data)) data++; + + dbwait = (dbdata_wait *)(&structbuffer); + Ustrcpy(dbwait->text, data); + + start = clock(); + while (count-- > 0) + rc = dbfn_write(dbblock + current, key, dbwait, + Ustrlen(data) + sizeof(dbdata_wait)); + stop = clock(); + if (rc != 0) printf("Failed: %s\n", strerror(errno)); + } + + else if (Ustrncmp(cmd, "read", 4) == 0) + { + uschar *key = cmd + 4; + if (current < 0) + { + printf("No current database\n"); + continue; + } + while (isspace((uschar)*key)) key++; + start = clock(); + while (count-- > 0) + dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL); + stop = clock(); + printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text); + } + + else if (Ustrncmp(cmd, "delete", 6) == 0) + { + uschar *key = cmd + 6; + if (current < 0) + { + printf("No current database\n"); + continue; + } + while (isspace((uschar)*key)) key++; + dbfn_delete(dbblock + current, key); + } + + else if (Ustrncmp(cmd, "scan", 4) == 0) + { + EXIM_CURSOR *cursor; + BOOL startflag = TRUE; + uschar *key; + uschar keybuffer[256]; + if (current < 0) + { + printf("No current database\n"); + continue; + } + start = clock(); + while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL) + { + startflag = FALSE; + Ustrcpy(keybuffer, key); + dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current, + keybuffer, NULL); + printf("%s: %s\n", keybuffer, dbwait->text); + } + stop = clock(); + printf("End of scan\n"); + } + + else if (Ustrncmp(cmd, "close", 5) == 0) + { + uschar *s = cmd + 5; + while (isspace((uschar)*s)) s++; + i = Uatoi(s); + if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else + { + start = clock(); + dbfn_close(dbblock + i); + stop = clock(); + dbblock[i].dbptr = NULL; + if (i == current) current = -1; + } + } + + else if (Ustrncmp(cmd, "file", 4) == 0) + { + uschar *s = cmd + 4; + while (isspace((uschar)*s)) s++; + i = Uatoi(s); + if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); + else current = i; + } + + else if (Ustrncmp(cmd, "time", 4) == 0) + { + showtime = ~showtime; + printf("Timing %s\n", showtime? "on" : "off"); + } + + else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break; + + else if (Ustrncmp(cmd, "help", 4) == 0) + { + printf("close [<number>] close file [<number>]\n"); + printf("delete <key> remove record from current file\n"); + printf("file <number> make file <number> current\n"); + printf("open <name> open db file\n"); + printf("q[uit] exit program\n"); + printf("read <key> read record from current file\n"); + printf("scan scan current file\n"); + printf("time time display on/off\n"); + printf("write <key> <rest-of-line> write record to current file\n"); + } + + else printf("Eh?\n"); + + if (showtime && stop >= start) + printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop, + (int)(stop - start)); + + printf("> "); + } + +for (i = 0; i < max_db; i++) + { + if (dbblock[i].dbptr != NULL) + { + printf("\nClosing %d", i); + dbfn_close(dbblock + i); + } + } + +printf("\n"); +return 0; +} + +#endif + +/* End of dbfn.c */ |