/* Distributed Checksum Clearinghouse
 *
 * --C-LICENSE--
 * $Revision: 1.81 $
 */

#include "dcc_ck.h"
#include "dcc_xhdr.h"


const char *
wf_fnm(const WF *wf, int fno)
{
	if (!fno) {
		return wf->ascii_nm.c;
	} else {
		return wf->wtbl->hdr.white_incs[fno-1].nm.c;
	}
}



const char *
wf_fnm_lno(DCC_FNM_LNO_BUF *buf, const WF *wf)
{
	int fno;

	if (!wf)
		return "";

	fno = wf->fno;
	if (!fno)
		return dcc_fnm_lno(buf, wf->ascii_nm.c, wf->lno);

	snprintf(buf->b, sizeof(buf->b),
		 DCC_FNM_LNO_PAT" included in %s",
		 wf->lno, dcc_path2fnm(wf->wtbl->hdr.white_incs[fno-1].nm.c),
		 wf->ascii_nm.c);
	return buf->b;
}



DCC_TGTS
dcc_str2thold(DCC_CK_TYPES type, const char *str)
{
	u_long l;
	char *p;

	l = strtoul(str, &p, 10);
	if (*p != '\0') {
		if (*p == '%') {
			if (*++p != '\0' || type != DCC_CK_REP_BULK)
				return DCC_TGTS_INVALID;
		} else if (!strcasecmp(str, DCC_XHDR_TOO_MANY)) {
			l = DCC_TGTS_TOO_MANY;
		} else if (!strcasecmp(str, DCC_XHDR_THOLD_NEVER)) {
			l = THOLD_NEVER;
		} else {
			return DCC_TGTS_INVALID;
		}
	}

	if (l <= DCC_TGTS_TOO_MANY) {
		/* You cannot have a reputation worse than 100%.
		 * Use "never" to turn off reputation rejections or logging */
		if (type == DCC_CK_REP_BULK && l > 100)
			return DCC_TGTS_INVALID;
		/* the reputation threshold for "bulk" must be finite */
		if (type == DCC_CK_REP_TOTAL && l >= DCC_TGTS_TOO_MANY)
			return DCC_TGTS_INVALID;
	}

	return l;
}



/* look for a word followed by whitespace */
static const char *
ck_word_white(const char *p,
	      const char *word,
	      int word_len)
{
	int sps;

	if (strncasecmp(p, word, word_len))
		return 0;

	p += word_len;
	sps = strspn(p, DCC_WHITESPACE);
	if (sps == 0)
		return 0;

	return p+sps;
}



/* parse DCC thresholds */
static u_char
option_thold(DCC_EMSG *emsg, WF *wf,
	     const char *opt,
	     DCC_FNM_LNO_BUF *fnm_buf,
	     DCC_FNM_LNO_BUF *thold_fnm_buf)
{
	const char *thold_type, *thold;
	DCC_TGTS thold_tgts;
	DCC_CK_TYPES type, t2;
	u_char result;

	thold_type = ck_word_white(opt, "threshold", LITZ("threshold"));
	if (!thold_type) {
		/* it is not a threshold setting */
		dcc_pemsg(EX_DATAERR, emsg,
			  "unrecognized \"option %s\"%s",
			  opt, wf_fnm_lno(fnm_buf, wf));
		return 0;
	}

	thold = strchr(thold_type, ',');
	if (!thold_type) {
		dcc_pemsg(EX_DATAERR, emsg,
			  "no comma in \"option %s\"%s",
			  opt, wf_fnm_lno(fnm_buf, wf));
		return 0;
	}

	type = dcc_str2type_thold(thold_type, thold - thold_type);
	if (type == DCC_CK_INVALID) {
		dcc_pemsg(EX_DATAERR, emsg,
			  "unknown checksum type in \"option %s\"%s",
			  opt, wf_fnm_lno(fnm_buf, wf));
		return 0;
	}
	thold_tgts = dcc_str2thold(type, ++thold);
	if (thold_tgts == DCC_TGTS_INVALID) {
		dcc_pemsg(EX_DATAERR, emsg,
			  "unrecognized threshold in \"option %s\"%s",
			  opt, wf_fnm_lno(fnm_buf, wf));
		return 0;
	}


	wf_fnm_lno(thold_fnm_buf, wf);
	result = 1;
	for (t2 = DCC_CK_TYPE_FIRST; t2 <= DCC_CK_TYPE_LAST; ++t2) {
		if (!(t2 == type
		      || (type == SET_ALL_THOLDS && IS_ALL_CKSUM(t2))
		      || (type == SET_CMN_THOLDS && IS_CMN_CKSUM(t2))))
			continue;
		if (wf->wtbl->hdr.tholds_rej.t[t2] != THOLD_UNSET
		    && wf->wtbl->hdr.tholds_rej.t[t2] != thold_tgts) {
			if (result)
				dcc_pemsg(EX_DATAERR, emsg,
					  "conflicting threshold setting%s",
					  thold_fnm_buf->b);
			result = 0;
		} else {
			wf->wtbl->hdr.tholds_rej.t[t2] = thold_tgts;
			wf->wtbl_fgs |= WHITE_FG_HAVE_THOLDS;
			wf->wtbl->hdr.fgs = wf->wtbl_fgs;
		}
	}

	return result;
}



/* honor controls in client whitelists of the forms:
 *	option log-{all,normal}
 *	option log-subdirectory-{day,hour,minute}
 *	option greylist-{on,off,
 *			ignore-spam-on,ignore-spam-off,
 *			log-on,log-off}
 *	option DCC-{on,off}
 *	option DCC-rep-{on,off}
 *	option dnsbl {1,2,3}-{on,off}
 *	option dnsbl-{on,off}		obsolete
 *	option MTA-{first,last}
 *	option forced-discard-ok
 *	option no-forced-discard
 *	option forced-discard-nok	obsolete
 *	option threshold CKSUM,THOLD
 *	option spam-trap-discard
 *	option spam-trap-reject
 *	option not-spam-trap
 * change the sample whiteclnt file when this changes
 */
static u_char
parse_option(DCC_EMSG *emsg,
	     WF *wf,
	     const char *opt,
	     DCC_FNM_LNO_BUF *fnm_buf,
	     DCC_FNM_LNO_BUF *thold_fnm_buf)
{
	static const struct {
	    const char	*str;
	    int		len;
	    WHITE_SWS	on;		/* turn on this bit */
	    WHITE_SWS	conflicts;	/* conflict with these bits */
	} *tp, *tp1, tbl[] = {
#	    define DE(s,on,conflicts) {s, LITZ(s), on, conflicts},
#	    define DE_DNSBL(s,n) DE("DNSBL"s"-on", WHITE_SW_DNSBL_ON(n),    \
				    WHITE_SW_DNSBL_OFF(n) | WHITE_SW_TRAPS) \
		DE("DNSBL"s"-off", WHITE_SW_DNSBL_OFF(n),		    \
		   WHITE_SW_DNSBL_ON(n))
	    DE("log-all",
	       WHITE_SW_LOG_ALL,
	       WHITE_SW_LOG_NORMAL)
	    DE("log-normal",
	       WHITE_SW_LOG_NORMAL,
	       WHITE_SW_LOG_ALL)

	    DE("log-subdirectory-day",
	       WHITE_SW_LOG_D,
	       WHITE_SW_LOG_H | WHITE_SW_LOG_M)
	    DE("log-subdirectory-hour",
	       WHITE_SW_LOG_H,
	       WHITE_SW_LOG_D | WHITE_SW_LOG_M)
	    DE("log-subdirectory-minute",
	       WHITE_SW_LOG_M,
	       WHITE_SW_LOG_D | WHITE_SW_LOG_H)

	    DE("greylist-on",
	       WHITE_SW_GREY_ON,
	       WHITE_SW_GREY_OFF)
	    DE("greylist-off",
	       WHITE_SW_GREY_OFF,
	       WHITE_SW_GREY_ON)

	    DE("greylist-ignore-spam-on",
	       WHITE_SW_GREY_SPAM_ON,
	       WHITE_SW_GREY_SPAM_OFF)
	    DE("greylist-ignore-spam-off",
	       WHITE_SW_GREY_SPAM_OFF,
	       WHITE_SW_GREY_SPAM_ON)

	    DE("greylist-log-on",
	       WHITE_SW_GREY_LOG_ON,
	       WHITE_SW_GREY_LOG_OFF)
	    DE("greylist-log-off",
	       WHITE_SW_GREY_LOG_OFF,
	       WHITE_SW_GREY_LOG_ON)

	    DE("DCC-on",
	       WHITE_SW_DCC_ON,
	       WHITE_SW_DCC_OFF)
	    DE("DCC-off",
	       WHITE_SW_DCC_OFF,
	       WHITE_SW_DCC_ON
	       | WHITE_SW_TRAP_DIS | WHITE_SW_TRAP_REJ)

	    DE("forced-discard-ok",
	       WHITE_SW_DISCARD_OK,
	       WHITE_SW_DISCARD_NO)
	    DE("no-forced-discard",
	       WHITE_SW_DISCARD_NO,
	       WHITE_SW_DISCARD_OK
	       | WHITE_SW_TRAP_DIS | WHITE_SW_TRAP_REJ)
	    DE("no_forced-discard",	/* bug that might still be in use */
	       WHITE_SW_DISCARD_NO,
	       WHITE_SW_DISCARD_OK
	       | WHITE_SW_TRAP_DIS | WHITE_SW_TRAP_REJ)
	    DE("forced-discard-nok",	/* obsolete */
	       WHITE_SW_DISCARD_NO,
	       WHITE_SW_DISCARD_OK
	       | WHITE_SW_TRAP_DIS | WHITE_SW_TRAP_REJ)

	    DE("MTA-first",
	       WHITE_SW_MTA_FIRST,
	       WHITE_SW_MTA_LAST)
	    DE("MTA-last",
	       WHITE_SW_MTA_LAST,
	       WHITE_SW_MTA_FIRST)

	    DE("DCC-rep-on",
	       WHITE_SW_REP_ON,
	       WHITE_SW_REP_OFF)
	    DE("DCC-rep-off",
	       WHITE_SW_REP_OFF,
	       WHITE_SW_REP_ON)
	    DE("DCC-reps-on",		/* obsolete */
	       WHITE_SW_REP_ON,
	       WHITE_SW_REP_OFF)
	    DE("DCC-reps-off",		/* obsolete */
	       WHITE_SW_REP_OFF,
	       WHITE_SW_REP_ON)

	    DE_DNSBL("1",0)
	    DE_DNSBL("2",1)
	    DE_DNSBL("3",2)
	    DE_DNSBL("4",3)
	    DE_DNSBL(,0)		/* obsolete */

	    DE(DCC_XHDR_TRAP_NOT,
	       WHITE_SW_TRAP_NOT,
	       WHITE_SW_TRAPS)

	    DE(DCC_XHDR_TRAP_DIS,
	       WHITE_SW_TRAP_DIS,
	       WHITE_SW_TRAP_REJ | WHITE_SW_TRAP_NOT
	       | WHITE_SW_DCC_OFF | WHITE_SW_REP_OFF
	       | WHITE_SW_DNSBL_ON_M | WHITE_SW_DISCARD_NO)
	    DE("spam-trap-accept",	/* obsolete */
	       WHITE_SW_TRAP_DIS,
	       WHITE_SW_TRAP_REJ | WHITE_SW_TRAP_NOT
	       | WHITE_SW_DCC_OFF | WHITE_SW_REP_OFF
	       | WHITE_SW_DNSBL_ON_M | WHITE_SW_DISCARD_NO)

	    DE(DCC_XHDR_TRAP_REJ,
	       WHITE_SW_TRAP_REJ,
	       WHITE_SW_TRAP_DIS | WHITE_SW_TRAP_NOT
	       | WHITE_SW_DCC_OFF | WHITE_SW_REP_OFF
	       | WHITE_SW_DNSBL_ON_M)

#	    undef DE
	};

	int i;
	WHITE_FGS conflicts;

	i = strlen(opt);
	for (tp = tbl; ; ++tp) {
		/* try a threshold if not a boolean option */
		if (tp > LAST(tbl))
			return option_thold(emsg, wf, opt,
					    fnm_buf, thold_fnm_buf);
		if (i == tp->len && !strcasecmp(tp->str, opt))
			break;
	}

	conflicts = (wf->wtbl_sws & tp->conflicts);
	if (conflicts != 0) {
		wf->wtbl->hdr.sws = wf->wtbl_sws;
		for (tp1 = tbl; tp1 <= LAST(tbl); ++tp1) {
			if ((tp1->on & conflicts) != 0) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "\"option %s\"%s conflicts with "
					  "\"option %s\"",
					  opt, wf_fnm_lno(fnm_buf, wf),
					  tp1->str);
				return 0;
			}
		}
		dcc_pemsg(EX_DATAERR, emsg,
			  "conflicting \"option %s\"%s",
			  opt, wf_fnm_lno(fnm_buf, wf));
		return 0;
	}

	wf->wtbl_sws |= tp->on;
	wf->wtbl->hdr.sws = wf->wtbl_sws;
	return 1;
}



int					/* -1=fatal 0=problems 1=ok */
parse_whitefile(DCC_EMSG *emsg,
		WF *wf,
		int main_fd,		/* main file */
		DCC_PARSED_CK_FNC add_fnc,
		DCC_PARSED_CK_RANGE_FNC range_fnc)
{
	struct f {
	    int	    fd;
	    char    *start;
	    char    *eob;
	    DCC_PATH path;
	    char    c[1024];
	} main_buf, inc_buf, *cur_buf;
	char tgts_buf[16];
	char *bol, *eol, *type_nm, *ck;
	const char *nmp, *opt;
	DCC_FNM_LNO_BUF fnm_buf;
	DCC_FNM_LNO_BUF white_fnm_buf;
	DCC_FNM_LNO_BUF thold_fnm_buf;
	DCC_TGTS new_tgts;
	DCC_CK_TYPES type;
	struct stat sb;
	int white_fno;
	int main_lno;
	u_char result, hex;
	int i, j;

	result = 1;
	white_fnm_buf.b[0] = '\0';
	thold_fnm_buf.b[0] = '\0';
	main_buf.fd = main_fd;
	main_buf.eob = main_buf.c;
	main_buf.start = main_buf.c;
	cur_buf = &main_buf;
	wf->fno = white_fno = 0;
	wf->lno = main_lno = 0;
	new_tgts = DCC_TGTS_INVALID;
	for (;;) {
next_line:;
		/* Each substantive line has one of the forms:
		 *	tgts	[hex] type	string
		 *		[hex] type	string
		 *	include	pathname
		 *	option ...
		 * A missing number of targets means the line has the
		 * same number of targets as the previous line */

		++wf->lno;
		for (;;) {
			/* continue getting more text until we have an
			 * end-of-line in the buffer */
			if (cur_buf->start < cur_buf->eob) {
				eol = memchr(cur_buf->start, '\n',
					     cur_buf->eob - cur_buf->start);
				if (eol)
					break;
			}

			if (cur_buf->start != cur_buf->c) {
				i = cur_buf->eob - cur_buf->start;
				if (i > 0)
					memmove(cur_buf->c, cur_buf->start, i);
				cur_buf->start = cur_buf->c;
				cur_buf->eob = &cur_buf->c[i];
			}
			j = &cur_buf->c[sizeof(cur_buf->c)] - cur_buf->eob;
			if (j <= 0) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "line too long%s",
					  wf_fnm_lno(&fnm_buf, wf));
				result = 0;
			} else {
				i = read(cur_buf->fd, cur_buf->eob, j);
				if (i > 0) {
					cur_buf->eob += i;
					continue;
				}

				if (i < 0) {
					dcc_pemsg(EX_IOERR, emsg,
						  "read(%s, %d): %s",
						  wf_fnm(wf, wf->fno), j,
						  ERROR_STR());
					result = 0;
				}
				/* act as if the last line in the file ends
				 * with '\n' even if it does not */
				if (cur_buf->start < cur_buf->eob) {
					eol = cur_buf->eob++;
					break;
				}
			}
			if (cur_buf == &main_buf)
				return result;
			if (0 > close(cur_buf->fd)) {
				dcc_pemsg(EX_IOERR, emsg, "close(%s): %s",
					  wf_fnm(wf, wf->fno), ERROR_STR());
				result = 0;
			}
			/* return to the main file at end of included file */
			cur_buf = &main_buf;
			wf->fno = 0;
			wf->lno = main_lno;
			new_tgts = DCC_TGTS_INVALID;
			continue;
		}
		bol = cur_buf->start;
		cur_buf->start = eol+1;

		/* trim trailing blanks */
		do {
			*eol-- = '\0';
		} while (eol > bol
			 && (*eol == ' ' || *eol == '\t' || *eol == '\r'));

		/* Ignore blank lines and lines starting with '#' */
		type_nm = bol+strspn(bol, DCC_WHITESPACE);
		if (*type_nm == '\0' || *type_nm == '#')
			goto next_line;

		/* parse
		 *	include	pathname */
		nmp = ck_word_white(type_nm, "include", LITZ("include"));
		if (nmp) {
			DCC_PATH nm;

			/* can't continue list of identical/missing target
			 * counts into included file */
			new_tgts = DCC_TGTS_INVALID;

			if (cur_buf != &main_buf) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "nested \"include\" not allowed%s",
					  wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}

			/* trim quotes if present from the file name */
			i = strlen(nmp);
			if (i > 1
			    && ((nmp[0] == '"' && nmp[i-1] == '"')
				|| (nmp[0] == '<' &&
				    i > 1 && nmp[i-1] == '>'))) {
				++nmp;
				i -= 2;
			}

			if (i == 0) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "missing file name%s",
					  wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}
			if (i >= ISZ(DCC_PATH)) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "file name too long in"
					  " \"include %.32s...\"%s",
					  nmp, wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}
			memcpy(nm.c, nmp, i);
			nm.c[i] = '\0';
			if (white_fno >= DIM(wf->wtbl->hdr.white_incs)) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "too many \"include\" files%s",
					  wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}

			if (!dcc_fnm2rel(&wf->wtbl->hdr.white_incs[white_fno
							].nm,
					 nm.c, 0)) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "name \"%s\" too long%s",
					  nm.c, wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}

			inc_buf.fd = open(nm.c, O_RDONLY, 0);
			if (inc_buf.fd < 0) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "\"include %s\": %s%s",
					  nm.c, ERROR_STR(),
					  wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}
			inc_buf.eob = inc_buf.c;
			inc_buf.start = inc_buf.c;
			cur_buf = &inc_buf;

			if (0 > fstat(inc_buf.fd, &sb)) {
				wf->wtbl->hdr.white_incs[white_fno].mtime = 0;
			} else {
				wf->wtbl->hdr.white_incs[white_fno
							].mtime = sb.st_mtime;
			}

			wf->fno = ++white_fno;
			main_lno = wf->lno+1;
			wf->lno = 0;
			goto next_line;
		}

		/* honor "option" lines in whiteclnt files */
		opt = ck_word_white(type_nm, "option", LITZ("option"));
		if (opt) {
			/* stop continuation lines when we hit an option */
			new_tgts = DCC_TGTS_INVALID;

			if (!wf->wtbl) {
				dcc_pemsg(EX_DATAERR, emsg, "\"%s\""
					  " not legal in server whitelist%s",
					  type_nm, wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}

			if (!parse_option(emsg, wf, opt, &fnm_buf,
					  &thold_fnm_buf))
				result = 0;
			goto next_line;
		}


		/* Look for the number of targets in a simple line */
		if (type_nm != bol) {
			/* If the line started with white space, the number
			 * of targets is the same as the previous line. */
			*bol = '\0';
		} else {
			type_nm += strcspn(type_nm, DCC_WHITESPACE);
			if (*type_nm == '\0') {
				dcc_pemsg(EX_DATAERR, emsg,
					  "missing type in \"%s\"%s",
					  bol, wf_fnm_lno(&fnm_buf, wf));
				result = 0;
				goto next_line;
			}
			*type_nm++ = '\0';
			/* bol now starts with null-terminated
			 * number of targets, "include", or "log" */
			type_nm += strspn(type_nm, DCC_WHITESPACE);
		}

		ck = type_nm+strcspn(type_nm, DCC_WHITESPACE);

		if (*ck != '\0') {
			/* null terminate the type */
			*ck++ = '\0';
			ck += strspn(ck, DCC_WHITESPACE);
		}

		if (strcasecmp(type_nm, "hex")) {
			hex = 0;
		} else {
			hex = 1;
			type_nm = ck;
			ck = type_nm+strcspn(type_nm, DCC_WHITESPACE);
			if (*ck != '\0') {
				*ck++ = '\0';
				ck += strspn(ck, DCC_WHITESPACE);
			}
		}

		/* parse the target count if it is present instead of blank */
		if (*bol != '\0')
			new_tgts = dcc_str2cnt(bol);
		if (new_tgts == 0 || new_tgts == DCC_TGTS_INVALID) {
			dcc_pemsg(EX_DATAERR, emsg,
				  "missing or invalid # of targets \"%s\"%s",
				  bol, wf_fnm_lno(&fnm_buf, wf));
			new_tgts = DCC_TGTS_INVALID;
			result = 0;
			goto next_line;
		}

		if (*ck == '\0') {
			dcc_pemsg(EX_DATAERR, emsg, "missing value%s",
				  wf_fnm_lno(&fnm_buf, wf));
			new_tgts = DCC_TGTS_INVALID;
			result = 0;
			goto next_line;
		}

		type = dcc_str2type_wf(type_nm, -1);

		if (new_tgts == DCC_TGTS_OK_MX
		    || new_tgts == DCC_TGTS_OK_MXDCC
		    || new_tgts == DCC_TGTS_SUBMIT_CLIENT) {
			if (type != DCC_CK_IP) {
				dcc_pemsg(EX_DATAERR, emsg,
					  new_tgts == DCC_TGTS_SUBMIT_CLIENT
					  ? "client must be an IP address%s"
					  : "MX server must be an IP address%s",
					  wf_fnm_lno(&fnm_buf, wf));
				new_tgts = DCC_TGTS_INVALID;
				result = 0;
				goto next_line;
			}
			if (wf->wf_flags & WF_PER_USER) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "%s illegal in per-user whitelist%s",
					  tgts2str(tgts_buf, sizeof(tgts_buf),
						   new_tgts, 0),
					  wf_fnm_lno(&fnm_buf, wf));
				new_tgts = DCC_TGTS_INVALID;
				result = 0;
				goto next_line;
			}
		}

		/* Look for the type of the checksum, compute the checksum,
		 * and write the checksum to the hash table file.
		 * Write all of its aliases if it is a host name. */
		if (hex) {
			i = dcc_parse_hex_ck(emsg, wf,
					     type_nm, type, ck, new_tgts,
					     add_fnc);
		} else {
			i = dcc_parse_ck(emsg, wf,
					 type_nm, type, ck, new_tgts,
					 add_fnc, range_fnc);
		}
		/* give up on a fatal problem adding a checksum to the file */
		if (i < 0)
			break;
		if (i == 0)
			result = 0;
		else if (new_tgts == DCC_TGTS_OK || new_tgts == DCC_TGTS_OK2)
			wf_fnm_lno(&white_fnm_buf, wf);
	}

	/* fatal problem such as writing to the file */
	if (cur_buf != &main_buf)
		close(cur_buf->fd);
	return -1;
}
