/*
** myadsp.c - written in milano by vesely on 4feb2015
** query for _adsp._domainkeys.example.com and DMARC
*/
/*
* zdkimfilter - Sign outgoing, verify incoming mail messages

Copyright (C) 2015-2024 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 work, by linking or combining it
with software developed by The OpenDKIM Project and its contributors,
containing parts covered by the applicable licence, the licensor or
zdkimfilter grants you additional permission to convey the resulting work.
*/
#include <config.h>
#if !ZDKIMFILTER_DEBUG
#define NDEBUG
#endif
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>
#if defined HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#if defined HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#if defined HAVE_ARPA_NAMESER_H
#include <arpa/nameser.h>
#endif
#if defined HAVE_NETDB_H
#include <netdb.h>
#endif

#include <resolv.h>

#include <idn2.h>

#include "myadsp.h"
#include "util.h"
#if defined TEST_MYADSP
#include <unistd.h> // isatty
#endif
#include <assert.h>

#define NS_BUFFER_SIZE 1536

#if defined TEST_MYADSP && ! defined NO_DNS_QUERY
static const char* explain_h_errno(int my_h_errno)
{
	switch(my_h_errno)
	{
		/* netdb.h: Possible values left in `h_errno'.  */
		case HOST_NOT_FOUND: return "HOST_NOT_FOUND: Authoritative Answer Host not found.";
		case TRY_AGAIN: return "TRY_AGAIN: Non-Authoritative Host not found, or SERVERFAIL.";
		case NO_RECOVERY: return "NO_RECOVERY: Non recoverable errors, FORMERR, REFUSED, NOTIMP.";
		case NO_DATA: return "NO_DATA: Valid name, no data record of requested type.";
		default: return "Unexpected h_errno.";
	}
}
#endif

static int do_txt_query(resolver_state *rs, char *query, size_t len_d, size_t len_sub,
	int (*parse_fn)(char*, void*), void* parse_arg)
/*
* query is a char buffer (1536 long) also used to parse answers,
* len_d is the length of the query string,
* len_sub is the length of the prefix or 0 if no base query is needed,
* parse_fn is a parsing function, and parse_arg its argument. 
*
* Run query and return:
*   >= 0 number of txt records successfully parsed
*  -1  (PRESULT_INT_ERROR) on caller's error
*  -2  (PRESULT_DNS_ERROR) on temporary error (includes SERVFAIL)
*  -3  (PRESULT_DNS_BAD) on bad DNS data or other (transient?) error
*  -4  for NXDOMAIN if len_sub > 0, or just res_query() failed
*/
{
#if defined NO_DNS_QUERY // dummy for zfilter_db
return 0; (void) rs; (void)query, (void)len_d, (void)len_sub, (void)parse_fn, (void)parse_arg;
#else //  real

	assert(rs);
	assert(query);
	assert(len_d);
	assert(parse_fn);

	if (rs == NULL || rs->statep == NULL)
		return PRESULT_DNS_ERROR;

	char *a_query = NULL;
	if (idn2_to_ascii_8z(query, &a_query, 0) != IDN2_OK ||
		strlen(a_query) >= NS_BUFFER_SIZE)
	{
		free(a_query);
		return PRESULT_DNS_BAD;
	}

	strcpy(query, a_query);
	free(a_query);

	union dns_buffer
	{
		unsigned char answer[NS_BUFFER_SIZE];
		HEADER h;
	} buf;
	
	// res_query returns -1 for NXDOMAIN
	unsigned int qtype;
	char *query_cmp = query;
	int rc = res_nquery(rs->statep, query, 1 /* Internet */, qtype = 16 /* TXT */,
		buf.answer, sizeof buf.answer);

	if (rc < 0)
	{
		int orig_h_errno = h_errno;

#if defined TEST_MYADSP
		if (isatty(fileno(stdout)))
			printf("query: %s; error: %s\n", query, explain_h_errno(orig_h_errno));
#endif
		// check the base domain is given
		if (len_sub == 0)
			return -4;

		len_d -= len_sub;
		query_cmp = query + len_sub;

		static const int try_qtype[] =
		{
			1,  // A
			28, // AAAA
			2,  // NS
			15, // MX
			6   // SOA
		};
		for (size_t t = 0; t < sizeof try_qtype/ sizeof try_qtype[0]; ++t)
		{
			rc = res_nquery(rs->statep, query_cmp, 1 /* Internet */, qtype = try_qtype[t],
				buf.answer, sizeof buf.answer);

			int my_h_errno = h_errno;
#if defined TEST_MYADSP
			if (isatty(fileno(stdout)))
				printf("tried %s, qtype=%d, %s\n", query_cmp,
					qtype, rc < 0? explain_h_errno(my_h_errno): "found");
#endif

			/*
			*   For DMARC purposes, a non-existent domain is a domain
			*   for which there is an NXDOMAIN or NODATA response for
			*   A, AAAA, and MX records.  This is a broader definition
			*   than that in NXDOMAIN [RFC8020].
			*/
			if (rc >= 0 ||
				(my_h_errno != NO_DATA && my_h_errno != HOST_NOT_FOUND))
			{
				if (orig_h_errno == NO_DATA || orig_h_errno == HOST_NOT_FOUND)
					return 0;  // domain exists, no TXT record found
				return PRESULT_DNS_ERROR; // orig was TRY_AGAIN or NO_RECOVERYU
			}
		}

		return -4;
	}

#if defined TEST_MYADSP
	if (isatty(fileno(stdout)))
		printf("query: %s\n", query);
#endif

	size_t ancount;
	if (rc < HFIXEDSZ ||
		(unsigned)rc > sizeof buf ||
		ntohs(buf.h.qdcount) != 1 ||
		(ancount = ntohs(buf.h.ancount)) < 1 ||
		buf.h.tc ||
		buf.h.rcode != NOERROR)
			return -3;

	unsigned char *cp = &buf.answer[HFIXEDSZ];
	unsigned char *const eom = &buf.answer[rc];

	// question
	char expand[NS_BUFFER_SIZE];
	int n = dn_expand(buf.answer, eom, cp, expand, sizeof expand); //name
	if (n < 0 || strncasecmp(expand, query_cmp, len_d + 1) != 0)
		return -3;

	cp += n;
	if (cp + 2*INT16SZ > eom ||
		my_get16(cp) != qtype ||
			my_get16(cp + INT16SZ) != 1) // qclass
				return -3;

	if (qtype != 16 /* TXT */) return 0;

	cp += 2*INT16SZ;

	// answers
	int found = 0;
	while (ancount--> 0)
	{
		n = dn_expand(buf.answer, eom, cp, expand, sizeof expand);
		if (n < 0 || cp + n + 3*INT16SZ + INT32SZ > eom)
			return -3;

		uint16_t type = my_get16(cp + n);
		uint16_t class = my_get16(cp + n + INT16SZ);
		uint16_t rdlength = my_get16(cp + n + 2*INT16SZ + INT32SZ); // (skip ttl)

		cp += n + 3*INT16SZ + INT32SZ;
		// not if it was cname... if (strncasecmp(expand, query, len_d) != 0 ||
		if (type != 16 || class != 1)
		{
			cp += rdlength;
			continue;
		}

		char *p = &query[0];  // reuse query to assemble character-strings.
		char *const end = p + NS_BUFFER_SIZE;

		// TXT-DATA consists of one or more <character-string>s.
		// <character-string> is a single length octet followed by that number
		// of characters.  RFC 1035

		while (rdlength > 0 && p < end)
		{
			size_t sl = *(unsigned char*)cp++;
			rdlength -= 1;
			if (p + sl >= end || sl > rdlength)
				break;

			memcpy(p, cp, sl);
			p += sl;
			cp += sl;
			rdlength -= sl;
		}

		if (rdlength == 0 && p < end)
		{
			*p = 0;
#if defined TEST_MYADSP
			if (isatty(fileno(stdout)))
				printf("answer: %s\n", query);
#endif
			int rtc = parse_fn(query, parse_arg);
			if (rtc < 0)
				return -3;

			found += rtc;
		}
	}

	return found;
#endif // NO_DNS_QUERY
}

static int (*txt_query)(resolver_state *rs, char*, size_t, size_t, int (*)(char*, void*), void*) =
	&do_txt_query;

static int parse_adsp(char *record, void *v_policy)
{
	assert(record);

	int *policy = v_policy;
	int found = 0;

	if (strncmp(record, "dkim", 4) == 0)
	{
		char *p = skip_fws(&record[4]);
		if (p && *p == '=')
		{
			p = skip_fws(p + 1);
			if (p)
			{
				found = 1;

				char *q = p;
				int ch;
				while (isalnum(ch = *(unsigned char*)q) || ch == '-')
					++q;

				size_t len = q - p;
				if (policy)
				{
					if (len == 7 && strncmp(p, "unknown", 7) == 0)
						*policy = ADSP_POLICY_UNKNOWN;
					else if (len == 3 && strncmp(p, "all", 3) == 0)
						*policy = ADSP_POLICY_ALL;
					else if (len == 11 && strncmp(p, "discardable", 11) == 0)
						*policy = ADSP_POLICY_DISCARDABLE;
					else
						found = -1;
				}
			}
		}
	}

	return found;
}

static int do_adsp_query(resolver_state *rs, char const *domain, int *policy)
// run query and return:
//   0  (PRESULT_FOUND) and a response if found
//   1  (PRESULT_NOT_FOUND) found, but no adsp retrieved
//   3  (PRESULT_NXDOMAIN) for NXDOMAIN
//  -1  (PRESULT_INT_ERROR) on caller's error
//  -2  (PRESULT_DNS_ERROR) on temporary error (includes SERVFAIL)
//  -3  (PRESULT_DNS_BAD) on bad DNS data or other transient error
{
	if (domain == NULL || *domain == 0)
		return PRESULT_INT_ERROR;

	static char const subdomain[] = "_adsp._domainkey.";
	size_t len_sub = sizeof subdomain - 1;
	size_t len_d = strlen(domain) + len_sub;
	char query[NS_BUFFER_SIZE];

	if (len_d >= sizeof query)
		return PRESULT_INT_ERROR;

	memcpy(query, subdomain, sizeof subdomain);
	strcat(&query[sizeof subdomain - 1], domain);

	int rtc = (*txt_query)(rs, query, len_d, len_sub, parse_adsp, policy);
	return rtc == -4? PRESULT_NXDOMAIN: rtc > 0? PRESULT_FOUND: rtc == 0? PRESULT_NOT_FOUND: rtc;
}

static int do_get_adsp(resolver_state *rs, char const *domain, int* policy)
{
	return do_adsp_query(rs, domain, policy);
}

static int
(*adsp_query)(resolver_state *rs, char const*, int*) = &do_get_adsp;

static int
fake_adsp_query_policyfile(resolver_state *rs, char const *domain, int *policy)
// debug function: reads data from "POLICYFILE" formatted like record
// return values similar to do_adsp_query
{
	char buf[512];
	FILE *fp = fopen("POLICYFILE", "r");
	if (fp == NULL)
		return PRESULT_NXDOMAIN; // Why was -4? this call isn't filtered...

	char *s;
	int rtc = 0;
	while ((s = fgets(buf, sizeof buf, fp)) != NULL)
	{
		if (parse_adsp(buf, policy))
		{
			rtc = 1;
			break;
		}
	}

	fclose(fp);
	return rtc;

	(void)domain;
	(void) rs;
}

static int
fake_txt_query_keyfile(resolver_state *rs, char *query, size_t len_d, size_t len_sub,
	int (*parse_fn)(char*, void*), void* parse_arg)

// debug function: reads data from "KEYFILE" formatted like
//   <label> <SPACE> <txt-record>
{
	char buf[2048];
	char *query_cmp = query + len_sub;
	size_t tail = len_d - len_sub;
	int found = 0, good = 0;
	FILE *fp = fopen("KEYFILE", "r");
	if (fp)
	{
		while (fgets(buf, sizeof buf, fp) != NULL)
		{
			if (strncmp(buf, query, len_d) == 0 && buf[len_d] == ' ')
			{
				++found;
				good += (*parse_fn)(&buf[len_d + 1], parse_arg);
			}
			else if (strncmp(buf, query_cmp, tail) == 0 && buf[tail] == ' ')
				++found;
		}

		fclose(fp);
	}
	return found? good: -4;
	(void) rs;
}

static int fake_txt_query_both(resolver_state *rs, char *query, size_t len_d, size_t len_sub,
	int (*parse_fn)(char*, void*), void* parse_arg)
// allow get_dmarc to be authoritative on nxdomain
{
	int nu, p_rtc = adsp_query == &fake_adsp_query_policyfile?
		fake_adsp_query_policyfile(rs, query, &nu): -4;

	/*
	* This used to give wrong results when reading DMARC and
	* having a POLICYFILE.  Reading the latter yields p_rtc = 1,
	* then rtc == -4 from KEYFILE wrongly set rtc = p_rtc = 1.
	*
	* Now DMARC is not read via this any more.
	*/

	int rtc = fake_txt_query_keyfile(rs, query, len_d, len_sub, parse_fn, parse_arg);
	if (rtc == -4 && parse_fn != parse_adsp)
		rtc = p_rtc;

	return rtc;
}

int set_adsp_query_faked(int mode)
// mode is r(eal), k(eyfile), or p(olicyfile)
// this also affects dmarc.
{
	int const old_mode =
		adsp_query == &fake_adsp_query_policyfile? 'p':
		txt_query == &do_txt_query? 'r': 'k';
	switch (mode)
	{
		case 'p': // test3
			adsp_query = &fake_adsp_query_policyfile;
			txt_query = &fake_txt_query_both;
			break;

		case 'k': // test2
			adsp_query = &do_adsp_query;
			txt_query = &fake_txt_query_keyfile;
			break;

		case 'r':
		default:
			adsp_query = &do_adsp_query;
			txt_query = &do_txt_query;
			break;
	}
	return old_mode;
}

int my_get_adsp(resolver_state *rs, char const *domain, int *policy)
{
	return (*adsp_query)(rs, domain, policy);
}

int parse_dmarc_rec(dmarc_rec *dmarc, char const *rec)
{
	int rc = -1;
	if (dmarc)
	{
		char *r = strdup(rec);
		if (r)
		{
			if (parse_dmarc(r, dmarc))
				rc = 0;
			free(r);
		}
	}
	return rc;
}

static int my_parse_dmarc(char *rec, void *v)
{
	return parse_dmarc(rec, (dmarc_rec*)v);
}

int verify_dmarc_addr(resolver_state *rs, char const *poldo, char const *rcptdo,
	char **override, char **badout)
// override must be given, badout may be NULL.
// run query and return:
//   0  valid, possible overrides and badouts adjusted and no sentinel
//  -1  on caller's error
//  -2  on temporary error (includes SERVFAIL)
//  -3  on bad DNS data or other transient error
//  -4  on NXDOMAIN
//  -5  no DMARC record found
{
	assert(poldo);
	assert(rcptdo);
	assert(override);

	if (poldo == NULL || *poldo == 0 || rcptdo == NULL || *rcptdo == 0)
		return -1;


	size_t const len_poldo = strlen(poldo);
	// static char const subdomain[] =  "._report._dmarc.";
	static size_t const len_sub = 16; // 01234567890123456
	size_t len_d = strlen(rcptdo) + len_sub + len_poldo;
	char query[NS_BUFFER_SIZE];

	if (len_d >= sizeof query)
		return -1;

	dmarc_rec dmarc;
	memset(&dmarc, 0, sizeof dmarc);

	snprintf(query, sizeof query, "%s._report._dmarc.%s", poldo, rcptdo);

	int rtc = (*txt_query)(rs, query, len_d, len_sub, my_parse_dmarc, &dmarc);
	if (rtc >= 1)
	{
		if (dmarc.rua)
		{
			*override = adjust_rua(&dmarc.rua, badout);
			check_remove_sentinel(*override);
		}
		rtc = 0;
	}
	else if (rtc == 0)
		rtc = -5;

	return rtc;
}

int get_dmarc(resolver_state *rs, char const *domain, dmarc_rec *dmarc)
// Since treewalk, this is only called by zaggregate
// run query and return:
//   0  (PRESULT_FOUND) and a response if found
//   1  (PRESULT_NOT_FOUND) found, but no dmarc retrieved
//   3  (PRESULT_NXDOMAIN) for NXDOMAIN
//  -1  (PRESULT_INT_ERROR) on caller's error
//  -2  (PRESULT_DNS_ERROR) on temporary error (includes SERVFAIL)
//  -3  (PRESULT_DNS_BAD) on bad DNS data or other transient error
{
	assert(rs);
	assert(domain);
	assert(dmarc);

	if (domain == NULL || *domain == 0)
		return PRESULT_INT_ERROR;

	// clear it on entry
	memset(dmarc, 0, sizeof *dmarc);

	static char const subdomain[] = "_dmarc.";
	size_t const len_sub = sizeof subdomain - 1;
	size_t len_d = strlen(domain) + len_sub;
	char query[NS_BUFFER_SIZE];

	if (len_d >= sizeof query)
		return PRESULT_INT_ERROR;

	memcpy(query, subdomain, sizeof subdomain);
	strcat(&query[len_sub], domain);

	int rtc = (*txt_query)(rs, query, len_d, len_sub, my_parse_dmarc, dmarc);

	if (rtc != 1)
	{
		free(dmarc->rua);
		memset(dmarc, 0, sizeof *dmarc);
	}

	return rtc == -4? PRESULT_NXDOMAIN: rtc > 0? PRESULT_FOUND: rtc == 0? PRESULT_NOT_FOUND: rtc;
}

char const *presult_explain(int rtc)
{
	if (rtc == PRESULT_FOUND) return "found";
	if (rtc == PRESULT_NOT_FOUND) return "not found";
	if (rtc == PRESULT_NXDOMAIN) return "NXDOMAIN";
	if (rtc == PRESULT_INT_ERROR) return "internal error";
	if (rtc == PRESULT_DNS_ERROR) return "DNS temperror";
	if (rtc == PRESULT_DNS_BAD) return "garbled DNS";
	return "unknown error";
}

#if defined TEST_MYADSP && ! defined NO_DNS_QUERY
#include <errno.h>

static void disp_dmarc(dmarc_rec *dmarc)
{
	char *bad = NULL, *rua = NULL, *rua2 = NULL;
	char *wrec = write_dmarc_rec(dmarc, 0);
	if (dmarc->rua)
	{
		rua2 = strdup(dmarc->rua);
		rua = adjust_rua(&dmarc->rua, &bad);
	}

	printf(
		"rewritten as: \"%s\"\n", wrec? wrec: "");
	if (rua2) printf(
		"rua:          \"%s\"\n"
		"rewritten as: \"%s\"\n"
		"and bad URI:  \"%s\"\n",
			rua2, rua? rua: "", bad? bad: "");
	free(rua);
	free(bad);
	free(wrec);
	free(rua2);
}

int main(int argc, char *argv[])
{
	parm_t parm;
	memset(&parm, 0, sizeof parm);
	parm.verbose = 10;

	resolver_state *rs = init_resolv(&parm, RESOLV_CONF);
	if (rs == NULL)
	{
		fprintf(stderr,
			"cannot init query: %s", strerror(errno));
		return 1;
	}

	if (argc >= 2)
	{
		if (strcmp(argv[1], "--parse") == 0)
		{
			for (int i = 2; i < argc; ++i)
			{
				char *more = strlen(argv[i]) <= 20? "": "...";
				dmarc_rec dmarc;
				memset(&dmarc, 0, sizeof dmarc);
				int rtc = parse_dmarc_rec(&dmarc, argv[i]);
				if (rtc == 0)
				{
					if (i > 2)
						putchar('\n');
					printf(
						"record:       \"%.20s%s\"\n", argv[i], more);
					disp_dmarc(&dmarc);
				}
				else printf("bad record %.20s%s\n", argv[i], more);
			}
		}
		else for (int i = 1; i < argc; ++i)
		{
			int policy = 0;
			char *a = argv[i];
			if (a[0] == '-' && strchr("rkp", a[1]) && a[2] == 0)
			{
				set_adsp_query_faked(a[1]);
				continue;
			}
			dmarc_rec dmarc;
			memset(&dmarc, 0, sizeof dmarc);
			int rtc = get_dmarc(rs, a, &dmarc);
			printf("rtc = %d %s\n", rtc, presult_explain(rtc));
			if (rtc == 0)
				disp_dmarc(&dmarc);
			rtc = my_get_adsp(rs, a, &policy);
			printf("rtc = %d %s, policy = %d\n\n",
				rtc, presult_explain(rtc), policy);
		}
	}
	else
		printf("Usage:\n\t%s domain...\nor\n\t%s --parse dmarc-record...\n",
			argv[0], argv[0]);
	clear_resolv(rs);
	return 0;
}
#endif

