/*
 * Copyright (c) 2002-2005 Sendmail, Inc. and its suppliers.
 *      All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 */

#include "sm/generic.h"
SM_RCSID("@(#)$Id: dnsdecode.c,v 1.43 2007/11/05 05:47:53 ca Exp $")
#include "sm/ctype.h"
#include "sm/cstr.h"
#include "sm/dns.h"
#include "sm/dns-int.h"
#include "nameok.h"
#if MTA_LIBDNS_DEBUG
#include "sm/io.h"
#endif

/*
**  DNS reply structure; used for communication between the various
**	decoding functions.
*/

typedef struct dns_reply_S	dns_reply_T, *dns_reply_P;

struct dns_reply_S
{
	sm_str_P	 dnsrpl_answer;	/* answer data (reference only) */
	ushort		 dnsrpl_qry_cnt;	/* # of queries */
	ushort		 dnsrpl_ans_cnt;	/* # of answers */
	uint16_t	 dnsrpl_id;		/* id of reply */
	HEADER		*dnsrpl_header;
	uchar		*dnsrpl_cur;		/* current place in answer */
	uchar		*dnsrpl_end;		/* first char past reply */
};

/*
**  DNS_RES_ADD -- add DNS result (may remove CNAME)
**
**	Parameters:
**		dns_res -- DNS result header (this points to a list of entries
**			to which the current DNS result is added)
**		rrname -- name of query
**		rrlen -- len of rrname
**		host -- name of host
**		len -- len of host
**		ttl -- TTL
**		pref -- preference
**		type -- DNS query type
**		ipv4 -- IPv4 address
**
**	Returns:
**		usual sm_error code
*/

static sm_ret_T
dns_rr_add(dns_res_P dns_res, uchar *rrname, uint rrlen, uchar *host, uint len, uint ttl, ushort pref, dns_type_T type, ipv4_T ipv4)
{
	sm_ret_T ret;
	uint i;
	bool replaced;
	dns_rese_P dns_rese, dns_reseh;

	if (dns_res->dnsres_entries >= dns_res->dnsres_maxentries)
		return sm_error_temp(SM_EM_DNS, SM_E_FULL);

	/*
	**  Look for CNAMEs that can be eliminated:
	**  Q	T_CNAME	R0
	**  R1	T_x	A
	**  If R0 == R1: get rid of T_CNAME entry and keep just A
	**  Note: this means dnsrese_name does NOT match the original query!
	**  Should this be "merged"?
	**
	**  What about larger chains?
	**  Q	T_CNAME	R0
	**  R1	T_CNAME	R2
	**  R3	T_x	A
	**  R0 == R1 && R2 == R3?
	*/

	replaced = false;
	dns_reseh = NULL;
	if (dns_res->dnsres_entries > 0 && T_CNAME == type && host != NULL) {
		/*
		**  Case 1: Maybe add CNAME.
		**  Does R exist as dnsrese_val?
		**  Check whether some other entry has the name for this
		*/

		for (dns_rese = DRESL_FIRST(dns_res), i = 0;
		     dns_rese != DRESL_END(dns_res) && i < dns_res->dnsres_entries;
		     dns_rese = DRESL_NEXT(dns_rese), i++)
		{
			if (dns_rese->dnsrese_type == dns_res->dnsres_qtype &&
#if 0
				/* T_A? not yet... */
			    T_A == dns_rese->dnsrese_type &&
			      dns_rese->dnsrese_val.dnsresu_a == ipv4
#endif /* 0 */
			      strcasecmp((const char *) host,
			         (const char *) sm_cstr_data(dns_rese->dnsrese_name)) == 0)
			{
				MTA_LIBDNS_DBG_DPRINTF((smioerr,
					"dns_rr_add: CNAME=%s, found=%C\n",
					host, dns_rese->dnsrese_name));
				/* found duplicate */
				return SM_SUCCESS;
			}
		}
	}
	if (dns_res->dnsres_entries > 0) {
		/*
		**  Case 2: Add new RR.
		**  Check whether there is a CNAME entry for this.
		**  If yes: remove it and add only this.
		**  Which entry should be removed??
		**  See note above; it would be best to "collapse"
		**  Q	T_CNAME	R
		**  R	T_x	A
		**  to
		**  Q	T_x	A
		*/

		for (dns_rese = DRESL_FIRST(dns_res), i = 0;
		     dns_rese != DRESL_END(dns_res) && i < dns_res->dnsres_entries;
		     dns_rese = DRESL_NEXT(dns_rese), i++)
		{
			if (T_CNAME == dns_rese->dnsrese_type &&
			    strcasecmp((const char *) rrname,
			     (const char *) sm_cstr_data(dns_rese->dnsrese_val.dnsresu_name)
			     ) == 0)
			{
				/*
				**  Found CNAME: remove it.
				**  Optimization: reuse this entry instead of
				**  calling free and new.
				**  Note: the subsequent new may fail and
				**  then we "lost" a valid entry; we could
				**  mark this entry for deletion and do it
				**  after the new succeeded.
				*/

				MTA_LIBDNS_DBG_DPRINTF((smioerr,
					"dns_rr_add: host=%s, ipv4=%A, CNAME=%C\n"
					, host, (ipv4_T) htonl(ipv4)
					, dns_rese->dnsrese_name));

				DRESL_REMOVE(dns_res, dns_rese);
				dns_res->dnsres_entries--;
				dns_reseh = dns_rese;
				replaced = true;
				break;
			}
		}
	}

#if 0
	/*
	**  Check whether this RR name matches the original query.
	**  Note: it doesn't need to match if it's an expanded CNAME.
	**  Need to pass in: query and qlen?
	**  dns_res->dnsres_query is NOT valid here!
	**  Question: is this really necessary? A rogue DNS server
	**  can give us any data it likes, so why care?
	*/

	if (!replaced &&
	    (rrlen != qlen || !sm_memcaseeq(query, rrname, rrlen)))
	{
		sm_io_fprintf(smioerr,
			"dns_rr_add: query=%C, got=%C, BOGUS=IGNORED\n",
			dns_res->dnsres_query, dns_rese->dnsrese_name);
	}
#endif /* 0 */

	ret = dns_rese_new(rrname, rrlen, type, ttl, pref, 0, host, len, ipv4,
			&dns_rese);
	if (sm_is_err(ret))
		goto error;
	if (dns_reseh != NULL && replaced) {
		/*
		**  Replace the name of the query R1 with Q.
		**  Q	T_CNAME	R0
		**  R1	T_x	A
		**  -> Q	T_CNAME	A
		**  If Q == A: loop!
		*/

		SM_CSTR_FREE(dns_rese->dnsrese_name);
		dns_rese->dnsrese_name = SM_CSTR_DUP(dns_reseh->dnsrese_name);
		dns_rese_free(dns_reseh);
		dns_reseh = NULL;

		if (dns_rese->dnsrese_type == T_CNAME
		    && SM_CSTR_CASEQ(dns_rese->dnsrese_name,
				dns_rese->dnsrese_val.dnsresu_name))
		{
			MTA_LIBDNS_DBG_DPRINTF((smioerr,
				"dns_rr_add: T_CNAME=%C, query=%C, error=points_to_itself\n"
				, dns_rese->dnsrese_name
				, dns_rese->dnsrese_val.dnsresu_name));
			/* just ignore this? */
			/* break; */

			/* treat it as temporary error? */
			dns_rese_free(dns_rese);
			ret = DNSR_TEMP;
			goto error;
		}

		MTA_LIBDNS_DBG_DPRINTF((smioerr,
			"dns_rr_add: eliminated one CNAME entry, query=%C, ipv4=%A, host=%C\n"
			, dns_rese->dnsrese_name
			, type == T_A
			  ? (ipv4_T) htonl(dns_rese->dnsrese_val.dnsresu_a) : 0
			, T_A == type ? NULL : dns_rese->dnsrese_val.dnsresu_name
			));
	}

	DRESL_INSERT_TAIL(dns_res, dns_rese);
	dns_res->dnsres_entries++;
	return SM_SUCCESS;

  error:
	if (dns_reseh != NULL && replaced) {
		dns_rese_free(dns_reseh);
		dns_reseh = NULL;
	}
	return ret;
}


#define DNS_RESE_IS_WORSE(dns_rese, pref, weight)	\
	((dns_rese)->dnsrese_pref > pref ||		\
	 ((dns_rese)->dnsrese_pref == pref && (dns_rese)->dnsrese_weight > weight))

/*
**  DNS_MXRR_INSERT -- insert DNS result for MX RR at the right place.
**
**	Parameters:
**		dns_res -- DNS result header
**		mxhost -- name of host
**		len -- len of mxhost
**		ttl -- TTL
**		pref -- preference
**		type -- DNS query type
**
**	Returns:
**		usual sm_error code
*/

static sm_ret_T
dns_mxrr_insert(dns_res_P dns_res, uchar *mxhost, uint len, uint ttl, ushort pref, dns_type_T type)
{
	ushort weight;
	sm_ret_T ret;
	dns_rese_P dns_rese, dns_rese_next;

	weight = mxrand(mxhost);

	/*
	**  Duplicate? Is this sanctioned by the RFCs?
	*/

	for (dns_rese = DRESL_FIRST(dns_res);
	     dns_rese != DRESL_END(dns_res);
	     dns_rese = dns_rese_next)
	{
		dns_rese_next = DRESL_NEXT(dns_rese);
		if (strcasecmp((const char *) mxhost,
			(const char *) sm_cstr_data(dns_rese->dnsrese_val.dnsresu_name))
		    == 0)
		{
			if (DNS_RESE_IS_WORSE(dns_rese, pref, weight)) {
				DRESL_REMOVE(dns_res, dns_rese);
				dns_rese_free(dns_rese);
				dns_res->dnsres_entries--;
				break;
			}
			else /* Existing MX record is "better" */
				return SM_SUCCESS;
		}
	}

	/* Insert the record */
	for (dns_rese = DRESL_FIRST(dns_res);
	     dns_rese != DRESL_END(dns_res);
	     dns_rese = dns_rese_next)
	{
		dns_rese_next = DRESL_NEXT(dns_rese);
		if (DNS_RESE_IS_WORSE(dns_rese, pref, weight)) {
			/* Enough space for one more? */
			if (dns_res->dnsres_entries < dns_res->dnsres_maxentries)
				dns_res->dnsres_entries++;
			else {
				/* remove last element */
				dns_rese_next = DRESL_END(dns_rese);
				DRESL_REMOVE(dns_res, dns_rese_next);
				dns_rese_free(dns_rese_next);
			}
			ret = dns_rese_new(NULL, 0, type, ttl, pref, weight,
					mxhost, len, (ipv4_T) 0, &dns_rese_next);
			if (sm_is_err(ret)) {
				/* More cleanup?? */
				dns_res->dnsres_entries--;
				return ret;
			}
			DRESL_INSERT_BEFORE(dns_res, dns_rese, dns_rese_next);
			return SM_SUCCESS;
		}
	}

	/* Not reached if record has been inserted */
	if (dns_res->dnsres_entries >= dns_res->dnsres_maxentries)
		return sm_error_temp(SM_EM_DNS, SM_E_FULL);
	ret = dns_rese_new(NULL, 0, type, ttl, pref, weight, mxhost,
			len, (ipv4_T) 0, &dns_rese);
	if (sm_is_err(ret)) {
		/* Cleanup?? */
		return ret;
	}
	DRESL_INSERT_TAIL(dns_res, dns_rese);
	dns_res->dnsres_entries++;
	return SM_SUCCESS;
}

/*
**  DNS_RD_HDR -- extract DNS header
**
**	Parameters:
**		ans -- DNS answer
**		dns_reply -- DNS reply structure (provided by caller)
**
**	Returns:
**		usual sm_error code
**
**	Side Effects:
**		dns_reply is filled.
*/

static sm_ret_T
dns_rd_hdr(sm_str_P ans, dns_reply_P dns_reply)
{
	HEADER *hp;

	SM_REQUIRE(dns_reply != NULL);
	dns_reply->dnsrpl_cur = (uchar *) sm_str_data(ans) + HFIXEDSZ;
	dns_reply->dnsrpl_end = (uchar *) sm_str_data(ans) + sm_str_getlen(ans);
	if (dns_reply->dnsrpl_cur > dns_reply->dnsrpl_end)
		return sm_error_perm(SM_EM_DNS, EINVAL);
	hp = (HEADER *) sm_str_data(ans);
	dns_reply->dnsrpl_header = hp;
	dns_reply->dnsrpl_ans_cnt = ntohs((ushort) hp->ancount);
	dns_reply->dnsrpl_qry_cnt = ntohs((ushort) hp->qdcount);
	dns_reply->dnsrpl_id = hp->id;

	/* Watch out... this is NOT a copy! */
	dns_reply->dnsrpl_answer = ans;
	return SM_SUCCESS;
}

/*
**  DNS_RD_QUERY -- extract DNS query from DNS reply
**
**	Parameters:
**		dns_reply -- DNS reply structure (provided by caller)
**		query -- query (output!, can be NULL)
**		qlen -- length of query
**		ptype -- (pointer to) DNS result type (output)
**
**	Returns:
**		usual sm_error code
**
**	Side Effects:
**		dns_reply is filled.
*/

static sm_ret_T
dns_rd_query(dns_reply_P dns_reply, uchar *query, int qlen, dns_type_T *ptype)
{
	int n;
	ushort qry_cnt;
	dns_type_T type;
	sm_str_P ans;

	SM_REQUIRE(dns_reply != NULL);
	ans = dns_reply->dnsrpl_answer;
	SM_REQUIRE(ans != NULL);	/* SM_IS_STR()? sm/str-int.h ? */
	type = 0;
	for (qry_cnt = dns_reply->dnsrpl_qry_cnt; qry_cnt > 0; qry_cnt--) {
		if (dns_reply->dnsrpl_cur >= dns_reply->dnsrpl_end)
			goto error;

		/* copy it first */
		if (query != NULL) {
			n = dn_expand(sm_str_data(ans), dns_reply->dnsrpl_end,
					dns_reply->dnsrpl_cur, (RES_UNC_T) query, qlen);
		}

		n = dn_skipname(dns_reply->dnsrpl_cur, dns_reply->dnsrpl_end);
		if (n < 0)
			goto error;

		dns_reply->dnsrpl_cur += n;
		if (dns_reply->dnsrpl_cur >= dns_reply->dnsrpl_end)
			goto error;

		/* extract the type */
		GETSHORT(type, dns_reply->dnsrpl_cur);

		/* skip the class (it's always C_IN) */
		dns_reply->dnsrpl_cur += INT16SZ;
	}
	if (ptype != NULL)
		*ptype = type;
	return SM_SUCCESS;

  error:
	return sm_error_perm(SM_EM_DNS, EINVAL);
}

/*
**  DNS_DECODE -- decode DNS resource records
**
**	Parameters:
**		ans -- answer from DNS server
**		query -- query (output!, can be NULL)
**		qlen -- length of query
**		ptype -- (pointer to) DNS result type (output)
**		dns_res -- DNS result struct (output)
**
**	Returns:
**		>=0: the number of resource records found.
**		<0 if there is an internal failure.
*/

/* Check whether current pointer exceeds valid range */
#define DNS_CUR_CHK do {									\
		if (dns_reply.dnsrpl_cur >= dns_reply.dnsrpl_end)	\
			goto error;										\
	} while (0)

/* Advance current pointer and check */
#define DNS_CUR_ADV_CHK(n) do {				\
		dns_reply.dnsrpl_cur += (n);		\
		DNS_CUR_CHK;						\
	} while (0)

/* Advance current pointer, no check */
#define DNS_CUR_ADV(n) dns_reply.dnsrpl_cur += (n)

/* Careful! This uses "continue", so it can't be in do{}while */
#define DNS_GET_NAME(bufp, buf, len, nullen)           \
	(len) = strlen((const char *) (bufp));             \
	if ((len) >= sizeof(buf) - 2)  /* paranoia */      \
		goto error;                                    \
	                                                   \
	/* Can this happen? */                             \
	if (0 == (len)) {                                  \
		nullen;                                        \
	}                                                  \
	if ((bufp)[(len) - 1] != '.') {                    \
		(bufp)[(len)] = '.';                           \
		(len)++;                                       \
	}                                                  \
	(bufp)[(len)] = '\0'

sm_ret_T
dns_decode(sm_str_P ans, uchar *query, int qlen, dns_type_T *ptype, dns_res_P dns_res)
{
	int n, ancount, buflen;
	uint l, r, ttl;
	ushort pref;
	dns_type_T type;
	ipv4_T ipv4;
	sm_ret_T ret;
	uchar *bp;
	uchar hostbuf[MAXPACKET], namebuf[MAXPACKET];
	dns_reply_T dns_reply;

	/* SM_REQUIRE(sizeof(hostbuf) == sizeof(namebuf));	*/
	ttl = 0;

	/* extract header */
	ret = dns_rd_hdr(ans, &dns_reply);
	if (sm_is_err(ret))
		goto error;

	/* extract query */
	ret = dns_rd_query(&dns_reply, query, qlen, ptype);
	if (sm_is_err(ret))
		goto error;

	/* original query */
	dns_res->dnsres_qtype = (ptype != NULL) ? *ptype : 0;
	dns_res->dnsres_id = dns_reply.dnsrpl_id;

	/*
	**  Extract return code (we need query first to associate it with a
	**  request)
	*/

	ret = SM_SUCCESS;
	switch (dns_reply.dnsrpl_header->rcode) {
	  case NOERROR:
		break;

		/* treat all of these as permanent errors */
	  case REFUSED:
	  case NOTIMP:
	  case FORMERR:
		ret = DNSR_PERM;
		break;

	  case NXDOMAIN:
		/* host doesn't exist in DNS; might be in /etc/hosts */
		ret = DNSR_NOTFOUND;
		break;

	  case SERVFAIL:
		ret = DNSR_TEMP;
		break;

	  default:
		/* complain about unknown error code? */
		ret = DNSR_TEMP;
		break;
	}
	if (sm_is_err(ret))
		goto error;

	/*
	**  Extract data, store it in an appropriate place.
	*/

	buflen = sizeof(hostbuf) - 1;
	SM_ASSERT(buflen > 0);
	ancount = dns_reply.dnsrpl_ans_cnt;
	type = 0;

	/* See RFC 1035 for layout of RRs. */
	while (--ancount >= 0
	       && dns_reply.dnsrpl_cur < dns_reply.dnsrpl_end
	       && dns_res->dnsres_entries < dns_res->dnsres_maxentries)
	{
		DNS_CUR_CHK;
		bp = namebuf;
		n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end,
				dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen);
		if (n < 0)
			goto error;
		DNS_GET_NAME(bp, namebuf, r, continue);
		DNS_CUR_ADV_CHK(n);
		GETSHORT(type, dns_reply.dnsrpl_cur);

		/* skip over class */
		DNS_CUR_ADV_CHK(INT16SZ);

		GETLONG(ttl, dns_reply.dnsrpl_cur);
		DNS_CUR_CHK;
		GETSHORT(n, dns_reply.dnsrpl_cur);	/* rdlength */
		DNS_CUR_CHK;
		bp = hostbuf;

		switch (type) {
		  case T_MX:
			GETSHORT(pref, dns_reply.dnsrpl_cur);
			DNS_CUR_CHK;
			n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end,
					dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen);
			if (n < 0)
				goto error;
			DNS_CUR_ADV(n);

			/* MX 0 . results in len = 0 */
			DNS_GET_NAME(bp, hostbuf, l, ret = DNSR_MXINVALID; goto error;);
			MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_MX=%s\n", bp));

			/* can we make these flags configurable? */
			if (!validdnsdomain(bp, DNS__OK|DNS_HYPHENS /*|DNS_DOT */)) {
				ret = DNSR_MXINVALID;
				goto error;
			}

			ret = dns_mxrr_insert(dns_res, bp, l, ttl, pref, type);
			if (sm_is_err(ret))
				goto error;
			break;

		  case T_A:
			sm_memcpy(&ipv4, dns_reply.dnsrpl_cur, INT32SZ);
			MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_A=%X\n", ipv4));
			DNS_CUR_ADV(INT32SZ);
			ret = dns_rr_add(dns_res, namebuf, r, (uchar *) NULL, 0, ttl,
					(ushort) 0, type, ipv4);
			if (sm_is_err(ret))
				goto error;
			break;

		  case T_PTR:
			/* Check for correctness! */
			n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end,
					dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen);
			if (n < 0)
				goto error;
			DNS_CUR_ADV(n);
			DNS_GET_NAME(bp, hostbuf, l, continue);
			MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_PTR=%s\n", bp));
#if 0
			if (!validdnsdomain(bp, DNS__OK|DNS_HYPHENS)) {
				ret = DNSR_PTRINVALID;
				goto error;
			}
#endif /* 0 */
			ret = dns_rr_add(dns_res, namebuf, r, bp, l, ttl,
					(ushort) 0, type, 0);
			if (sm_is_err(ret))
				goto error;
			break;

		  case T_CNAME:
			/* Check for correctness! */
			n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end,
					dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen);
			if (n < 0)
				goto error;
			DNS_CUR_ADV(n);
			DNS_GET_NAME(bp, hostbuf, l, continue);
			MTA_LIBDNS_DBG_DPRINTF((smioerr,
				"dns_decode: name=%s, T_CNAME=%s\n"
				, namebuf, bp));
#if 0
/*
problem:

176.233.31.209.in-addr.arpa.  2H IN CNAME  176.160/27.233.31.209.in-addr.arpa.
176.160/27.233.31.209.in-addr.arpa.  3h14m45s IN PTR  knecht.sendmail.org.

moreover, caller does not deal with this error (yet)
*/
			if (!validdnsdomain(bp, DNS__OK|DNS_HYPHENS)) {
				ret = DNSR_CNINVALID;
				goto error;
			}
#endif /* 0 */
			if (r == l && sm_memcaseeq(namebuf, bp, r)) {
				MTA_LIBDNS_DBG_DPRINTF((smioerr,
					"dns_decode: T_CNAME=%s, error=points_to_itself\n", bp));
				/* just ignore this? */
				/* break; */

				/* treat it as temporary error? */
				ret = DNSR_TEMP;
				goto error;
			}

			ret = dns_rr_add(dns_res, namebuf, r, bp, l, ttl,
					(ushort) 0, type, 0);
			if (sm_is_err(ret))
				goto error;
			break;

		  default:
			DNS_CUR_ADV(n);
			break;
		}

	}
	if (dns_reply.dnsrpl_ans_cnt == 0)
		dns_res->dnsres_ret = DNSR_NOTFOUND;
	else
		dns_res->dnsres_ret = DNSR_OK;
	return dns_res->dnsres_entries;

  error:
	/* Remove results */
	dns_resl_free(dns_res);
	dns_res->dnsres_ret = ret;
	return ret;
}


syntax highlighted by Code2HTML, v. 0.9.1