/*
* parm.c - written by ale in milano on 27sep2012
* parameter file parsing

Copyright (C) 2012-2023 Alessandro Vesely

This file is part of zdkimfilter

zdkimfilter 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 3 of the License, or
(at your option) any later version.

zdkimfilter 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 version 3
along with zdkimfilter.  If not, see <http://www.gnu.org/licenses/>.

Additional permission under GNU GPLv3 section 7:

If you modify zdkimfilter, or any covered part of it, by linking or combining
it with OpenSSL, OpenDKIM, Sendmail, or any software developed by The Trusted
Domain Project or Sendmail Inc., containing parts covered by the applicable
licence, the licensor of zdkimfilter grants you additional permission to convey
the resulting work.
*/
#include <config.h>
#if !ZDKIMFILTER_DEBUG
#define NDEBUG
#endif
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <limits.h>
#include <ctype.h>
#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include "parm.h"
#include "vb_fgets.h"
#include "util.h"
#include <assert.h>

/*
* logging weariness:
* zdkimfilter logs using fl_report, designed after Courier's error stream.
* zdkimsign emulates Courier behavior to run on syslog.
* other command utilities can get/set the logfun/program_name:
* fl_report, syslog, and stderrlog are viable candidates.
*/

static const char *program_name = "zfilter";
static logfun_t do_report = &stderrlog;

const char* set_program_name(const char * new_name)
{
	const char* rt = program_name;
	if (new_name)
		program_name = new_name;
	return rt;
}

logfun_t set_parm_logfun(logfun_t new_report)
{
	logfun_t rt = do_report;
	if (new_report)
		do_report = new_report;
	return rt;
}

#if !defined LOG_PRIMASK
#define LOG_PRIMASK \
(LOG_EMERG|LOG_ALERT|LOG_CRIT|LOG_ERR|LOG_WARNING|LOG_NOTICE|LOG_INFO|LOG_DEBUG)
#endif
void stderrlog(int severity, char const* fmt, ...)
{
	char const *logmsg;
	switch (severity & LOG_PRIMASK)
	{
		case LOG_EMERG:
		//	logmsg = "EMERG";
		//	break;

		case LOG_ALERT:
			logmsg = "ALERT";
			break;

		case LOG_CRIT:
			logmsg = "CRIT";
			break;

		case LOG_ERR:
		default:
			logmsg = "ERR";
			break;

		case LOG_WARNING:
			logmsg = "WARN";
			break;

		case LOG_NOTICE:
		//	logmsg = "NOTICE";
		//	break;

		case LOG_INFO:
			logmsg = "INFO";
			break;

		case LOG_DEBUG:
			logmsg = "DEBUG";
			break;
	}
	
	fprintf(stderr, "%s: %s: ", logmsg, program_name);
	va_list ap;
	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);
	fputc('\n', stderr);
}

typedef struct config_conf
{
	char const *name, *descr;
	int (*assign_fn)(void*, struct config_conf const*, char*);
	size_t offset, size;
	parm_target_t type_id;
} config_conf;

#define PARM_PTR(T) *(T*)(((char*)parm) + c->offset)

static int
assign_ptr(void *parm, config_conf const *c, char*s)
{
	assert(parm && c && s && c->size == sizeof(char*));
	char *v = strdup(s);
	if (v == NULL)
	{
		(*do_report)(LOG_ALERT, "MEMORY FAULT");
		return -1;
	}
	PARM_PTR(char*) = v;
	return 0;
}

static int
assign_char(void *parm, config_conf const *c, char*s)
{
	assert(parm && c && s && c->size == sizeof(char));
	char ch = *s, v;
	if (strchr("YyTt1", ch)) v = 1; //incl. ch == 0
	else if (strchr("Nn0", ch)) v = 0;
	else return -1;
	PARM_PTR(char) = v;
	return 0;
}

static int
assign_int(void *parm, config_conf const *c, char*s)
{
	assert(parm && c && s && c->size == sizeof(unsigned int));
	char *t = NULL;
	errno = 0;
	long l = strtol(s, &t, 0);
	if (l > INT_MAX || l < INT_MIN || !t || *t || errno == ERANGE) return -1;
	
	PARM_PTR(int) = (int)l;
	return 0;
}

static int hfields(char *h, const char **a)
/*
* Count the elements in h.  If a is given, assign pointers to
* each element of h, and 0-terminate the latter as well.
*
* Parameter: h - a space separated list of strings,
* a - NULL for counting only, allocated space for assignment.
*/
{
	assert(h);

	char *s = h;
	int ch, count = 0;
	
	for (;;)
	{
		while (isspace(ch = *(unsigned char*)s))
			++s;
		if (ch == 0)
			break;

		char *field = s;
		++count;
		++s;
		while (!isspace(ch = *(unsigned char*)s) && ch != 0)
			++s;
	
		if (a)
		{
			*a++ = field;
			*s++ = 0;
		}
		if (ch == 0)
			break;
	}
	return count;
}

static int
assign_array(void *parm, config_conf const *c, char*s)
{
	assert(parm && c && s && c->size == sizeof(char**));

	const char **a = NULL;
	int count = hfields(s, NULL);
	if (count > 0)
	{
		size_t l = strlen(s) + 1, n = (count + 1) * sizeof(char*);
		char *all = malloc(l + n);
		if (all == NULL)
		{
			(*do_report)(LOG_ALERT, "MEMORY FAULT");
			return -1;
		}
		a = (const char**)all;
		all += n;
		strcpy(all, s);
		a[count] = NULL;
		count -= hfields(all, a);
	}
	assert(count == 0);

	PARM_PTR(const char **) = a;
	return 0;
}

#define STRING2(P) #P
#define STRING(P) STRING2(P)
#define CONFIG(T,P,D,F) {STRING(P), D, F, \
	offsetof(T, P), sizeof(((T*)0)->P), T##_id }

static config_conf const conf[] =
{
	CONFIG(parm_t, all_mode, "Y/N", assign_char),
	CONFIG(parm_t, trust_a_r, "Y/N", assign_char),
	CONFIG(parm_t, verbose, "int", assign_int),
	CONFIG(parm_t, domain_keys, "key's directory", assign_ptr),
	CONFIG(parm_t, header_canon_relaxed, "Y/N, N for simple", assign_char),
	CONFIG(parm_t, body_canon_relaxed, "Y/N, N for simple", assign_char),
	CONFIG(parm_t, sign_rsa_sha1, "Y/N, N for rsa-sha256", assign_char),
	CONFIG(parm_t, key_choice_header, "key choice header", assign_array),
	CONFIG(parm_t, default_domain, "dns", assign_ptr), // used by zdkimsign.c
	CONFIG(parm_t, let_relayclient_alone, "Y/N, Y rewrite rcpts", assign_char),
	CONFIG(parm_t, selector, "global", assign_ptr),
	CONFIG(parm_t, sign_hfields, "space-separated, no colon", assign_array),
	CONFIG(parm_t, oversign_hfields, "space-separated, no colon", assign_array),
	CONFIG(parm_t, skip_hfields, "space-separated, no colon", assign_array),
	CONFIG(parm_t, xtag_value, "extension tag=value", assign_ptr),
	CONFIG(parm_t, no_signlen, "Y/N", assign_char),
	CONFIG(parm_t, no_qp_conversion, "Y/N", assign_char),
	CONFIG(parm_t, min_key_bits, "int", assign_int),
	CONFIG(parm_t, redact_received_auth, "any text", assign_ptr), // by redact.c
	CONFIG(parm_t, add_auth_pass, "Y/N", assign_char),
	CONFIG(parm_t, tmp, "temp directory", assign_ptr), // by zdkimsign.c
	CONFIG(parm_t, tempfail_on_error, "Y/N", assign_char),
	CONFIG(parm_t, split_verify, "exec name", assign_ptr),
	CONFIG(parm_t, add_ztags, "Y/N, Y for debug z=", assign_char),
	CONFIG(parm_t, blocked_user_list, "filename", assign_ptr),
	CONFIG(parm_t, no_spf, "Y/N", assign_char),
	CONFIG(parm_t, save_from_anyway, "Y/N", assign_char),
	CONFIG(parm_t, add_a_r_anyway, "Y/N", assign_char),
	CONFIG(parm_t, report_all_sigs, "Y/N", assign_char),
	CONFIG(parm_t, verify_one_domain, "Y/N", assign_char),
	CONFIG(parm_t, disable_experimental, "Y/N", assign_char),
	CONFIG(parm_t, noaddrrewrite, "Y/N", assign_char),
	CONFIG(parm_t, still_allow_no_from, "Y/N", assign_char),
	CONFIG(parm_t, checkhelo_if_no_auth, "Y/N", assign_char),
	CONFIG(parm_t, max_signatures, "int", assign_int),
	CONFIG(parm_t, log_dkim_order_above, "int", assign_int),
	CONFIG(parm_t, publicsuffix, "filename", assign_ptr),
	CONFIG(parm_t, honored_report_interval, "seconds", assign_int),
	CONFIG(parm_t, honor_dmarc, "Y/N", assign_char),
	CONFIG(parm_t, honor_author_domain, "Y/N", assign_char),
	CONFIG(parm_t, reject_on_nxdomain, "Y=procrustean ADSP", assign_char),
	CONFIG(parm_t, action_header, "header field name", assign_ptr),
	CONFIG(parm_t, header_action_is_reject, "Y/N", assign_char),
	CONFIG(parm_t, save_drop, "quarantine directory", assign_ptr),
	CONFIG(parm_t, dnswl_worthiness_pass, "int", assign_int),
	CONFIG(parm_t, dnswl_invalid_ip, "int", assign_int),
	CONFIG(parm_t, dnswl_octet_index, "int", assign_int),
	CONFIG(parm_t, trusted_dnswl, "space-separated dns.zones", assign_array),
	CONFIG(parm_t, whitelisted_pass, "int", assign_int),
	CONFIG(parm_t, dns_timeout, "secs", assign_int),
	CONFIG(parm_t, i_signer, "set the i= address", assign_ptr),
	CONFIG(parm_t, sign_local, "int, 0=never, 1=always, 2=unless already signed", assign_int),

	CONFIG(db_parm_t, db_backend, "conn", assign_ptr),
	CONFIG(db_parm_t, db_host, "conn", assign_ptr),
	CONFIG(db_parm_t, db_port, "conn", assign_ptr),
	CONFIG(db_parm_t, db_opt_tls, "A/N/T if given", assign_ptr),
	CONFIG(db_parm_t, db_opt_multi_statements, "Y/N if given", assign_char),
	CONFIG(db_parm_t, db_opt_compress, "Y/N if given", assign_char),
	CONFIG(db_parm_t, db_opt_mode, "", assign_ptr),
	CONFIG(db_parm_t, db_opt_paged_results, "int", assign_int),
	CONFIG(db_parm_t, db_timeout, "secs", assign_int),
	CONFIG(db_parm_t, db_trace_sql, "Y/N if given", assign_char),
	CONFIG(db_parm_t, db_database, "", assign_ptr),
	CONFIG(db_parm_t, db_user, "credentials", assign_ptr),
	CONFIG(db_parm_t, db_password, "credentials", assign_ptr),

#define DATABASE_STATEMENT(x) CONFIG(db_parm_t, x, "", assign_ptr),
	#include "database_statements.h"
#undef DATABASE_STATEMENT

	{NULL, NULL, NULL, 0, 0, 0}
};
#define CONF_ELEMENTS (sizeof conf/sizeof conf[0])
#undef CONFIG


void print_parm(void *parm_target[PARM_TARGET_SIZE])
{
	config_conf const *c = &conf[0];
	while (c->name)
	{
		int i = 0;
		void *parm = parm_target[c->type_id];

		if (parm)
		{
			printf("%-24s = ", c->name);
			if (c->size == 1U)
			{
				char v = PARM_PTR(char);
				char const*rv;
				switch (v)
				{
					case 0: rv = "N"; break;
					case 1: rv = "Y"; break;
					default: rv = "not given"; break;
				}
				fputs(rv, stdout);
			}
			else if (c->assign_fn == assign_ptr)
			{
				char const * const p = PARM_PTR(char*);
				fputs(p? p: "NULL", stdout);
			}
			else if (c->assign_fn == assign_array)
			{
				char const ** const a = PARM_PTR(char const**);
				if (a == NULL)
					fputs("NULL", stdout);
				else
				{
					if (c->descr && c->descr[0])
						printf(" (%s)", c->descr);
					fputc('\n', stdout);
					for (; a[i]; ++i)
						printf("%26d %s\n", i, a[i]);

					i = 1;
				}
			}
			else
				printf("%d", PARM_PTR(int));

			if (i == 0)
			{
				if (c->descr && c->descr[0])
					printf(" (%s)", c->descr);
				fputc('\n', stdout);
			}
		}
		++c;
	}
}

void clear_parm(void *parm_target[PARM_TARGET_SIZE])
{
	config_conf const *c = &conf[0];
	while (c->name)
	{
		void *parm = parm_target[c->type_id];

		if (parm)
		{
			if (c->assign_fn == assign_ptr ||
				c->assign_fn == assign_array)
			{
				free(PARM_PTR(void*));
				PARM_PTR(void*) = NULL;
			}
		}
		++c;
	}
}

static config_conf const* conf_name(char const *p)
{
	for (config_conf const *c = conf; c->name; ++c)
		if (stricmp(c->name, p) == 0)
			return c;

	return NULL;
}

typedef struct track_definition
{
	short int define;
	char skip, recurse;
} track_definition;

typedef enum keyword
{
	keyword_none,
	keyword_override,
	keyword_include
} keyword;

static char* skip_space_equal(char *s)
{
	assert(s);

	while (isspace(*(unsigned char*)s))
		++s;

	if (*s == '=')
	{
		++s;
		while (isspace(*(unsigned char*)s))
			++s;
	}

	return s;
}


static keyword get_keyword(char **ps, char **name)
{
	assert(ps);
	assert(!isspace(*(unsigned char*)*ps));
	assert(name);

	char *s = *ps, *entry = s;
	keyword key = keyword_none;
	int ch;

	while (isalnum(ch = *(unsigned char*)s) || ch == '_')
		++s;

	if (s - entry == 8 && strncmp(entry, "override", 8) == 0)
		key = keyword_override;
	else if (s - entry == 7 && strncmp(entry, "include", 7) == 0)
		key = keyword_include;

	if (key != keyword_none)
	{
		entry = s = skip_space_equal(s);
		if (key == keyword_override)  // advance until end of name
			while (isalnum(ch = *(unsigned char*)s) || ch == '_')
				++s;
		else // include, trim trailing spaces
		{
			while (*s)
				++s;
			--s;
			while (isspace(*(unsigned char*)s))
				--s;
			++s;
		}
	}

	*ps = s;
	*name = entry;
	return key;
}

static int do_read_all_values(void *parm_target[PARM_TARGET_SIZE],
	char const *fname, track_definition track_def[], int recurse)
// recursive initialization, 0 on success
{
	assert(parm_target);
	assert(fname);

	int line_no = 0;
	errno = 0;

	FILE *fp = fopen(fname, "r");
	if (fp == NULL)
	{
		int saveerrno = errno;
		char *sep = "", *pcwd = "";
		char cwd[PATH_MAX];
		if (*fname != '/')
		{
			pcwd = getcwd(cwd, sizeof cwd);
			if (pcwd == NULL)
				pcwd = "";
			else
				sep = ", current directory is ";
		}
		(*do_report)(LOG_ALERT,
			"Cannot read %s: %s%s%s", fname, strerror(errno), sep, pcwd);
		return saveerrno == ENOENT? -2: -1;;
	}
	
	var_buf vb;
	if (vb_init(&vb))
	{
		fclose(fp);
		return -1;
	}

	int errs = 0;
	size_t keep = 0;
	char *p;

	while ((p = vb_fgets(&vb, keep, fp)) != NULL)
	{
		char *eol = p + strlen(p) - 1;
		int ch = 0;
		++line_no;

		while (eol >= p && isspace(ch = *(unsigned char*)eol))
			*eol-- = 0;

		if (ch == '\\')
		{
			*eol = ' '; // this replaces the backslash
			keep += eol + 1 - p;
			continue;
		}

		/*
		* full logic line
		*/
		keep = 0;

		char *s = p = vb.buf;
		while (isspace(ch = *(unsigned char*)s))
			++s;
		if (ch == '#' || ch == 0)
			continue;

		char *name;
		keyword key = get_keyword(&s, &name);

		ch = *s;
		*s = 0;
		if (key == keyword_include)
		{
			int rtc = recurse > 20? -3:
				do_read_all_values(parm_target, name, track_def, recurse + 1);
			if (rtc < 0)
			{
				if (rtc == -2) // ENOENT
					(*do_report)(LOG_INFO,
						"%s was included by %s", name, fname);
				else if (rtc == -3)
				{
					(*do_report)(LOG_ALERT,
						"Too much recursion at line %d in %s",
						line_no, fname);
					rtc = -1;
				}
				fclose(fp);
				vb_clean(&vb);
				return rtc;
			}
			errs += rtc;
			continue;
		}

		config_conf const *c = conf_name(name);
		if (c == NULL)
		{
			(*do_report)(LOG_ERR,
				"Invalid name %s at line %d in %s", name, line_no, fname);
			++errs;
			continue;
		}

		int const conf_ndx = c - &conf[0];
		void *const parm = parm_target[c->type_id];
		if (track_def[conf_ndx].define) // already defined?
		{
			if (c->assign_fn == assign_ptr || c->assign_fn == assign_array)
				free(PARM_PTR(void*));

			if (key != keyword_override)
			{
				char const *prev =
					track_def[conf_ndx].recurse < recurse? "the file that included this":
					track_def[conf_ndx].recurse > recurse? "an included file":
					"this file";
				(*do_report)(LOG_WARNING,
					"Repeated definition of %s at line %d in %s (previous definition was in %s)",
					name, line_no, fname, prev);
			}
		}
		track_def[conf_ndx].recurse = recurse;
		track_def[conf_ndx].define += 1;

		*s = ch;
		if (track_def[conf_ndx].skip)
			continue;

		char *const value = skip_space_equal(s);

		if (parm != NULL && (*c->assign_fn)(parm, c, value) != 0)
		{
			(*do_report)(LOG_ERR,
				"Invalid value %s for %s at line %d in %s",
					value, c->name, line_no, fname);
			++errs;
		}
	}

	vb_clean(&vb);
	fclose(fp);

	return errs;
}

static track_definition *get_track_def(void)
{
	static const unsigned int size = CONF_ELEMENTS * sizeof(track_definition);
	track_definition *track_def = malloc(size);
	if (track_def)
		memset(track_def, 0, size);
	return track_def;
}

int read_all_values(void *parm_target[PARM_TARGET_SIZE], char const *fname)
{
	track_definition *track_def = get_track_def();
	if (track_def == NULL)
		return -1;

	int rtc = do_read_all_values(parm_target, fname, track_def, 0);
	free(track_def);
	return rtc;
}

int read_single_values(char const *fname, int n,
	char const **names, config_item *out)
/*
* Read a few values from parameter file and set malloced string in out.
* return -1 for fname problem, memory fault, invalid parameter;
* otherwise return a flag of the values set, where bit 0 corresponds to out[0],
* bit 1 to out[1], and so forth.

* Only string values are supported.
* Only values in parm_t are supported.
*/
{
	if (n <= 0 || n > 8)
		return -1;

	int ndx[n];
	for (int i = 0; i < n; ++i)
	{
		assert(names[i]);
		config_conf const *c = conf_name(names[i]);
		if (c == NULL)
			return -1;

		assert(c->type_id == parm_t_id);
		ndx[i] = c - &conf[0];
	}

	parm_t *parm = calloc(1, sizeof *parm);
	if (parm == NULL)
		return -1;

	void *parm_target[PARM_TARGET_SIZE];
	parm_target[parm_t_id] = parm;
	parm_target[db_parm_t_id] = NULL;

	int value_flags = -1;
	track_definition *track_def = get_track_def();
	if (track_def == NULL)
		goto error_exit;

	for (unsigned int i = 0; i < CONF_ELEMENTS; ++i)
		track_def[i].skip = 1;

	for (int i = 0; i < n; ++i)
		track_def[ndx[i]].skip = 0;

	if (fname == NULL)
		fname = default_config_file;

	int errs = do_read_all_values(parm_target, fname, track_def, 0);
	if (errs)
		goto error_exit;

	int mask = 1;
	value_flags = 0;

	for (int i = 0; i < n; ++i, mask <<= 1)
	{
		value_flags |= mask;
		config_conf const *c = &conf[ndx[i]];

		if (c->assign_fn == assign_ptr)
		{
			out[i].p = PARM_PTR(char *);
			PARM_PTR(char *) = NULL;
		}
		else if (c->assign_fn == assign_array)
		{
			out[i].a = PARM_PTR(char **);
			PARM_PTR(const char **) = NULL;
		}
		else if (c->assign_fn == assign_char)
			out[i].c = PARM_PTR(char);

		else if (c->assign_fn == assign_int)
			out[i].i = PARM_PTR(int);

		else
			assert(0);
	}

error_exit:
	free(track_def);
	clear_parm(parm_target);
	free(parm);

	return value_flags;
}


#ifdef TEST_PARM
static void do_names(char const *fname, int argc, char const *argv[])
{
	config_item *out = calloc(argc, sizeof *out);
	int rtc = read_single_values(fname, argc, argv, out);
	printf("read_single_values(%s) = %x\n", fname, rtc);

	for (int i = 0; i < argc; ++i)
	{
		config_conf const *c = conf_name(argv[i]);

		printf ("%s =", argv[i]);
		if (c == NULL)
			printf(" not found\n");
		else if (c->assign_fn == assign_ptr)
		{
			char *p = out[i].p;
			printf(" %s\n", p? p: "NULL");
			free(p);
		}
		else if (c->assign_fn == assign_array)
		{
			char **a = out[i].a;
			while (a && *a)
			{
				char *p = *a++;
				printf(" %s",  p? p: "NULL");
			}
			putchar('\n');
			free(out[i].a);
		}
		else if (c->assign_fn == assign_char)
			printf(" %c\n", out[i].c);

		else if (c->assign_fn == assign_int)
			printf(" %d\n", out[i].i);

		else
			printf(" TYPE FAILURE!!\n");
	}

	free(out);
}

int main(int argc, char const *argv[])
{
	int first_name = 1;
	int i;
	for (i = 1; i < argc; ++i)
	{
		if (argv[i][0] == '-')
		{
			if (argv[i][1] == 'f' && argv[i][2] == 0)
			{
				if (first_name > 0 && first_name < i)
					do_names(argv[i + 1], i - first_name, argv + first_name);
				else
					first_name = -1;
				continue;
			}

			else /* if ((argv[i][1] == 'h' && argv[i][2] == 0) ||
				strcmp(argv[i], "--help") == 0) */
			{
				printf("%s { [-f] arg}...\n"
					"where arg is either a config file (if -f) or a name.\n"
					"If the file name is given after some names, only those\n"
					"names are printed\n",
					argv[0]);
				break;
			}
		}

		if (first_name < 0)
		{
			parm_t *parm = calloc(1, sizeof *parm);
			if (parm == NULL)
				return 1;

			void *parm_target[PARM_TARGET_SIZE];
			parm_target[parm_t_id] = parm;
			parm_target[db_parm_t_id] = NULL;

			int rtc = read_all_values(parm_target, argv[i]);
			printf("read_all_values(parm, %s) = %d\n", argv[i], rtc);
			print_parm(parm_target);
			clear_parm(parm_target);
			free(parm);
			first_name = i + 1;
		}
	}
}
#endif // TEST_PARM
