/**************************************************************************************************
	$Id: check.c,v 1.36 2005/05/04 16:49:59 bboy Exp $

	check.c: Check for problems with the data in the database.

	Copyright (C) 2002-2005  Don Moore <bboy@bboy.net>

	This program 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 2 of the License, or
	(at Your option) any later version.

	This program 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
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
**************************************************************************************************/

#include "util.h"

MYDNS_SOA	*soa;													/* Current SOA record being scanned */
MYDNS_RR		*rr;													/* Current RR record */
char	name[DNS_MAXNAMELEN*2];									/* Current expanded name */
char	data[DNS_MAXNAMELEN*2];									/* Current expanded data */
int	opt_consistency = 0;										/* Consistency check? */
int	opt_consistency_only = 0;								/* Consistency check only? */
int	ignore_minimum = 0;										/* Ignore minimum TTL? */

#ifdef EXTENDED_CHECK_WRITTEN
int	opt_extended_check = 0;									/* Extended check? */
#endif

int	syntax_errors, consistency_errors;					/* Number of errors found */

#define EXPAND_DATA(str) \
			if (!(str)[0] || LASTCHAR((str)) != '.') \
			{ \
				if ((str)[0]) strncat((str), ".", sizeof((str))-strlen((str))-1); \
				strncat((str), soa->origin, sizeof((str))-strlen((str))-1); \
			}


/**************************************************************************************************
	USAGE
	Display program usage information.
**************************************************************************************************/
static void
usage(int status)
{
	if (status != EXIT_SUCCESS)
	{
		fprintf(stderr, _("Try `%s --help' for more information."), progname);
		fputs("\n", stderr);
	}
	else
	{
		printf(_("Usage: %s [ZONE..]"), progname);
		puts("");
		puts(_("Check zone(s) or entire database for errors and consistency."));
		puts("");
/*		puts("----------------------------------------------------------------------------78");  */
		puts(_("  -c, --consistency       do key consistency checks"));
		puts(_("  -C, --consistency-only  do only the key consistency checks"));
#ifdef EXTENDED_CHECK_WRITTEN
		puts(_("  -x, --extended          extended check for data/name references"));
#endif
		puts("");
		puts(_("  -D, --database=DB       database name to use"));
		puts(_("  -h, --host=HOST         connect to SQL server at HOST"));
		puts(_("  -p, --password=PASS     password for SQL server (or prompt from tty)"));
		puts(_("  -u, --user=USER         username for SQL server if not current user"));
		puts("");
#if DEBUG_ENABLED
		puts(_("  -d, --debug             enable debug output"));
#endif
		puts(_("  -v, --verbose           be more verbose while running"));
		puts(_("      --help              display this help and exit"));
		puts(_("      --version           output version information and exit"));
		puts("");
		printf(_("Report bugs to <%s>.\n"), PACKAGE_BUGREPORT);
	}
	exit(status);
}
/*--- usage() -----------------------------------------------------------------------------------*/


/**************************************************************************************************
	CMDLINE
	Process command line options.
**************************************************************************************************/
static void
cmdline(int argc, char **argv)
{
	char	*optstr;
	int	optc, optindex;
	struct option const longopts[] =
	{
		{"consistency-only",	no_argument,			NULL,	'C'},
		{"consistency",		no_argument,			NULL,	'c'},
#ifdef EXTENDED_CHECK_WRITTEN
		{"extended",			no_argument,			NULL,	'x'},
#endif

		{"database",			required_argument,	NULL,	'D'},
		{"host",					required_argument,	NULL,	'h'},
		{"password",			optional_argument,	NULL,	'p'},
		{"user",					required_argument,	NULL,	'u'},

		{"debug",				no_argument,			NULL,	'd'},
		{"verbose",				no_argument,			NULL,	'v'},
		{"help",					no_argument,			NULL,	0},
		{"version",				no_argument,			NULL,	0},

		{NULL, 0, NULL, 0}
	};

	err_file = stdout;
	error_init(argv[0], LOG_USER);							/* Init output routines */
	optstr = getoptstr(longopts);
	while ((optc = getopt_long(argc, argv, optstr, longopts, &optindex)) != -1)
	{
		switch (optc)
		{
			case 0:
				{
					const char *opt = longopts[optindex].name;

					if (!strcmp(opt, "version"))									/* --version */
					{
						printf("%s ("PACKAGE_NAME") "PACKAGE_VERSION" ("SQL_VERSION_STR")\n", progname);
						puts("\n" PACKAGE_COPYRIGHT);
						puts(_("This is free software; see the source for copying conditions.  There is NO"));
						puts(_("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."));
						exit(EXIT_SUCCESS);
					}
					else if (!strcmp(opt, "help"))								/* --help */
						usage(EXIT_SUCCESS);
				}
				break;
			case 'C':																	/* -C, --consistency-only */
				opt_consistency = opt_consistency_only = 1;
				break;
			case 'c':																	/* -c, --consistency */
				opt_consistency = 1;
				break;
			case 'd':																	/* -d, --debug */
#if DEBUG_ENABLED
				err_verbose = err_debug = 1;
#endif
				break;
			case 'D':																	/* -D, --database=DB */
				conf_set(&Conf, "database", optarg, 0);
				break;
			case 'h':																	/* -h, --host=HOST */
				conf_set(&Conf, "db-host", optarg, 0);
				break;
			case 'p':																	/* -p, --password=PASS */
				if (optarg)
				{
					conf_set(&Conf, "db-password", optarg, 0);
					memset(optarg, 'X', strlen(optarg));
				}
				else
					conf_set(&Conf, "db-password", passinput(_("Enter password")), 0);
				break;
			case 'u':																	/* -u, --user=USER */
				conf_set(&Conf, "db-user", optarg, 0);
				break;

			case 'v':																	/* -v, --verbose */
				err_verbose = 1;
				break;
#ifdef EXTENDED_CHECK_WRITTEN
			case 'x':																	/* -x, --extended */
				opt_extended_check = 1;
				break;
#endif
			default:
				usage(EXIT_FAILURE);
		}
	}
}
/*--- cmdline() ---------------------------------------------------------------------------------*/


/**************************************************************************************************
	RRPROBLEM
	Output a string describing a problem found.
**************************************************************************************************/
static void rrproblem(const char *fmt, ...) __printflike(1,2);
static void
rrproblem(const char *fmt, ...)
{
	va_list ap;

	meter(0,0);

	va_start(ap, fmt);
	vprintf(fmt, ap);																					/* 1. message */
	va_end(ap);
	printf("\t");

	if (soa)																								/* 2. soa id */
		printf("%u\t", soa->id);
	else
		printf("-\t");

	if (rr)																								/* 3. rr id */
		printf("%u\t", rr->id);
	else
		printf("-\t");

	printf("%s\t", *name ? name : "-");															/* 4. name */

	if (soa || rr)																						/* 5. ttl */
		printf("%u\t", rr ? rr->ttl : soa->ttl);
	else
		printf("-\t");

	printf("%s\t", rr ? mydns_qtype_str(rr->type) : "-");									/* 6. rr type */
	printf("%s\n", *data ? data : "-");															/* 7. data */

	fflush(stdout);

	syntax_errors++;
}
/*--- rrproblem() -------------------------------------------------------------------------------*/


#ifdef EXTENDED_CHECK_WRITTEN
/**************************************************************************************************
	CHECK_NAME_EXTENDED
**************************************************************************************************/
static void
check_name_extended(const char *name_in, const char *fqdn, const char *col)
{
	/* XXX: Add check to detect names that we should be authoritative for but
		that do not have records */
}
/*--- check_name_extended() ---------------------------------------------------------------------*/
#endif


/**************************************************************************************************
	SHORTNAME
	Removes the origin from a name if it is present.
**************************************************************************************************/
static char *
shortname(char *name_to_shorten, int empty_name_is_ok)
{
	size_t nlen = strlen(name_to_shorten), olen = strlen(soa->origin);

	if (nlen < olen)
		return (name_to_shorten);
	if (!strcasecmp(soa->origin, name_to_shorten))
	{
		if (empty_name_is_ok)
			return ("");
		else
			return (name_to_shorten);
	}
	if (!strcasecmp(name_to_shorten + nlen - olen, soa->origin))
		name[nlen - olen - 1] = '\0';
	return (name_to_shorten);
}
/*--- shortname() -------------------------------------------------------------------------------*/


/**************************************************************************************************
	CHECK_NAME
	Verifies that "name" is a valid name.
**************************************************************************************************/
static void
check_name(const char *name_in, const char *col, int is_rr)
{
	char buf[DNS_MAXNAMELEN * 2], *b, *label;
	char fqdn[DNS_MAXNAMELEN * 2];

	strncpy(fqdn, name_in, sizeof(fqdn)-1);

	/* If last character isn't '.', append the origin */
	if (is_rr && LASTCHAR(fqdn) != '.')
		strncat(fqdn, soa->origin, sizeof(fqdn) - strlen(fqdn) - 1);

	if (!strlen(fqdn))
		return rrproblem(_("FQDN in `%s' is empty"), col);

	if (strlen(fqdn) > DNS_MAXNAMELEN)
		return rrproblem(_("FQDN in `%s' is too long"), col);

	/* Break into labels, verifying each */
	if (strcmp(fqdn, "."))
	{
		strncpy(buf, fqdn, sizeof(buf)-1);
		for (b = buf; (label = strsep(&b, ".")); )
		{
			register int len = strlen(label);
			register char *cp;

			if (!b)		/* Last label - should be the empty string */
			{
				if (strlen(label))
					rrproblem(_("Last label in `%s' not the root zone"), col);
				break;
			}
			if (strcmp(label, "*"))
			{
				if (len > DNS_MAXLABELLEN)
					rrproblem(_("Label in `%s' is too long"), col);
				if (len < 1)
					rrproblem(_("Blank label in `%s'"), col);
				for (cp = label; *cp; cp++)
				{
					if (*cp == '-' && cp == label)
						rrproblem(_("Label in `%s' begins with a hyphen"), col);
					if (*cp == '-' && ((cp - label) == len-1))
						rrproblem(_("Label in `%s' ends with a hyphen"), col);
					if (!isalnum((int)(*cp)) && *cp != '-')
					{
						if (is_rr && *cp == '*')
							rrproblem(_("Wildcard character `%c' in `%s' not alone"), *cp, col);
						else
							rrproblem(_("Label in `%s' contains illegal character `%c'"), col, *cp);
					}
				}
			}
			else if (!is_rr)
				rrproblem(_("Wildcard not allowed in `%s'"), col);
		}
	}

#ifdef EXTENDED_CHECK_WRITTEN
	/* If extended check, do extended check */
	if (is_rr && opt_extended_check)
		check_name_extended(name_in, fqdn, col);
#endif
}
/*--- check_name() ------------------------------------------------------------------------------*/


/**************************************************************************************************
	CHECK_SOA
	Perform SOA check for this zone and return the SOA record.
	Checks currently performed:
		- Make sure "ns" and "mbox" are present and valid.
		- Make sure none of the numeric values are unreasonable (like 0)
**************************************************************************************************/
static MYDNS_SOA *
check_soa(const char *zone)
{
	if (mydns_soa_load(sql, &soa, (char *)zone) != 0)
		Errx("%s: %s", zone, _("error loading SOA record for zone"));
	if (!soa)
		Errx("%s: %s", zone, _("zone not found"));
	rr = NULL;
	*name = *data = '\0';

	/* SOA validation */
	strncpy(name, soa->origin, sizeof(name)-1);
	check_name(soa->ns, "soa.ns", 0);
	check_name(soa->mbox, "soa.mbox", 0);

	if (LASTCHAR(name) != '.')
		rrproblem(_("soa.origin is not a FQDN (no trailing dot)"));

	if (soa->refresh < 300) rrproblem(_("soa.refresh is less than 300 seconds"));
	if (soa->retry < 300) rrproblem(_("soa.retry is less than 300 seconds"));
	if (soa->expire < 300) rrproblem(_("soa.expire is less than 300 seconds"));
	if (soa->minimum < 300) rrproblem(_("soa.minimum is less than 300 seconds"));
	if (soa->ttl < 300) rrproblem(_("soa.ttl is less than 300 seconds"));
	if (soa->minimum < 300) rrproblem(_("soa.minimum is less than 300 seconds"));

	return (soa);
}
/*--- check_soa() -------------------------------------------------------------------------------*/


/**************************************************************************************************
	CHECK_RR_CNAME
	Expanded check for CNAME resource record.
**************************************************************************************************/
static void
check_rr_cname(void)
{
	unsigned char *xname;
	int found = 0;

	EXPAND_DATA(data);
	check_name(data, "rr.data", 1);

	/* A CNAME record can't have any other type of RR data for the same name */
	if (!(xname = calloc(strlen(name) * 2 + 1, sizeof(unsigned char))))
		Err(_("out of memory"));
	sql_escstr(sql, xname, (unsigned char *)name, strlen(name));
	found = sql_count(sql, "SELECT COUNT(*) FROM %s WHERE zone=%u AND name='%s' AND type != 'CNAME'",
								  mydns_rr_table_name, rr->zone, xname);

	/* If not found that way, check short name */
	if (!found)
	{
		Free(xname);
		shortname(name, 1);
		if (!(xname = calloc(strlen(name) * 2 + 1, sizeof(unsigned char))))
			Err(_("out of memory"));
		sql_escstr(sql, xname, (unsigned char *)name, strlen(name));
		found = sql_count(sql, "SELECT COUNT(*) FROM %s WHERE zone=%u AND name='%s' AND type != 'CNAME'",
									  mydns_rr_table_name, rr->zone, xname);
		EXPAND_DATA(name);
	}

	if (found)
		rrproblem(_("non-CNAME record(s) present alongside CNAME"));
	Free(xname);
}
/*--- check_rr_cname() --------------------------------------------------------------------------*/


/**************************************************************************************************
	CHECK_RR_HINFO
	Expanded check for HINFO resource record.
**************************************************************************************************/
static void
check_rr_hinfo(void)
{
	char	os[DNS_MAXNAMELEN + 1] = "", cpu[DNS_MAXNAMELEN + 1] = "";

	if (hinfo_parse(rr->data, cpu, os, DNS_MAXNAMELEN) < 0)
		rrproblem(_("data too long in HINFO record"));
}
/*--- check_rr_hinfo() --------------------------------------------------------------------------*/


/**************************************************************************************************
	CHECK_RR_NAPTR
	Expanded check for NAPTR resource record.
**************************************************************************************************/
static void
check_rr_naptr(void)
{
	char tmp[DNS_MAXNAMELEN * 2 + 2], data_copy[DNS_MAXNAMELEN * 2 + 2], *p;

	strncpy(data_copy, rr->data, sizeof(data_copy) - 1);
	p = data_copy;

	if (!strsep_quotes(&p, tmp, sizeof(tmp)))
		return rrproblem(_("'order' field missing from NAPTR record"));

	if (!strsep_quotes(&p, tmp, sizeof(tmp)))
		return rrproblem(_("'preference' field missing from NAPTR record"));

	if (!strsep_quotes(&p, tmp, sizeof(tmp)))
		return rrproblem(_("'flags' field missing from NAPTR record"));

	if (!strsep_quotes(&p, tmp, sizeof(tmp)))
		return rrproblem(_("'service' field missing from NAPTR record"));

	if (!strsep_quotes(&p, tmp, sizeof(tmp)))
		return rrproblem(_("'regexp' field missing from NAPTR record"));

	if (!strsep_quotes(&p, tmp, sizeof(tmp)))
		return rrproblem(_("'replacement' field missing from NAPTR record"));

	/* For now, don't check 'replacement'.. the example in the RFC even contains illegal chars */
	/* EXPAND_DATA(tmp); */
	/* check_name(tmp, "replacement", 1); */
}
/*--- check_rr_naptr() --------------------------------------------------------------------------*/


/**************************************************************************************************
	CHECK_RR
	Check an individual resource record.
**************************************************************************************************/
static void
check_rr(void)
{
	/* Expand RR's name into `name' */
	strncpy(name, rr->name, sizeof(name)-1);
	strncpy(data, rr->data, sizeof(data)-1);
	EXPAND_DATA(name);
	check_name(name, "rr.name", 1);

	if (!ignore_minimum && (rr->ttl < soa->minimum))
		rrproblem(_("TTL below zone minimum"));

	switch (rr->type)
	{
		case DNS_QTYPE_A:											/* Data: IPv4 address */
			{
				struct in_addr addr;
#if ALIAS_ENABLED
				if (rr->alias == 1)
					check_rr_cname();
				else
				{
#endif /* ALIAS_ENABLED */
					if (inet_pton(AF_INET, data, (void *)&addr) <= 0)
						rrproblem(_("IPv4 address in `data' is invalid"));
#if ALIAS_ENABLED
				}
#endif /* ALIAS_ENABLED */
			}
			break;

		case DNS_QTYPE_AAAA:										/* Data: IPv6 address */
			{
				uint8_t addr[16];
				if (inet_pton(AF_INET6, data, (void *)&addr) <= 0)
					rrproblem(_("IPv6 address in `data' is invalid"));
			}
			break;

		case DNS_QTYPE_CNAME:									/* Data: Name */
			check_rr_cname();
			break;

		case DNS_QTYPE_HINFO:									/* Data: Host info */
			check_rr_hinfo();
			break;

		case DNS_QTYPE_MX:										/* Data: Name */
			EXPAND_DATA(data);
			check_name(data, "rr.data", 1);
			break;

		case DNS_QTYPE_NAPTR:									/* Data: Multiple fields */
			check_rr_naptr();
			break;

		case DNS_QTYPE_NS:										/* Data: Name */
			EXPAND_DATA(data);
			check_name(data, "rr.data", 1);
			break;

		case DNS_QTYPE_PTR:										/* Data: PTR */
			/* TODO */
			break;

		case DNS_QTYPE_RP:										/* Data: Responsible person */
			{
				char	txt[DNS_MAXNAMELEN*2];

				strncpy(txt, rr->rp_txt, sizeof(txt)-1);
				EXPAND_DATA(txt);
				check_name(data, "rr.data (mbox)", 1);
				check_name(txt, "rr.data (txt)", 1);
			}
			break;

		case DNS_QTYPE_SRV:										/* Data: Server location */
			/* TODO */
			break;

		case DNS_QTYPE_TXT:										/* Data: Undefined text string */
			/* Can be anything, so consider it always OK */
			break;

		default:
			rrproblem(_("Unknown/unsupported resource record type"));
			break;
	}
}
/*--- check_rr() --------------------------------------------------------------------------------*/

                                                                                                                               

/**************************************************************************************************
	CHECK_ZONE
	Checks each RR in the current zone through check_rr.
**************************************************************************************************/
static void
check_zone(void)
{
	char query[BUFSIZ];
	size_t querylen;
	unsigned int rrct = 0;
	SQL_RES *res;
	SQL_ROW row;

	querylen = snprintf(query, sizeof(query), "SELECT "MYDNS_RR_FIELDS" FROM %s WHERE zone=%u",
		mydns_rr_table_name, soa->id);
	if (!(res = sql_query(sql, query, querylen)))
		return;
	while ((row = sql_getrow(res)))
	{
		if (!(rr = mydns_rr_parse(row, soa->origin)))
			continue;
		check_rr();
		mydns_rr_free(rr);
		rrct++;
   }
	sql_free(res);

	if (err_verbose)
	{
		meter(0, 0);
		Verbose("%s: %u %s", soa->origin, rrct, rrct == 1 ? _("resource record") : _("resource records"));
	}
}
/*--- check_zone() ------------------------------------------------------------------------------*/


/**************************************************************************************************
	CONSISTENCY_RR_ZONE
	Makes sure rr.zone matches a soa.id.
**************************************************************************************************/
static void
consistency_rr_zone(void)
{
	char query[BUFSIZ];
	size_t querylen;
	SQL_RES *res;
	SQL_ROW row;

	querylen = snprintf(query, sizeof(query),
      "SELECT %s.id,%s.zone FROM %s LEFT JOIN %s ON %s.zone=%s.id WHERE %s.id IS NULL",
		mydns_rr_table_name, mydns_rr_table_name, mydns_rr_table_name, mydns_soa_table_name,
		mydns_rr_table_name, mydns_soa_table_name, mydns_soa_table_name);
	if (!(res = sql_query(sql, query, querylen)))
		return;
	while ((row = sql_getrow(res)))
	{
		char msg[80];

		meter(0,0);
		snprintf(msg, sizeof(msg),
					_("%s id %s references invalid %s id %s"),
					mydns_rr_table_name, row[0], mydns_soa_table_name, row[1]);
		printf("%s\t-\t%s\t-\t-\t-\t-\t-\n", msg, row[0]);
		fflush(stdout);

		consistency_errors++;
	}
	sql_free(res);
}
/*--- consistency_rr_zone() ---------------------------------------------------------------------*/


/**************************************************************************************************
	CONSISTENCY_CHECK
	Does a general database consistency check - makes sure all keys are kosher.
**************************************************************************************************/
static void
consistency_check(void)
{
	consistency_rr_zone();
}
/*--- consistency_check() -----------------------------------------------------------------------*/


/**************************************************************************************************
	MAIN
**************************************************************************************************/
int
main(int argc, char **argv)
{
	setlocale(LC_ALL, "");										/* Internationalization */
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
	cmdline(argc, argv);
	load_config();
	ignore_minimum = GETBOOL(conf_get(&Conf, "ignore-minimum", NULL));
	db_connect();

	if (!opt_consistency_only)
	{
		if (optind >= argc)											/* Check all zones */
		{
			char query[BUFSIZ];
			size_t querylen;
			SQL_RES *res;
			SQL_ROW row;
			unsigned long current = 0, total;

			querylen = snprintf(query, sizeof(query), "SELECT origin FROM %s", mydns_soa_table_name);
			if ((res = sql_query(sql, query, querylen)))
			{
				total = sql_num_rows(res);
				while ((row = sql_getrow(res)))
				{
					meter(current++, total);
					if ((soa = check_soa(row[0])))
					{
						check_zone();
						mydns_soa_free(soa);
					}
				}
				sql_free(res);
			}
		}
		else while (optind < argc)									/* Check zones provided as args */
		{
			char zone[DNS_MAXNAMELEN+2];
			strncpy(zone, argv[optind++], sizeof(zone)-2);
			if (LASTCHAR(zone) != '.')
				strcat(zone, ".");
			if ((soa = check_soa(zone)))
			{
				check_zone();
				mydns_soa_free(soa);
			}
		}
	}

	if (opt_consistency)
		consistency_check();											/* Do consistency check if requested */

	meter(0, 0);
	if (!syntax_errors && !consistency_errors)
		Verbose(_("No errors"));
	else
	{
		if (opt_consistency_only)
			Verbose("%s: %d", _("Consistency errors"), consistency_errors);
		else if (opt_consistency)
			Verbose("%s: %d  %s: %d",
					 _("Syntax errors"), syntax_errors, _("Consistency errors"), consistency_errors);
		else
			Verbose("%s: %d", _("Syntax errors"), syntax_errors);
	}

	return (0);
}
/*--- main() ------------------------------------------------------------------------------------*/

/* vi:set ts=3: */
/* NEED_PO */


syntax highlighted by Code2HTML, v. 0.9.1