/* Distributed Checksum Clearinghouse
 *
 * client-ID and password parsing
 *
 * Copyright (c) 2005 by Rhyolite Software
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE
 * BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 *
 * Rhyolite Software DCC 1.2.74-1.28 $Revision$
 */

#include "dcc_clnt.h"
#include "dcc_ids.h"
#include "dcc_heap_debug.h"


const char *ids_nm = IDS_NM_DEF;

time_t ids_mtime;


/* authenticated client database
 * assume there will be about 8000 clients and servers known to each server */
#define ID_TBL_LEN 4001			/* must be prime */
#define ID_TBL_MAX (ID_TBL_LEN*4)
#define ID_HASH(id) id_tbl_hash[id % ID_TBL_LEN]
static ID_TBL *id_tbl_hash[ID_TBL_LEN];
static int id_tbl_len = 0;
static ID_TBL *id_tbl_free;


/* find an ID_TBL entry	*/
ID_TBL *
find_id_tbl(DCC_CLNT_ID id)
{
	ID_TBL *tp;

	for (tp = ID_HASH(id); tp != 0; tp = tp->fwd) {
		if (tp->id == id)
			return tp;
	}
	return 0;
}



u_char
parse_dccd_delay(DCC_EMSG emsg, time_t *delay_usp, u_int *delay_inflatep,
		 const char *val,
		 const char *fnm, int lineno)
{
	time_t delay_ms;
	u_long l;
	char *p1, *p2;

	delay_ms = strtoul(val, &p1, 0);

	if (delay_ms > DCC_ANON_DELAY_MAX/1000) {
		dcc_pemsg(EX_DATAERR, emsg, "invalid delay \"%s\"%s", val,
			  fnm_lineno(fnm, lineno));
		return 0;
	} else if (*p1 == '\0') {
		*delay_inflatep = DCC_ANON_INFLATE_OFF;
	} else if (*p1 != ',' && *p1 != '*') {
		dcc_pemsg(EX_DATAERR, emsg, "unrecognized delay \"%s\"%s", val,
			  fnm_lineno(fnm, lineno));
		return 0;
	} else {
		l = strtoul(++p1, &p2, 0);
		if (*p2 != '\0') {
			dcc_pemsg(EX_DATAERR, emsg,
				  "unrecognized delay inflation \"%s\"%s",
				  p1,
				  fnm_lineno(fnm, lineno));
			return 0;
		}
		if (l == 0)
			l = DCC_ANON_INFLATE_OFF;
		*delay_inflatep = l;
	}

	*delay_usp = delay_ms*1000;
	return 1;
}



/* (re)load the client-ID and password database */
int					/* -1=failed, 0=sick file, 1=ok */
load_ids(DCC_EMSG emsg, ID_TBL **tgt_tbl, DCC_CLNT_ID tgt_id)
{
	ID_TBL t, *tp, **tpp;
	FILE *f;
	int lineno, total;
	u_char found_it;
	int status;
	char buf[sizeof(ID_TBL)*2+1];
	const char *bufp;
	char id_buf[30];
	struct stat sb;
	char *p;
	int i;

	if (tgt_tbl)
		*tgt_tbl = 0;

	f = fopen(ids_nm, "r");
	if (!f) {
		dcc_pemsg(EX_NOINPUT, emsg, "fopen(%s): %s",
			  DCC_NM2PATH(ids_nm), ERROR_STR());
		return -1;
	}

	/* the file contains passwords, so refuse to use it if anyone else
	 * can read it */
	if (!dcc_ck_private(emsg, &sb, ids_nm, fileno(f))) {
		fclose(f);
		ids_mtime = 0;
		return -1;
	}

	ids_mtime = sb.st_mtime;

	/* Mark the existing IDs so we can delete them if they've
	 * disappeared from the ASCII file.  This is more complicated
	 * than deleting and recreating the table from scratch, but the
	 * results are better when a bad entry is added to the file. */
	for (i = 0; i < DIM(id_tbl_hash); i++) {
		for (tpp = &id_tbl_hash[i]; (tp = *tpp) != 0; tpp = &tp->fwd)
			tp->flags &= ~ID_FLG_MARKED;
	}

	total = 0;
	lineno = 0;
	status = 1;
	found_it = (tgt_id == DCC_ID_ANON);
	for (;;) {
		/* read and parse a line contain a client-ID and key(s) */
		bufp = fgets(buf, sizeof(buf), f);
		if (!bufp) {
			if (ferror(f))
				dcc_pemsg(EX_IOERR, emsg,
					  "fgets(ID file %s): %s",
					  DCC_NM2PATH(ids_nm), ERROR_STR());
			break;
		}
		++lineno;

		/* Ignore blank lines and lines starting with '#'.
		 * Note that '#' flags a comment only at the start of
		 * the line to avoid dealing with the escaping hassles
		 * of allowing '#' in passwords. */
		bufp += strspn(bufp, DCC_WHITESPACE);
		if (*bufp == '\0' || *bufp == '#')
			continue;

		memset(&t, 0, sizeof(t));
		t.delay_inflate = DCC_ANON_INFLATE_OFF;

		/* Each substantive line has the form:
		 *
		 *	ID[,rpt-ok][,delay=ms] password1 password2
		 *
		 *  Both passwords are always accepted.  They are intended
		 *  to be the previous and current or the current and
		 *  next to allow the password to be changed at both the
		 *  client and the server without loss of service. */

		bufp = dcc_parse_word(emsg, id_buf, sizeof(id_buf),
				      bufp, "ID", ids_nm, lineno);
		if (!bufp) {
			if (status > 0)
				status = 0;
			continue;
		}

		p = strchr(id_buf, ',');
		if (p)
			*p++ = '\0';
		t.id = dcc_get_id(emsg, id_buf, ids_nm, lineno);
		if (t.id == DCC_ID_INVALID) {
			if (status > 0)
				status = 0;
			continue;
		}
		if (t.id == DCC_ID_ANON) {
			dcc_pemsg(EX_DATAERR, emsg, "invalid ID \"%s\"%s",
				  id_buf, fnm_lineno(ids_nm, lineno));
			if (status > 0)
				status = 0;
			continue;
		}

		while (p) {
			char *p1 = strchr(p, ',');
			if (p1)
				*p1++ = '\0';
			if (t.id >= DCC_CLNT_ID_MIN
			    && t.id <= DCC_CLNT_ID_MAX
			    && (!strcasecmp(p, "rpt-ok")
				|| !strcasecmp(p, "rpt_ok"))) {
				t.flags |= ID_FLG_RPT_OK;

			} else if (t.id >= DCC_CLNT_ID_MIN
				   && t.id <= DCC_CLNT_ID_MAX
				   && !CSTRCMP(p, "delay=")) {
				if (!parse_dccd_delay(emsg, &t.delay_us,
						      &t.delay_inflate,
						      p+STRZ("delay="),
						      ids_nm, lineno))
					if (status > 0)
					    status = 0;

			} else if (status > 0) {
				dcc_pemsg(EX_DATAERR, emsg,
					  "invalid option \"%s\"%s", p,
					  fnm_lineno(ids_nm, lineno));
				status = 0;
			}

			p = p1;
		}

		bufp = dcc_parse_word(emsg, t.cur_passwd, sizeof(t.cur_passwd),
				      bufp, "current password", ids_nm, lineno);
		if (!bufp) {
			if (status > 0)
				status = 0;
			continue;
		}
		bufp = dcc_parse_word(emsg,
				      t.next_passwd, sizeof(t.next_passwd),
				      bufp, "next password", ids_nm, lineno);
		if (!bufp) {
			if (status > 0)
				status = 0;
			continue;
		}
		if (*bufp != '\0') {
			dcc_pemsg(EX_DATAERR, emsg,
				  "invalid next password for ID %d%s",
				  t.id, fnm_lineno(ids_nm, lineno));
			if (status > 0)
				status = 0;
			continue;
		}

		/* get rid of the old "unknown" place holding passwords */
		if (!strcmp(t.next_passwd, "unknown"))
			memset(t.next_passwd, 0, sizeof(t.next_passwd));
		if (!strcmp(t.cur_passwd, "unknown")) {
			strncpy(t.cur_passwd, t.next_passwd,
				sizeof(t.cur_passwd));
			memset(t.next_passwd, 0, sizeof(t.next_passwd));
		}

		/* put the entry into the hash table if it is not already
		 * present and update it if it is */
		for (tpp = &ID_HASH(t.id); ; tpp = &tp->fwd) {
			tp = *tpp;
			if (!tp) {
				/* it was not present, so add it */
				if (!id_tbl_free) {
					/* make more entries if necessary */
					i = 16;
					if (id_tbl_len <= ID_TBL_MAX
					    && id_tbl_len+i > ID_TBL_MAX)
					    dcc_error_msg("ID table overflow");
					id_tbl_len += i;
					tp = dcc_malloc(i*sizeof(*tp));
					if (!tp) {
					    dcc_pemsg(EX_OSERR, emsg,
						      "malloc(%d IDs) failed",
						      i);
					    if (status > 0)
						status = 0;
					    goto bad_id;
					}
					do {
					    tp->fwd = id_tbl_free;
					    id_tbl_free = tp;
					} while (++tp, --i > 0);
				}
				/* use the next free entry */
				tp = id_tbl_free;
				id_tbl_free = tp->fwd;
				memset(tp, 0, sizeof(*tp));
				tp->id = t.id;
				*tpp = tp;
				break;
			}

			if (tp->id == t.id) {
				/* If the ID is already present,
				 * the file is bad unless the previous
				 * or current line is a mere marker. */
				if ((tp->flags & ID_FLG_MARKED)
				    && tp->cur_passwd[0] != '\0'
				    && t.cur_passwd[0] != '\0') {
					dcc_pemsg(EX_DATAERR, emsg,
						  "duplicate ID %d%s",
						  t.id,
						  fnm_lineno(ids_nm, lineno));
					if (status > 0)
					    status = 0;
				}
				break;
			}
		}

		if (tp->flags & ID_FLG_MARKED) {
			if (t.flags & ID_FLG_RPT_OK)
				tp->flags |= ID_FLG_RPT_OK;
			if (t.delay_us != 0) {
				tp->delay_us = t.delay_us;
				tp->delay_inflate = t.delay_inflate;
			}
		} else {
			tp->flags |= ID_FLG_MARKED;
			tp->flags &= ~ID_FLG_RPT_OK;
			if (t.flags & ID_FLG_RPT_OK)
				tp->flags |= ID_FLG_RPT_OK;
			tp->delay_us = t.delay_us;
			tp->delay_inflate = t.delay_inflate;
		}

		if (t.cur_passwd[0] != '\0') {
			++total;
			strncpy(tp->cur_passwd, t.cur_passwd,
				sizeof(tp->cur_passwd));
			strncpy(tp->next_passwd, t.next_passwd,
				sizeof(tp->next_passwd));
			/* remember special password */
			if (tp->id == tgt_id
			    && t.cur_passwd[0] != '\0') {
				found_it = 1;
				if (tgt_tbl)
					*tgt_tbl = tp;
			}
		}
bad_id:;
	}
	fclose(f);

	if (status > 0 && !total) {
		dcc_pemsg(EX_DATAERR, emsg, "%s contains no passwords",
			  DCC_NM2PATH(ids_nm));
		status = 0;
	}
	if (!found_it) {
		dcc_pemsg(EX_DATAERR, emsg,
			  "%s does not contain the password for ID %d",
			  DCC_NM2PATH(ids_nm), tgt_id);
		status = -1;
	}

	/* If things are ok so far, forget old entries.
	 * This ensures that we do not forget the server
	 * password when we are given a sick database */
	if (status > 0) {
		for (i = 0; i < DIM(id_tbl_hash); i++) {
			tpp = &id_tbl_hash[i];
			while ((tp = *tpp) != 0) {
				/* skip entries seen in the file */
				if (tp->flags & ID_FLG_MARKED) {
					tpp = &tp->fwd;
					continue;
				}
				if (tp->id == tgt_id) {
					dcc_pemsg(EX_DATAERR, emsg,
						  "missing server passwd in %s",
						  DCC_NM2PATH(ids_nm));
					tpp = &tp->fwd;
					status = 0;
				} else {
					/* delete other entries */
					*tpp = tp->fwd;
					memset(tp, 0, sizeof(*tp));
					tp->fwd = id_tbl_free;
					id_tbl_free = tp;
				}
			}
		}
	}

	return status;
}



/* load the ids file if it has changed */
u_char					/* 0=failed, 1=reloaded, 2=up to date */
check_load_ids(DCC_EMSG emsg, DCC_CLNT_ID tgt_id)
{
	struct stat sb;

	if (!dcc_ck_private(emsg, &sb, ids_nm, -1)) {
		ids_mtime = 0;
		return 0;
	}

	if (ids_mtime == sb.st_mtime)
		return 2;

	return load_ids(emsg, 0, tgt_id) > 0;
}
