/*
**  Copyright (c) 2004, 2005, 2007, 2008 Sendmail, Inc. and its suppliers.
**	All rights reserved.
**
**  $Id: util.c,v 1.48 2008/03/12 14:17:17 msk Exp $
*/

#ifndef lint
static char util_c_id[] = "@(#)$Id: util.c,v 1.48 2008/03/12 14:17:17 msk Exp $";
#endif /* !lint */

/* system includes */
#include <sys/param.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/file.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <syslog.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <netdb.h>
#include <fcntl.h>

/* libsm includes */
#include <sm/gen.h>
#include <sm/string.h>

#if POPAUTH
/* system includes */
# include <pthread.h>

/* libdb includes */
# include <db.h>
#endif /* POPAUTH */

/* dk-filter includes */
#include "dk-filter.h"
#include "util.h"

/* globals */
#if POPAUTH
static pthread_mutex_t pop_lock;
#endif /* POPAUTH */

static char *optlist[] =
{
#if POPAUTH
	"POPAUTH",
#endif /* POPAUTH */

#if _FFR_ANTICIPATE_SENDMAIL_MUNGE
	"_FFR_ANTICIPATE_SENDMAIL_MUNGE",
#endif /* _FFR_ANTICIPATE_SENDMAIL_MUNGE */

#if _FFR_FLUSH_HEADERS
	"_FFR_FLUSH_HEADERS",
#endif /* _FFR_FLUSH_HEADERS */

#if _FFR_REQUIRED_HEADERS
	"_FFR_REQUIRED_HEADERS",
#endif /* _FFR_REQUIRED_HEADERS */

#if _FFR_SELECT_CANONICALIZATION
	"_FFR_SELECT_CANONICALIZATION",
#endif /* _FFR_SELECT_CANONICALIZATION */

	NULL
};

/*
**  DKF_OPTLIST -- print active options
**
**  Parameters:
**  	where -- where to write the list
**
**  Return value:
**   	None.
*/

void
dkf_optlist(FILE *where)
{
	bool first = TRUE;
	int c;

	assert(where != NULL);

	for (c = 0; optlist[c] != NULL; c++)
	{
		if (first)
		{
			fprintf(where, "Active code options:\n");
			first = FALSE;
		}

		fprintf(where, "\t%s\n", optlist[c]);
	}
}

/*
**  DKF_SETMAXFD -- increase the file descriptor limit as much as possible
**
**  Parameters:
**  	None.
**
**  Return value:
**  	None.
*/

void
dkf_setmaxfd(void)
{
	struct rlimit rlp;

	if (getrlimit(RLIMIT_NOFILE, &rlp) != 0)
	{
		syslog(LOG_WARNING, "getrlimit(): %s", strerror(errno));
	}
	else
	{
		rlp.rlim_cur = rlp.rlim_max;
		if (setrlimit(RLIMIT_NOFILE, &rlp) != 0)
		{
			syslog(LOG_WARNING, "setrlimit(): %s",
			       strerror(errno));
		}
	}
}

/*
**  DKF_STRIPBRACKETS -- remove angle brackets from the sender address
**
**  Parameters:
** 	addr -- address to be processed
**
**  Return value:
**  	None.
*/

void
dkf_stripbrackets(char *addr)
{
	char *p, *q;

	assert(addr != NULL);

	p = addr;
	q = addr + strlen(addr) - 1;

	while (*p == '<' && *q == '>')
	{
		p++;
		*q-- = '\0';
	}

	if (p != addr)
	{
		for (q = addr; *p != '\0'; p++, q++)
			*q = *p;
		*q = '\0';
	}
}

/*
**  DKF_LOWERCASE -- lowercase-ize a string
**
**  Parameters:
**  	str -- string to convert
**
**  Return value:
**  	None.
*/

void
dkf_lowercase(char *str)
{
	char *p;

	assert(str != NULL);

	for (p = str; *p != '\0'; p++)
	{
		if (isascii(*p) && isupper(*p))
			*p = tolower(*p);
	}
}

/*
**  DKF_LIST_LOOKUP -- look up a name in a peerlist
**
**  Parameters:
**  	list -- list of records to check
** 	data -- record to find
**
**  Return value:
**   	TRUE if found, FALSE otherwise
*/

static bool
dkf_list_lookup(Peer list, char *data)
{
	Peer current;

	assert(data != NULL);

	for (current = list; current != NULL; current = current->peer_next)
	{
		if (strcasecmp(data, current->peer_info) == 0)
			return TRUE;
	}

	return FALSE;
}

/*
**  DKF_CHECKHOST -- check the peerlist for a host and its wildcards
**
**  Parameters:
**  	list -- list of records to check
**  	host -- hostname to find
**
**  Return value:
**  	TRUE if there's a match, FALSE otherwise.
*/

bool
dkf_checkhost(Peer list, char *host)
{
	char *p;

	assert(host != NULL);

	/* short circuit */
	if (list == NULL)
		return FALSE;

	if (dkf_list_lookup(list, host))
		return TRUE;

	for (p = strchr(host, '.');
	     p != NULL;
	     p = strchr(p + 1, '.'))
	{
		if (dkf_list_lookup(list, p))
			return TRUE;
	}

	return FALSE;
}

/*
**  DKF_CHECKIP -- check a peerlist table for an IP address or its matching
**                 wildcards
**
**  Parameters:
**  	list -- list to check
**  	ip -- IP address to find
**
**  Return value:
**  	TRUE if there's a match, FALSE otherwise.
*/

bool
dkf_checkip(Peer list, struct sockaddr *ip)
{
	int c;
	char tmpbuf[MAXHOSTNAMELEN + 1];
	char ipbuf[MAXHOSTNAMELEN + 1];
	struct in_addr addr;
	struct in_addr mask;

	assert(ip != NULL);

	/* short circuit */
	if (list == NULL)
		return FALSE;

#if NETINET6
	if (ip->sa_family == AF_INET6)
	{
		struct in6_addr addr;

		memcpy(&addr, &((struct sockaddr_in6 *)ip)->sin6_addr,
		       sizeof addr);

		if (IN6_IS_ADDR_V4MAPPED(&addr))
		{
			inet_ntop(AF_INET,
			          &addr.s6_addr[INET6_ADDRSTRLEN - INET_ADDRSTRLEN],
			          tmpbuf, sizeof tmpbuf);
		}
		else
		{
			char *d;
			char *dst;
			size_t sz;
			size_t dst_len;

			dst = tmpbuf;
			d = dst;
			dst_len = sizeof tmpbuf;

			memset(tmpbuf, '\0', sizeof tmpbuf);

			sz = sm_strlcpy(tmpbuf, "IPv6:", sizeof tmpbuf);
			if (sz >= sizeof tmpbuf)
				return FALSE;

			dst += sz;
			dst_len -= sz;
			inet_ntop(AF_INET6, &addr, dst, dst_len);
		}

		return (dkf_list_lookup(list, tmpbuf));
	}
#endif /* NETINET6 */

	memcpy(&addr.s_addr, &((struct sockaddr_in *)ip)->sin_addr,
	       sizeof(addr.s_addr));

	/* test the IP by itself */
	(void) dkf_inet_ntoa(addr, ipbuf, sizeof ipbuf);
	if (dkf_list_lookup(list, ipbuf))
		return TRUE;

	/* test the IP/32 */
	sm_strlcat(ipbuf, "/32", sizeof ipbuf);
	if (dkf_list_lookup(list, ipbuf))
		return TRUE;

	/*
	**  Prepare to cycle through the other IP/bits possibilities.
	**  Turn on all the possible network bits in our bitmask.
	*/

	mask.s_addr = 0;
	for (c = 0; c < 32; c++)
		mask.s_addr |= htonl(1 << (31 - c));

	/* decompose the address bit-by-bit and try the appropriate entry */
	for (c = 0; c < 32; c++)
	{
		mask.s_addr ^= htonl(1 << c);
		addr.s_addr = (addr.s_addr & mask.s_addr);

		dkf_inet_ntoa(addr, tmpbuf, sizeof tmpbuf);
		snprintf(ipbuf, sizeof ipbuf, "%s/%d", tmpbuf, 31 - c);

		if (dkf_list_lookup(list, ipbuf))
			return TRUE;
	}

	return FALSE;
}

#if POPAUTH
/*
**  DKF_INITPOPAUTH -- initialize POPAUTH stuff
**
**  Parameters:
**  	None.
**
**  Return value:
**  	0 on success, an error code on failure.  See pthread_mutex_init().
*/

int
dkf_initpopauth(void)
{
	return pthread_mutex_init(&pop_lock, NULL);
}

/*
**  DKF_CHECKPOPAUTH -- check a POP before SMTP database for client
**                      authentication
**
**  Parameters:
**  	db -- DB handle to use for searching
**  	ip -- IP address to find
**
**  Return value:
**  	TRUE iff the database could be opened and the client was verified.
**
**  Notes:
**  	- should be a shared handle rather than a per-thread handle to
**  	  reduce descriptor consumption
**  	- needs locking
**  	- should be able to return TRUE/FALSE as well as errors
**  	- path should be runtime-configurable rather than hard-coded
**  	- does the key contain anything meaningful, like an expiry time?
*/

bool
dkf_checkpopauth(DB *db, struct sockaddr *ip)
{
	bool ret = FALSE;
	int status;
	int fd;
	struct sockaddr_in *sin;
	struct in_addr addr;
	DBT d;
	DBT q;
	char ipbuf[MAXHOSTNAMELEN + 1];

	assert(ip != NULL);

	if (db == NULL)
		return FALSE;

	/* skip anything not IPv4 (for now) */
	if (ip->sa_family != AF_INET)
		return FALSE;
	else
		sin = (struct sockaddr_in *) ip;

	memset(&d, 0, sizeof d);
	memset(&q, 0, sizeof q);

	/* we don't care what the value is for now */
	d.flags = DB_DBT_USERMEM|DB_DBT_PARTIAL;

	memcpy(&addr.s_addr, &sin->sin_addr, sizeof addr.s_addr);

	dkf_inet_ntoa(addr, ipbuf, sizeof ipbuf);

	q.data = ipbuf;
	q.size = strlen(q.data);

	/* establish read-lock */
	fd = -1;
# if DB_VERSION_MAJOR >= 2
	status = -1;
	status = db->fd(db, &fd);
# else /* DB_VERSION_MAJOR >= 2 */
	status = 0;
	fd = db->fd(db);
# endif /* DB_VERSION_MAJOR >= 2 */

	/* XXX -- allow multiple readers? */
	(void) pthread_mutex_lock(&pop_lock);

	if (status == 0 && fd != -1)
	{
# ifdef LOCK_SH
		status = flock(fd, LOCK_SH);

		if (status != 0 && dolog)
		{
			syslog(LOG_WARNING, "flock(LOCK_SH): %s",
			       strerror(errno));
		}
# else /* LOCK_SH */
		struct flock l;

		l.l_start = 0;
		l.l_len = 0;
		l.l_type = F_RDLCK;
		l.l_whence = SEEK_SET;

		status = fcntl(fd, F_SETLKW, &l);

		if (status != 0 && dolog)
		{
			syslog(LOG_WARNING, "fcntl(F_RDLCK): %s",
			       strerror(errno));
		}
# endif /* LOCK_SH */
	}

# if DB_VERSION_MAJOR < 2
	status = db->get(db, &q, &d, 0);
# else /* DB_VERSION_MAJOR < 2 */
	status = db->get(db, NULL, &q, &d, 0);
# endif /* DB_VERSION_MAJOR */
	if (status == 0)
	{
		ret = TRUE;
	}
	else if (status != DB_NOTFOUND && dolog)
	{
		syslog(LOG_ERR, "db->get(): %s", db_strerror(status));
	}

	/* surrender read-lock */
	if (fd != -1)
	{
# ifdef LOCK_SH
		status = flock(fd, LOCK_UN);

		if (status != 0 && dolog)
		{
			syslog(LOG_WARNING, "flock(LOCK_UN): %s",
			       strerror(errno));
		}
# else /* LOCK_SH */
		struct flock l;

		l.l_start = 0;
		l.l_len = 0;
		l.l_type = F_UNLCK;
		l.l_whence = SEEK_SET;

		status = fcntl(fd, F_SETLKW, &l);

		if (status != 0 && dolog)
		{
			syslog(LOG_WARNING, "fcntl(F_UNLCK): %s",
			       strerror(errno));
		}
# endif /* LOCK_SH */
	}

	(void) pthread_mutex_unlock(&pop_lock);

	return ret;
}
#endif /* POPAUTH */

/*
**  DKF_LOAD_LIST -- load a list of hosts/CIDR blocks into memory
**
**  Parameters:
**  	in -- input stream (must already be open)
**  	list -- list to be updated
**
**  Return value:
**  	TRUE if successful, FALSE otherwise
**
**  Side effects:
**  	Prints an error message when appropriate.
*/

bool
dkf_load_list(FILE *in, struct Peer **list)
{
	char *p;
	struct Peer *newpeer;
	char peer[BUFRSZ + 1];

	assert(in != NULL);
	assert(list != NULL);

	memset(peer, '\0', sizeof peer);

	while (fgets(peer, sizeof(peer) - 1, in) != NULL)
	{
		for (p = peer; *p != '\0'; p++)
		{
			if (*p == '\n')
			{
				*p = '\0';
				break;
			}
		}

		newpeer = (struct Peer *) malloc(sizeof(struct Peer));
		if (newpeer == NULL)
		{
			fprintf(stderr, "%s: malloc(): %s\n", progname,
			        strerror(errno));
			return FALSE;
		}

		newpeer->peer_next = *list;
		*list = newpeer;
		newpeer->peer_info = strdup(peer);
		if (newpeer->peer_info == NULL)
		{
			fprintf(stderr, "%s: strdup(): %s\n", progname,
			        strerror(errno));
			return FALSE;
		}
	}

	return TRUE;
}

/*
**  DKF_INET_NTOA -- thread-safe inet_ntoa()
**
**  Parameters:
**  	a -- (struct in_addr) to be converted
**  	buf -- destination buffer
**  	buflen -- number of bytes at buf
**
**  Return value:
**  	Size of the resultant string.  If the result is greater than buflen,
**  	then buf does not contain the complete result.
*/

size_t
dkf_inet_ntoa(struct in_addr a, char *buf, size_t buflen)
{
	uint32_t addr;

	assert(buf != NULL);

	addr = ntohl(a.s_addr);

	return snprintf(buf, buflen, "%u.%u.%u.%u",
	                (addr >> 24), (addr >> 16) & 0xff,
	                (addr >> 8) & 0xff, addr & 0xff);
}

/*
**  DKF_SPLITHDRS -- split up a "h=" stanza to look pretty
**
**  Parameters:
**  	buf -- input string
**  	buflen -- number of bytes at "buf"
**
**  Return value:
**  	None.
*/

void
dkf_splithdrs(char *buf, size_t buflen)
{
	bool first = TRUE;
	size_t len = 0;
	char *last;
	char *start;
	char *p;
	char tmp[MAXHEADERS];

	assert(buf != NULL);

	memset(tmp, '\0', sizeof tmp);

	len = 8;
	last = NULL;
	start = buf;

	for (p = buf; *p != '\0'; p++)
	{
		switch (*p)
		{
		  case ':':
			if (len + (size_t) (p - start) > MAXHDRLEN)
			{
				sm_strlcat(tmp, ":\n\t", sizeof tmp);
				len = 8;
			}
			else if (!first)
			{
				sm_strlcat(tmp, ":", sizeof tmp);
				len++;
			}
			first = FALSE;
			*p = '\0';
			sm_strlcat(tmp, start, sizeof tmp);
			*p = ':';
			start = NULL;
			break;

		  case '=':
			*p = '\0';
			sm_strlcat(tmp, start, sizeof tmp);
			*p = '=';
			sm_strlcat(tmp, "=", sizeof tmp);
			start = NULL;
			len++;
			break;

		  default:
			if (start == NULL)
				start = p;
			len++;
			break;
		}
	}

	if (start != NULL)
	{
		if (!first)
			sm_strlcat(tmp, ":", sizeof tmp);
		sm_strlcat(tmp, start, sizeof tmp);
	}

	sm_strlcpy(buf, tmp, buflen);
}

/*
**  DKF_TRIMSPACES -- trim trailing whitespace
**
**  Parameters:
**  	str -- string to modify
**
**  Return value:
**  	None.
*/

void
dkf_trimspaces(char *str)
{
	char *p;
	char *last;

	assert(str != NULL);

	last = NULL;

	for (p = str; *p != '\0'; p++)
	{
		if (isascii(*p) && isspace(*p) && last == NULL)
		{
			last = p;
			continue;
		}

		if (!isascii(*p) || !isspace(*p))
			last = NULL;
	}

	if (last != NULL)
		*last = '\0';
}
