/*
**  Copyright (c) 2007, 2009 Sendmail, Inc. and its suppliers.
**    All rights reserved.
*/

#ifdef _FFR_VBR

#ifndef lint
static char vbr_c_id[] = "@(#)$Id: vbr.c,v 1.12 2009/02/18 23:44:14 msk Exp $";
#endif /* !lint */

/* system includes */
#include <sys/param.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <resolv.h>

#ifdef __STDC__
# include <stdarg.h>
#else /* __STDC__ */
# include <varargs.h>
#endif /* _STDC_ */

/* libsm includes */
#include <sm/gen.h>
#ifdef WITHOUT_LIBSM
# define sm_strlcat	strlcat
# define sm_strlcpy	strlcpy
#else /* WITHOUT_LIBSM */
# include <sm/string.h>
#endif /* WITHOUT_LIBSM */

/* libar includes */
#if USE_ARLIB
# include <ar.h>
#endif /* USE_ARLIB */

/* libdkim includes */
#ifdef _FFR_VBR
# include "vbr.h"
#endif /* _FFR_VBR */

/* local definitions needed for DNS queries */
#define MAXPACKET		8192
#if defined(__RES) && (__RES >= 19940415)
# define RES_UNC_T		char *
#else /* __RES && __RES >= 19940415 */
# define RES_UNC_T		unsigned char *
#endif /* __RES && __RES >= 19940415 */

#ifndef FALSE
# define FALSE			0
#endif /* ! FALSE */
#ifndef TRUE
# define TRUE			1
#endif /* ! TRUE */

#define BUFRSZ			2048
#define	DEFERRLEN		64
#define	DEFTIMEOUT		10
#define MAXCNAMEDEPTH		3

struct vbr_handle
{
	size_t		vbr_errlen;		/* error buffer size */
#if USE_ARLIB
	u_int		vbr_timeout;		/* query timeout */
	u_int		vbr_callback_int;	/* callback interval */
	AR_LIB		vbr_arlib;		/* libar handle */
	void *		vbr_user_context;	/* user context for callback */
	void		(*vbr_dns_callback) (const void *context);
#endif /* USE_ARLIB */
	void *		(*vbr_malloc) (void *closure, size_t nbytes);
	void		(*vbr_free) (void *closure, void *p);
	void *		vbr_closure;		/* memory closure */
	char *		vbr_domain;		/* sending domain */
	char *		vbr_type;		/* message type */
	char *		vbr_cert;		/* claimed certifiers */
	u_char *	vbr_error;		/* error buffer */
	u_char **	vbr_trusted;		/* trusted certifiers */
};

/* prototypes */
static void vbr_error __P((VBR *, const char *, ...));

/* ========================= PRIVATE SECTION ========================= */

/*
**  VBR_MALLOC -- allocate memory
**
**  Parameters:
**  	vbr -- VBR context in which this is performed
**  	closure -- opaque closure handle for the allocation
**  	nbytes -- number of bytes desired
**
**  Return value:
**  	Pointer to allocated memory, or NULL on failure.
*/

static void *
vbr_malloc(VBR *vbr, void *closure, size_t nbytes)
{
	assert(vbr != NULL);

	if (vbr->vbr_malloc == NULL)
		return malloc(nbytes);
	else
		return vbr->vbr_malloc(closure, nbytes);
}

/*
**  VBR_FREE -- release memory
**
**  Parameters:
**  	vbr -- VBR context in which this is performed
**  	closure -- opaque closure handle for the allocation
**  	ptr -- pointer to memory to be freed
**
**  Return value:
**  	None.
*/

static void
vbr_free(VBR *vbr, void *closure, void *ptr)
{
	assert(vbr != NULL);

	if (vbr->vbr_free == NULL)
		free(ptr);
	else
		vbr->vbr_free(closure, ptr);
}

/*
**  VBR_VERROR -- log an error into a VBR handle (varargs version)
**
**  Parameters:
**  	vbr -- VBR context in which this is performed
**  	format -- format to apply
**  	va -- argument list
**
**  Return value:
**  	None.
*/

static void
vbr_verror(VBR *vbr, const char *format, va_list va)
{
	int flen;
	int saverr;
	u_char *new;

	assert(vbr != NULL);
	assert(format != NULL);

	saverr = errno;

	if (vbr->vbr_error == NULL)
	{
		vbr->vbr_error = vbr_malloc(vbr, vbr->vbr_closure, DEFERRLEN);
		if (vbr->vbr_error == NULL)
		{
			errno = saverr;
			return;
		}
		vbr->vbr_errlen = DEFERRLEN;
	}

	for (;;)
	{
		flen = vsnprintf(vbr->vbr_error, vbr->vbr_errlen, format, va);

		/* compensate for broken vsnprintf() implementations */
		if (flen == -1)
			flen = vbr->vbr_errlen * 2;

		if (flen >= vbr->vbr_errlen)
		{
			new = vbr_malloc(vbr, vbr->vbr_closure, flen + 1);
			if (new == NULL)
			{
				errno = saverr;
				return;
			}

			vbr_free(vbr, vbr->vbr_closure, vbr->vbr_error);
			vbr->vbr_error = new;
			vbr->vbr_errlen = flen + 1;
		}
		else
		{
			break;
		}
	}

	errno = saverr;
}

/*
**  VBR_ERROR -- log an error into a VBR handle
**
**  Parameters:
**  	vbr -- VBR context in which this is performed
**  	format -- format to apply
**  	... -- arguments
**
**  Return value:
**  	None.
*/

static void
vbr_error(VBR *vbr, const char *format, ...)
{
	va_list va;

	assert(vbr != NULL);
	assert(format != NULL);

	va_start(va, format);
	vbr_verror(vbr, format, va);
	va_end(va);
}

/*
**  VBR_TXT_DECODE -- decode a TXT reply
**
**  Parameters:
**  	ansbuf -- answer buffer
**  	anslen -- size of answer buffer
**  	buf -- output buffer
**  	buflen -- size of output buffer
**
**  Return value:
**  	TRUE iff ansbuf contains an IN TXT reply that could be deocde.
*/

static bool
vbr_txt_decode(u_char *ansbuf, size_t anslen, u_char *buf, size_t buflen)
{
	int type;
	int class;
	int qdcount;
	int ancount;
	int n;
	int c;
	u_char *cp;
	u_char *eom;
	u_char *p;
	HEADER hdr;
	char qname[VBR_MAXHOSTNAMELEN + 1];

	assert(ansbuf != NULL);
	assert(buf != NULL);

	/* set up pointers */
	memcpy(&hdr, ansbuf, sizeof hdr);
	cp = (u_char *) &ansbuf + HFIXEDSZ;
	eom = (u_char *) &ansbuf + anslen;

	/* skip over the name at the front of the answer */
	for (qdcount = ntohs((unsigned short) hdr.qdcount);
	     qdcount > 0;
	     qdcount--)
	{
		/* copy it first */
		(void) dn_expand((unsigned char *) &ansbuf, eom, cp,
		                 qname, sizeof qname);

		if ((n = dn_skipname(cp, eom)) < 0)
			return FALSE;
		cp += n;

		/* extract the type and class */
		if (cp + INT16SZ + INT16SZ > eom)
			return FALSE;

		GETSHORT(type, cp);
		GETSHORT(class, cp);
	}

	if (type != T_TXT || class != C_IN)
		return FALSE;

	if (hdr.rcode == NXDOMAIN)
		return FALSE;

	/* get the answer count */
	ancount = ntohs((unsigned short) hdr.ancount);
	if (ancount == 0)
		return FALSE;

	/* if truncated, we can't do it */
	if (hdr.tc)
		return FALSE;

	/* grab the label, even though we know what we asked... */
	if ((n = dn_expand((unsigned char *) &ansbuf, eom, cp,
	                   (RES_UNC_T) qname, sizeof qname)) < 0)
		return FALSE;
	/* ...and move past it */
	cp += n;

	/* extract the type and class */
	if (cp + INT16SZ + INT16SZ > eom)
		return FALSE;
	GETSHORT(type, cp);
	GETSHORT(class, cp);

	/* reject anything that's not valid (stupid wildcards) */
	if (type != T_TXT || class != C_IN)
		return FALSE;

	/* skip the TTL */
	cp += INT32SZ;

	/* get payload length */
	if (cp + INT16SZ > eom)
		return FALSE;
	GETSHORT(n, cp);

	/* XXX -- maybe deal with a partial reply rather than require it all */
	if (cp + n > eom)
		return FALSE;

	if (n > buflen)
		return FALSE;

	/* extract the payload */
	memset(buf, '\0', buflen);
	p = buf;
	while (n > 0)
	{
		c = *cp++;
		n--;
		while (c > 0)
		{
			*p++ = *cp++;
			c--;
			n--;
		}
	}

	return TRUE;
}

/* ========================= PUBLIC SECTION ========================= */

/*
**  VBR_INIT -- initialize a VBR handle
**
**  Parameters:
**  	caller_mallocf -- caller-provided memory allocation function
**  	caller_freef -- caller-provided memory release function
**  	closure -- memory closure to pass to the above when used
**
**  Return value:
**  	A new VBR handle suitable for use with other VBR functions, or
**  	NULL on failure.
**  
**  Side effects:
**  	Strange radar returns at Indianapolis ARTCC.
*/

VBR *
vbr_init(void *(*caller_mallocf)(void *closure, size_t nbytes),
         void (*caller_freef)(void *closure, void *p),
         void *closure)
{
	VBR *new;

	/* copy the parameters */
	new = (VBR *) malloc(sizeof(struct vbr_handle));
	if (new == NULL)
		return NULL;

	new->vbr_malloc = caller_mallocf;
	new->vbr_free = caller_freef;
	new->vbr_closure = closure;
#if USE_ARLIB
	new->vbr_timeout = DEFTIMEOUT;
	new->vbr_callback_int = 0;
	new->vbr_dns_callback = NULL;
	new->vbr_user_context = NULL;
#endif /* USE_ARLIB */
	new->vbr_errlen = 0;
	new->vbr_error = NULL;

	/* initialize the resolver */
#if USE_ARLIB
	new->vbr_arlib = ar_init(NULL, NULL, NULL, 0);
	if (new->vbr_arlib == NULL)
	{
		free(new);
		return NULL;
	}
#else /* USE_ARLIB */
	(void) res_init();
#endif /* USE_ARLIB */

	new->vbr_domain = NULL;
	new->vbr_type = NULL;
	new->vbr_cert = NULL;
	new->vbr_trusted = NULL;

	return new;
}

#define	CLOBBER(x)	if ((x) != NULL) \
			{ \
				vbr_free(vbr, vbr->vbr_closure, (x)); \
				(x) = NULL; \
			}

/*
**  VBR_CLOSE -- shut down a VBR instance
**
**  Parameters:
**  	vbr -- VBR handle to shut down
**
**  Return value:
**  	None.
*/

void
vbr_close(VBR *vbr)
{
	assert(vbr != NULL);

#if USE_ARLIB
	if (vbr->vbr_arlib != NULL)
		ar_shutdown(vbr->vbr_arlib);
#endif /* USE_ARLIB */

	CLOBBER(vbr->vbr_error);

	CLOBBER(vbr);
}

/*
**  VBR_GETERROR -- return any stored error string from within the VBR
**                  context handle
**
**  Parameters:
**  	vbr -- VBR handle from which to retrieve an error string
**
**  Return value:
**  	A pointer to the stored string, or NULL if none was stored.
*/

const char *
vbr_geterror(VBR *vbr)
{
	assert(vbr != NULL);

	return vbr->vbr_error;
}

/* XXX -- need a function to take in a VBR-Info: header and parse it? */

#ifdef USE_ARLIB
/*
**  VBR_SETTIMEOUT -- set the DNS timeout
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	timeout -- requested timeout (seconds)
**
**  Return value:
**  	None (yet).
*/

void
vbr_settimeout(VBR *vbr, u_int timeout)
{
	assert(vbr != NULL);

	vbr->vbr_timeout = timeout;
}

/*
**  VBR_SETCALLBACKINT -- set the DNS callback interval
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	cbint -- requested callback interval (seconds)
**
**  Return value:
**  	None (yet).
*/

void
vbr_setcallbackint(VBR *vbr, u_int cbint)
{
	assert(vbr != NULL);

	vbr->vbr_callback_int = cbint;
}

/*
**  VBR_SETCALLBACKCTX -- set the DNS callback context
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	ctx -- context to pass to the DNS callback
**
**  Return value:
**  	None (yet).
*/

void
vbr_setcallbackctx(VBR *vbr, void *ctx)
{
	assert(vbr != NULL);

	vbr->vbr_user_context = ctx;
}

/*
**  VBR_SETDNSCALLBACK -- set the DNS wait callback
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	func -- function to call; should take an opaque context pointer
**
**  Return value:
**  	VBR_STAT_OK -- success
**  	VBR_STAT_INVALID -- invalid use
*/

VBR_STAT
vbr_setdnscallback(VBR *vbr, void (*func)(const void *context))
{
	assert(vbr != NULL);

#if USE_ARLIB
	vbr->vbr_dns_callback = func;

	return VBR_STAT_OK;
#else /* USE_ARLIB */
	return VBR_STAT_INVALID;
#endif /* USE_ARLIB */
}

#endif /* USE_ARLIB */

/*
**  VBR_SETDOMAIN -- declare the sender's domain
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	cert -- certifiers string
**
**  Return value:
**  	None (yet).
*/

void
vbr_setdomain(VBR *vbr, char *domain)
{
	assert(vbr != NULL);
	assert(domain != NULL);

	vbr->vbr_domain = domain;
}

/*
**  VBR_SETCERT -- store the VBR certifiers of this message
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	cert -- certifiers string
**
**  Return value:
**  	None (yet).
*/

void
vbr_setcert(VBR *vbr, char *cert)
{
	assert(vbr != NULL);
	assert(cert != NULL);

	vbr->vbr_cert = cert;
}

/*
**  VBR_SETTYPE -- store the VBR type of this message
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	type -- type string
**
**  Return value:
**  	None (yet).
*/

void
vbr_settype(VBR *vbr, char *type)
{
	assert(vbr != NULL);
	assert(type != NULL);

	vbr->vbr_type = type;
}

/*
**  VBR_TRUSTEDCERTS -- store the trusted VBR certifiers
**
**  Parameters:
**  	vbr -- VBR handle, created by vbr_init()
**  	certs -- NULL-terminated list of trusted certifiers
**
**  Return value:
**  	None (yet).
*/

void
vbr_trustedcerts(VBR *vbr, char **certs)
{
	assert(vbr != NULL);
	assert(certs != NULL);

	vbr->vbr_trusted = (u_char **) certs;
}

/*
**  VBR_QUERY -- query the vouching servers for results
**
**  Parameters:
**  	vbr -- VBR handle
**  	res -- result string (one of "fail", "pass"); returned
**  	cert -- name of the certifier that returned a "pass"; returned
**
**  Return value:
**  	VBR_STAT_OK -- able to determine a result
**  	VBR_STAT_INVALID -- vbr_trustedcerts(), vbr_settype() and
**  	                    vbr_setcert() were not all called
**  	VBR_STAT_DNSERROR -- DNS issue prevented resolution
**
**  Notes:
**  	- "pass" is the result if ANY certifier vouched for the message.
**  	- "res" is not modified if no result could be determined
**  	- there's no attempt to validate the values found
**  	- vbr_cert is destroyed by this function
*/

VBR_STAT
vbr_query(VBR *vbr, char **res, char **cert)
{
	int n;
	char *p;
	char *last;
	char certs[VBR_MAXHEADER + 1];
	char query[VBR_MAXHOSTNAMELEN + 1];

	assert(vbr != NULL);
	assert(res != NULL);
	assert(cert != NULL);

	if (vbr->vbr_type == NULL ||
	    vbr->vbr_cert == NULL ||
	    vbr->vbr_trusted == NULL)
	{
		vbr_error(vbr, "required data for VBR check missing");
		return VBR_STAT_INVALID;
	}

	sm_strlcpy(certs, vbr->vbr_cert, sizeof certs);

	for (p = strtok_r(certs, ":", &last);
	     p != NULL;
	     p = strtok_r(NULL, ":", &last))
	{
		for (n = 0; vbr->vbr_trusted[n] != NULL; n++)
		{
			if (strcasecmp(p, vbr->vbr_trusted[n]) == 0)
			{
				int status;
#ifdef USE_ARLIB
				int arerror;
				AR_QUERY q;
				AR_LIB ar;
#endif /* USE_ARLIB */
				char *last2;
				char *p2;
#ifdef USE_ARLIB
				struct timeval timeout;
#endif /* USE_ARLIB */
				unsigned char ansbuf[MAXPACKET];
				unsigned char buf[BUFRSZ];

				snprintf(query, sizeof query, "%s.%s.%s",
				         vbr->vbr_domain, VBR_PREFIX, p);

#ifdef USE_ARLIB
				ar = vbr->vbr_arlib;
				timeout.tv_sec = vbr->vbr_timeout;
				timeout.tv_usec = 0;
				q = ar_addquery(ar, query, C_IN, T_TXT,
				                MAXCNAMEDEPTH, ansbuf,
		                                sizeof ansbuf, &arerror,
		                                vbr->vbr_timeout == 0 ? NULL
				                                        : &timeout);
				if (q == NULL)
				{
					vbr_error(vbr, "ar_addquery() failed");
					return VBR_STAT_DNSERROR;
				}

				if (vbr->vbr_dns_callback == NULL)
				{
					status = ar_waitreply(ar, q, NULL,
					                      NULL);
				}
				else
				{
					for (;;)
					{
						timeout.tv_sec = vbr->vbr_callback_int;
						timeout.tv_usec = 0;

						status = ar_waitreply(ar, q,
						                      NULL,
						                      &timeout);

						if (status != AR_STAT_NOREPLY)
							break;

						vbr->vbr_dns_callback(vbr->vbr_user_context);
					}
				}

				(void) ar_cancelquery(ar, q);
#else /* USE_ARLIB */
				status = res_query(query, C_IN, T_TXT, ansbuf,
				                   sizeof ansbuf);
#endif /* USE_ARLIB */

#if USE_ARLIB
				if (status == AR_STAT_ERROR ||
				    status == AR_STAT_EXPIRED)
				{
					vbr_error(vbr, "failed to retrieve %s",
						  query);
					return VBR_STAT_DNSERROR;
				}
#else /* USE_ARLIB */
				if (status == -1)
				{
					switch (h_errno)
					{
					  case HOST_NOT_FOUND:
					  case NO_DATA:
						continue;

					  case TRY_AGAIN:
					  case NO_RECOVERY:
					  default:
						vbr_error(vbr,
						          "failed to retreive %s",
						          query);
						return VBR_STAT_DNSERROR;
					}
				}
#endif /* USE_ARLIB */

				/* try to decode the reply */
				if (!vbr_txt_decode(ansbuf, sizeof ansbuf,
				                    buf, sizeof buf))
					continue;

				/* see if there's a vouch match */
				for (p2 = strtok_r(buf, " \t", &last2);
				     p2 != NULL;
				     p2 = strtok_r(NULL, " \t", &last2))
				{
					if (strcasecmp(p2, VBR_ALL) == 0 ||
					    strcasecmp(p2,
					               vbr->vbr_type) == 0)
					{
						/* we have a winner! */
						*res = "pass";
						*cert = p;
						return VBR_STAT_OK;
					}
				}
			}
		}
	}

	/* nobody vouched */
	*res = "fail";
	return VBR_STAT_OK;
}

/*
**  VBR_GETHEADER -- generate and store the VBR-Info header
**
**  Parameters:
**  	vbr -- VBR handle
**  	hdr -- header buffer
**  	len -- number of bytes available at "hdr"
**
**  Return value:
**  	VBR_STAT_OK -- success
**  	VBR_STAT_NORESOURCE -- "hdr" was too short
**  	VBR_STAT_INVALID -- not all VBR information was provided
*/

VBR_STAT
vbr_getheader(VBR *vbr, char *hdr, size_t len)
{
	size_t olen;

	assert(vbr != NULL);
	assert(hdr != NULL);

	if (vbr->vbr_cert == NULL || vbr->vbr_type == NULL)
	{
		vbr_error(vbr, "VBR certifiers or type missing");
		return VBR_STAT_INVALID;
	}

	olen = snprintf(hdr, len, "md=%s; mc=%s; mv=%s",
	                vbr->vbr_domain, vbr->vbr_type, vbr->vbr_cert);
	if (olen >= len)
	{
		vbr_error(vbr, "VBR buffer too small");
		return VBR_STAT_NORESOURCE;
	}

	return VBR_STAT_OK;
}
#endif /* _FFR_VBR */
