/*++
/* NAME
/*	dynamicmaps 3
/* SUMMARY
/*	load dictionaries dynamically
/* SYNOPSIS
/*	#include <dynamicmaps.h>
/*
/*	void dymap_init(const char *conf_path, const char *plugin_dir)
/* DESCRIPTION
/*	This module reads the dynamicmaps.cf file and performs
/*	run-time loading of Postfix dictionaries. Each dynamicmaps.cf
/*	entry specifies the name of a dictionary type, the pathname
/*	of a shared-library object, the name of a "dict_open"
/*	function for access to individual dictionary entries, and
/*	optionally the name of a "mkmap_open" function for bulk-mode
/*	dictionary creation. Plugins may be specified with a relative
/*	pathname.
/*
/*	A dictionary may be installed without editing the file
/*	dynamicmaps.cf, by placing a configuration file under the
/*	directory dynamicmaps.cf.d, with the same format as
/*	dynamicmaps.cf.
/*
/*	dymap_init() reads the specified configuration file which
/*	is in dynamicmaps.cf format, and hooks itself into the
/*	dict_open(), dict_mapnames(), and mkmap_open() functions.
/*
/*	dymap_init() may be called multiple times during a process
/*	lifetime, but it will not "unload" dictionaries that have
/*	already been linked into the process address space, nor
/*	will it hide their dictionaries types from later "open"
/*	requests.
/*
/*	Arguments:
/* .IP conf_path
/*	Pathname for the dynamicmaps configuration file.
/* .IP plugin_dir
/*	Default directory for plugins with a relative pathname.
/* SEE ALSO
/*	load_lib(3) low-level run-time linker adapter
/* DIAGNOSTICS
/*	Fatal errors: memory allocation problem, dictionary or
/*	dictionary function not available.  Panic: invalid use.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	LaMont Jones
/*	Hewlett-Packard Company
/*	3404 Harmony Road
/*	Fort Collins, CO 80528, USA
/*
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*--*/

 /*
  * System library.
  */
#include <sys_defs.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>

 /*
  * Utility library.
  */
#include <msg.h>
#include <mymalloc.h>
#include <htable.h>
#include <argv.h>
#include <dict.h>
#include <load_lib.h>
#include <vstring.h>
#include <vstream.h>
#include <vstring_vstream.h>
#include <stringops.h>
#include <split_at.h>
#include <scan_dir.h>

 /*
  * Global library.
  */
#include <mkmap.h>
#include <dynamicmaps.h>

#ifdef USE_DYNAMIC_MAPS

 /*
  * Contents of one dynamicmaps.cf entry.
  */
typedef struct {
    char   *soname;			/* shared-object file name */
    char   *dict_name;			/* dict_xx_open() function name */
    char   *mkmap_name;			/* mkmap_xx_open() function name */
} DYMAP_INFO;

static HTABLE *dymap_info;
static int dymap_hooks_done = 0;
static DICT_OPEN_EXTEND_FN saved_dict_open_hook = 0;
static MKMAP_OPEN_EXTEND_FN saved_mkmap_open_hook = 0;
static DICT_MAPNAMES_EXTEND_FN saved_dict_mapnames_hook = 0;

#define STREQ(x, y) (strcmp((x), (y)) == 0)

/* dymap_dict_lookup - look up "dict_foo_open" function */

static DICT_OPEN_FN dymap_dict_lookup(const char *dict_type)
{
    struct stat st;
    LIB_FN  fn[2];
    DICT_OPEN_FN dict_open_fn;
    DYMAP_INFO *dp;

    /*
     * Respect the hook nesting order.
     */
    if (saved_dict_open_hook != 0
	&& (dict_open_fn = saved_dict_open_hook(dict_type)) != 0)
	return (dict_open_fn);

    /*
     * Allow for graceful degradation when a database is unavailable. This
     * allows Postfix daemon processes to continue handling email with
     * reduced functionality.
     */
    if ((dp = (DYMAP_INFO *) htable_find(dymap_info, dict_type)) == 0)
	return (0);
    if (stat(dp->soname, &st) < 0) {
	msg_warn("unsupported dictionary type: %s (%s: %m)",
		 dict_type, dp->soname);
	return (0);
    }
    if (st.st_uid != 0 || (st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
	msg_warn("unsupported dictionary type: %s "
		 "(%s: file is owned or writable by non-root users)",
		 dict_type, dp->soname);
	return (0);
    }
    fn[0].name = dp->dict_name;
    fn[1].name = 0;
    load_library_symbols(dp->soname, fn, (LIB_DP *) 0);
    return ((DICT_OPEN_FN) fn[0].fptr);
}

/* dymap_mkmap_lookup - look up "mkmap_foo_open" function */

static MKMAP_OPEN_FN dymap_mkmap_lookup(const char *dict_type)
{
    struct stat st;
    LIB_FN  fn[2];
    MKMAP_OPEN_FN mkmap_open_fn;
    DYMAP_INFO *dp;

    /*
     * Respect the hook nesting order.
     */
    if (saved_mkmap_open_hook != 0
	&& (mkmap_open_fn = saved_mkmap_open_hook(dict_type)) != 0)
	return (mkmap_open_fn);

    /*
     * All errors are fatal. If the postmap(1) or postalias(1) command can't
     * create the requested database, then graceful degradation is not
     * useful.
     * 
     * Fix 20220416: if this dictionary type is registered for some non-mkmap
     * purpose, then don't talk nonsense about a missing package.
     */
    if ((dp = (DYMAP_INFO *) htable_find(dymap_info, dict_type)) == 0) {
	ARGV   *types = dict_mapnames();
	char  **cpp;

	for (cpp = types->argv; *cpp; cpp++) {
	    if (strcmp(dict_type, *cpp) == 0)
		msg_fatal("unsupported dictionary type: %s does not support "
			  "bulk-mode creation.", dict_type);
	}
	msg_fatal("unsupported dictionary type: %s. "
		  "Is the postfix-%s package installed?",
		  dict_type, dict_type);
    }
    if (!dp->mkmap_name)
	msg_fatal("unsupported dictionary type: %s does not support "
		  "bulk-mode creation.", dict_type);
    if (stat(dp->soname, &st) < 0)
	msg_fatal("unsupported dictionary type: %s (%s: %m). "
		  "Is the postfix-%s package installed?",
		  dict_type, dp->soname, dict_type);
    if (st.st_uid != 0 || (st.st_mode & (S_IWGRP | S_IWOTH)) != 0)
	msg_fatal("unsupported dictionary type: %s "
		  "(%s: file is owned or writable by non-root users)",
		  dict_type, dp->soname);
    fn[0].name = dp->mkmap_name;
    fn[1].name = 0;
    load_library_symbols(dp->soname, fn, (LIB_DP *) 0);
    return ((MKMAP_OPEN_FN) fn[0].fptr);
}

/* dymap_list - enumerate dynamically-linked database type names */

static void dymap_list(ARGV *map_names)
{
    HTABLE_INFO **ht_list, **ht;

    /*
     * Respect the hook nesting order.
     */
    if (saved_dict_mapnames_hook != 0)
	saved_dict_mapnames_hook(map_names);

    for (ht_list = ht = htable_list(dymap_info); *ht != 0; ht++)
	argv_add(map_names, ht[0]->key, ARGV_END);
    myfree((void *) ht_list);
}

/* dymap_entry_alloc - allocate dynamicmaps.cf entry */

static DYMAP_INFO *dymap_entry_alloc(char **argv)
{
    DYMAP_INFO *dp;

    dp = (DYMAP_INFO *) mymalloc(sizeof(*dp));
    dp->soname = mystrdup(argv[0]);
    dp->dict_name = mystrdup(argv[1]);
    dp->mkmap_name = argv[2] ? mystrdup(argv[2]) : 0;
    return (dp);
}

/* dymap_entry_free - htable(3) call-back to destroy dynamicmaps.cf entry */

static void dymap_entry_free(void *ptr)
{
    DYMAP_INFO *dp = (DYMAP_INFO *) ptr;

    myfree(dp->soname);
    myfree(dp->dict_name);
    if (dp->mkmap_name)
	myfree(dp->mkmap_name);
    myfree((void *) dp);
}

/* dymap_read_conf - read dynamicmaps.cf-like file */

static void dymap_read_conf(const char *path, const char *path_base)
{
    VSTREAM *fp;
    VSTRING *buf;
    char   *cp;
    ARGV   *argv;
    int     linenum = 0;
    struct stat st;

    /*
     * Silently ignore a missing dynamicmaps.cf file, but be explicit about
     * problems when the file does exist.
     */
    if ((fp = vstream_fopen(path, O_RDONLY, 0)) != 0) {
	if (fstat(vstream_fileno(fp), &st) < 0)
	    msg_fatal("%s: fstat failed; %m", path);
	if (st.st_uid != 0 || (st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
	    msg_warn("%s: file is owned or writable by non-root users"
		     " -- skipping this file", path);
	} else {
	    buf = vstring_alloc(100);
	    while (vstring_get_nonl(buf, fp) != VSTREAM_EOF) {
		cp = vstring_str(buf);
		linenum++;
		if (*cp == '#' || *cp == '\0')
		    continue;
		argv = argv_split(cp, " \t");
		if (argv->argc != 3 && argv->argc != 4)
		    msg_fatal("%s, line %d: Expected \"dict-type .so-name dict"
			      "-function [mkmap-function]\"", path, linenum);
		if (!ISALNUM(argv->argv[0][0]))
		    msg_fatal("%s, line %d: unsupported syntax \"%s\"",
			      path, linenum, argv->argv[0]);
		if (argv->argv[1][0] != '/') {
		    cp = concatenate(path_base, "/", argv->argv[1], (char *) 0);
		    argv_replace_one(argv, 1, cp);
		    myfree(cp);
		}
		if (htable_locate(dymap_info, argv->argv[0]) != 0)
		    msg_warn("%s: ignoring duplicate entry for \"%s\"",
			     path, argv->argv[0]);
		else
		    htable_enter(dymap_info, argv->argv[0],
				 (void *) dymap_entry_alloc(argv->argv + 1));
		argv_free(argv);
	    }
	    vstring_free(buf);

	    /*
	     * Once-only: hook into the dict_open(3) and mkmap_open(3)
	     * infrastructure,
	     */
	    if (dymap_hooks_done == 0) {
		dymap_hooks_done = 1;
		saved_dict_open_hook = dict_open_extend(dymap_dict_lookup);
		saved_mkmap_open_hook = mkmap_open_extend(dymap_mkmap_lookup);
		saved_dict_mapnames_hook = dict_mapnames_extend(dymap_list);
	    }
	}
	vstream_fclose(fp);
    } else if (errno != ENOENT) {
	msg_fatal("%s: file open failed: %m", path);
    }
}

/* dymap_init - initialize dictionary type to soname etc. mapping */

void    dymap_init(const char *conf_path, const char *plugin_dir)
{
    static const char myname[] = "dymap_init";
    SCAN_DIR *dir;
    char   *conf_path_d;
    const char *conf_name;
    VSTRING *sub_conf_path;

    /*
     * Reload dynamicmaps.cf, but don't reload already-loaded plugins.
     */
    if (dymap_info != 0)
	htable_free(dymap_info, dymap_entry_free);
    dymap_info = htable_create(3);

    /*
     * Read dynamicmaps.cf.
     */
    dymap_read_conf(conf_path, plugin_dir);

    /*
     * Read dynamicmaps.cf.d/filename entries.
     */
    conf_path_d = concatenate(conf_path, ".d", (char *) 0);
    if (access(conf_path_d, R_OK | X_OK) == 0
	&& (dir = scan_dir_open(conf_path_d)) != 0) {
	sub_conf_path = vstring_alloc(100);
	while ((conf_name = scan_dir_next(dir)) != 0) {
	    vstring_sprintf(sub_conf_path, "%s/%s", conf_path_d, conf_name);
	    dymap_read_conf(vstring_str(sub_conf_path), plugin_dir);
	}
	if (errno != 0)
	    /* Don't crash all programs - degrade gracefully. */
	    msg_warn("%s: directory read error: %m", conf_path_d);
	scan_dir_close(dir);
	vstring_free(sub_conf_path);
    } else if (errno != ENOENT) {
	/* Don't crash all programs - degrade gracefully. */
	msg_warn("%s: directory open failed: %m", conf_path_d);
    }
    myfree(conf_path_d);

    /*
     * Future proofing, in case someone "improves" the code. We can't hook
     * into other functions without initializing our private lookup table.
     */
    if (dymap_hooks_done != 0 && dymap_info == 0)
	msg_panic("%s: post-condition botch", myname);
}

#endif
