/*
 * This module contains all generic functions used by the authentication
 * mechanism.
 *
 * Copyright (C) 1995 Olaf Kirch  <okir@monad.swb.de>
 *
 * This software maybe be used for any purpose provided
 * the above copyright notice is retained.  It is supplied
 * as is, with no warranty expressed or implied.
 */

#include <pwd.h>
#include <rpc/rpc.h>
#include "auth.h"

#define AUTH_DEBUG

static _PRO( int hostmatch, (const char *, const char *)		);
static _PRO( void auth_check_wildcards, (nfs_client *cp)		);
static _PRO( void auth_add_mountlist, (nfs_client *, nfs_mount *)	);
static _PRO( void auth_create_hashent, (nfs_client *, struct in_addr *)	);
static _PRO( void auth_free_list, (nfs_client **)			);
static _PRO( void auth_warn_anon, (void)				);

#if !defined(__GLIBC__)
/* It appears to be an old and long-standing tradition on Unices not
 * to declare the netgroup functions in any header file
 */
extern _PRO( int setnetgrent, (char *netgroup)				);
extern _PRO( int getnetgrent, (char **host, char **user, char **domain)	);
extern _PRO( int endnetgrent, (void)					);
#endif


#define IPHASH(a)	(((a)^((a)>>8)^((a)>>16)^((a)>>24)) & (IPHASHMAX-1))
#define IPHASHMAX	32

typedef struct nfs_hash_ent {
	struct nfs_hash_ent	*next;
	struct in_addr		addr;
	nfs_client		*client;
} nfs_hash_ent;

static nfs_hash_ent		*hashtable[IPHASHMAX];
static nfs_client		*known_clients = NULL;
static nfs_client		*unknown_clients = NULL;
static nfs_client		*wildcard_clients = NULL;
static nfs_client		*netgroup_clients = NULL;
static nfs_client		*netmask_clients = NULL;
static nfs_client		*anonymous_client = NULL;
static nfs_client		*default_client = NULL;
static int			initialized = 0;

static nfs_options		default_options = {
					identity,	/* uid mapping */
					1,		/* root squash */
					0,		/* all squash */
					1,		/* secure port */
					0,		/* read-only */
					0,		/* relative links */
					0,		/* noaccess */
					(uid_t)-2,	/* default uid */
					(gid_t)-2,	/* default gid */
				};

/*
 * Get a client entry for a specific name or pattern.
 * If necessary, this function performs a hostname lookup to
 * obtain the host's FQDN. This is for the benefit of those who
 * use aliases in /etc/exports, or mix qualified and unqualified
 * hostnames.
 *
 * FIXME: Make this function create the nfs_client entry if none
 * is found. Currently, this function is called only from auth_init
 * anyway, which creates the client entry if this function returns
 * NULL.
 */
nfs_client *
auth_get_client(hname)
char *hname;
{
	struct hostent	*hp;
	nfs_client	*cp;

	if (hname == NULL || *hname == '\0') {
		auth_warn_anon();
		return anonymous_client;
	}

	if (strchr(hname, '*') != NULL || strchr(hname, '?') != NULL) {
		for (cp = wildcard_clients; cp != NULL; cp = cp->next) {
			if (!strcmp(cp->clnt_name, hname))
				return cp;
		}
		return NULL;
	}

	for (cp = unknown_clients; cp != NULL; cp = cp->next) {
		if (!strcmp(cp->clnt_name, hname))
			return cp;
	}

	for (cp = known_clients; cp != NULL; cp = cp->next) {
		if (!strcmp(cp->clnt_name, hname))
			return cp;
	}

	if ((hp = gethostbyname(hname)) != NULL) {
		for (cp = known_clients; cp != NULL; cp = cp->next) {
			if (!strcmp(cp->clnt_name, hp->h_name))
				return cp;
		}
	}

	return NULL;
}

/*
 * Given a client and a pathname, try to find the proper mount point.
 * This code relies on the mount list being sorted from largest to
 * smallest w.r.t strcmp.
 */
nfs_mount *
auth_match_mount(cp, path)
nfs_client *cp;
char *path;
{
	nfs_mount	*mp;
	char		c;

	if (path == NULL)
		return NULL;

	for (mp = cp->m; mp != NULL; mp = mp->next) {
		if (!strncmp(mp->path, path, mp->length) &&
		    ((c = path[mp->length]) == '/' || c == '\0')) {
			return mp;
		}
	}
	return NULL;
}

/*
 * Find a known client given its IP address.
 * The matching hash entry is moved to the list head. This may be useful
 * for sites with large exports list (e.g. due to huge netgroups).
 */
nfs_client *
auth_known_clientbyaddr(addr)
struct in_addr	addr;
{
	nfs_hash_ent	**htp, *hep, *prv;

	htp = hashtable + IPHASH(addr.s_addr);
	hep = *htp;
	for (prv = NULL; hep != NULL; prv = hep, hep = hep->next) {
		if (hep->addr.s_addr == addr.s_addr) {
			if (prv != NULL) {
				prv->next = hep->next;
				hep->next = *htp;
				*htp = hep;
			}
			hep->client->clnt_addr = addr;
			return hep->client;
		}
	}
	return NULL;
}

/*
 * Find a known client given its FQDN.
 */
nfs_client *
auth_known_clientbyname(hname)
char *hname;
{
	nfs_client	*cp;

	if (hname == NULL)
		return NULL;

	for (cp = known_clients; cp != NULL; cp = cp->next) {
		if (!strcmp(cp->clnt_name, hname))
			return cp;
	}
	return NULL;
}

/*
 * Find an unknown client given its IP address. This functions checks
 * previously unresolved hostnames, wildcard hostnames, the anon client,
 * and the default client.
 */
nfs_client *
auth_unknown_clientbyaddr(addr)
struct in_addr addr;
{
	struct hostent	*hp;
	nfs_client	*cp, *ncp;
	char		*hname;

	hp = gethostbyaddr((char *)&addr, sizeof(addr), AF_INET);

	for (cp = netmask_clients; cp != NULL; cp = cp->next) {
		if (!((addr.s_addr^cp->clnt_addr.s_addr)&cp->clnt_mask.s_addr))
			break;
	}
	if (cp != NULL) {
		ncp = auth_create_client(hp? hp->h_name : inet_ntoa(addr), hp);
		auth_add_mountlist(ncp, cp->m);
		auth_check_wildcards(ncp);
		return ncp;
	}

	if (hp != NULL) {
		nfs_client **cpp;
		char	**ap;

		/* So, this is ugly, but we don't do it that often. And it's
		 * better than fixed size strings
		 */
		hname = malloc(strlen(hp->h_name) + 1);
		strcpy (hname, hp->h_name);
		hp = gethostbyname(hname);
		free (hname);
		hname = (char *) hp->h_name;

		if (hp == NULL) {
			goto anonymous;
		}
		for (ap = hp->h_addr_list; *ap != NULL; ap++) {
			if (!memcmp(*ap, (char *)&addr, hp->h_length))
				break;
		}

		/* Okay, so this is a spoof attempt. */
		if (*ap == NULL) {
			goto anonymous;
		}

		/* First, check for clients that couldn't be resolved during
		 * initialization */
		for (cpp = &unknown_clients; *cpp!=NULL; cpp = &(*cpp)->next) {
			if (!strcmp((*cpp)->clnt_name, hp->h_name)) {
				nfs_client *cp;

				cp = *cpp;
				cp->clnt_addr = addr;

				/* remove client from list of unknown and
				 * add it to list of known hosts.
				 */
				(*cpp) = (*cpp)->next;
				cp->next = known_clients;
				known_clients = cp;

				/* Add host to hashtable */
				for (ap = hp->h_addr_list; *ap != NULL; ap++) {
					auth_create_hashent(cp, 
							(struct in_addr *)*ap);
				}

				return cp;
			}
		}

		/* Okay, now check for wildcard names. NB the wildcard
		 * patterns are sorted from most to least specific.
		 *
		 * The pattern matching should also be applied to
		 * all names in h_aliases.
		 */
		for (cpp = &wildcard_clients; *cpp!=NULL; cpp = &(*cpp)->next) {
			if (hostmatch(hp->h_name, (*cpp)->clnt_name)) {
				nfs_client *cp;

				cp = auth_create_client(hp->h_name, hp);
				auth_check_wildcards(cp);
				return cp;
			}
		}
	} else {
		hname = inet_ntoa(addr);
	}

anonymous:
	if (anonymous_client != NULL) {
		cp = anonymous_client;
	} else
		cp = default_client;

	if (cp) {
		ncp = auth_create_client(hname, hp);
		auth_add_mountlist(ncp, cp->m);
		return ncp;
	}
	return NULL;
}

/*
 * Create a client struct for the given hostname. The hp parameter
 * optionally contains hostent information obtained from a previous
 * gethostbyname.
 */
nfs_client *
auth_create_client(hname, hp)
const char *hname;
struct hostent *hp;
{
	nfs_client	*cp, **cpp;
	int		wildcards, netgroup, netmask, namelen;

	cp = (nfs_client *) xmalloc(sizeof(nfs_client));

	cp->clnt_addr.s_addr = INADDR_ANY;
	cp->flags = 0;
	cp->m = NULL;
	cp->umap = NULL;

	if (hname == NULL) {
		if (anonymous_client != NULL) {
			free (cp);
			cp = anonymous_client;
		} else {
			anonymous_client = cp;
		}
		cp->clnt_name = NULL;
		cp->next = NULL;
		cp->flags = AUTH_CLNT_ANONYMOUS;
		return cp;
	}

	wildcards = (strchr(hname, '*') != NULL || strchr(hname, '?') != NULL);
	netgroup = (hname[0] == '@');
	netmask = (strchr(hname, '/') != NULL);

	if (!wildcards && !netgroup && !netmask && hp == NULL) {
		hp = gethostbyname(hname);
	}
	if (hp != NULL) {
		if (hp->h_addrtype != AF_INET) {
			free (cp);
			return NULL;
		}
		hname = hp->h_name;	/* use FQDN */
	}

	cp->clnt_name = xstrdup(hname);

	if (wildcards) {
		/* We have a wildcard name. Wildcard names are sorted
		 * in order of descending pattern length. This way,
		 * pattern *.pal.xgw.fi is matched before *.xgw.fi.
		 */
		cp->flags = AUTH_CLNT_WILDCARD;
		cpp = &wildcard_clients;
		namelen = strlen(hname);
		while (*cpp != NULL && namelen <= strlen((*cpp)->clnt_name)) {
			cpp = &((*cpp)->next);
		}
	} else if (netgroup) {
		/* Netgroup name. These are added to a separate list. This
		 * list won't be consulted, but we have to keep the struct
		 * around because of the way exports parsing is done.
		 */
		cp->flags = AUTH_CLNT_NETGROUP;
		cpp = &netgroup_clients;
	} else if (netmask) {
		/* Address/mask pair. */
		char	*slashp = strchr(hname, '/');
		cp->flags = AUTH_CLNT_NETMASK;
		cpp = &netmask_clients;
		*slashp = '\0';
		cp->clnt_mask.s_addr = inet_addr(slashp+1);
		cp->clnt_addr.s_addr = inet_addr(hname);
		if (cp->clnt_addr.s_addr == -1 || cp->clnt_mask.s_addr == -1) {
			*slashp = '/';
		}
	} else if (hp == NULL) {
		cpp = &unknown_clients;
	} else {
		char	**ap;

		cpp = &known_clients;
		for (ap = hp->h_addr_list; *ap != NULL; ap++) {
			auth_create_hashent(cp, (struct in_addr *)*ap);
		}
	}
	cp->next = *cpp;
	*cpp = cp;

	return cp;
}

/*
 * Create the default client.
 */
nfs_client *
auth_create_default_client()
{
	nfs_client	*cp;

	if (default_client == NULL) {
		cp = (nfs_client *) xmalloc(sizeof(nfs_client));
		cp->clnt_name = NULL;
		cp->next = NULL;
		cp->flags = AUTH_CLNT_DEFAULT;
		cp->m = NULL;
		default_client = cp;
	}
	auth_warn_anon();
	return default_client;
}

static void
auth_warn_anon()
{
	static int	warned = 0;
	struct hostent	*hp;
	char		name[257];
	char		**ap;

	if (warned)
		return;
	warned = 1;

	if (gethostname(name, sizeof(name)) < 0) {
		return;
	}
	if ((hp = gethostbyname(name)) == NULL) {
		return;
	}
	if (hp->h_addrtype != AF_INET) {
		return;
	}
	for (ap = hp->h_addr_list; *ap; ap++) {
		struct in_addr	addr;
		unsigned long	net3, net2;

		addr = *(struct in_addr *) *ap;
		net3 = ntohl(addr.s_addr) & 0xff000000;
		net2 = ntohl(addr.s_addr) & 0x00ff0000;
		if (net3 == 0x0A000000 ||
		   (net3 == 0xAC000000 && 0x100000 <= net2 && net2 < 0x200000)||
		   (net3 == 0XC0000000 && 0xA80000 == net2))
			continue;
	}
}

/*
 * Create an entry in the hashtable of known clients.
 */
void
auth_create_hashent(cp, ap)
nfs_client	*cp;
struct in_addr	*ap;
{
	nfs_hash_ent	*hep;
	int		hash;

	hash = IPHASH(ap->s_addr);

	hep = (nfs_hash_ent *) xmalloc(sizeof(*hep));
	hep->client = cp;
	hep->addr = *ap;
	hep->next = hashtable[hash];
	hashtable[hash] = hep;
}

/*
 * After reading the entire exports file, this routine checks if any
 * of the known clients match any of the patterns in wildcard_clients.
 * If one does, the wildcard's mount points are added to its list of
 * mount points.
 */
void
auth_check_all_wildcards(void)
{
	nfs_client	*cp;

	for (cp = known_clients; cp != NULL; cp = cp->next) {
		auth_check_wildcards(cp);
	}
	for (cp = unknown_clients; cp != NULL; cp = cp->next) {
		auth_check_wildcards(cp);
	}
}

static void
auth_check_wildcards(cp)
nfs_client *cp;
{
	nfs_client	*wcp;
	nfs_mount	*mp;

	for (wcp = wildcard_clients; wcp != NULL; wcp = wcp->next) {
		if (hostmatch(cp->clnt_name, wcp->clnt_name)) {
			auth_add_mountlist(cp, wcp->m);
		}
	}
	if (anonymous_client != NULL) {
		auth_add_mountlist(cp, anonymous_client->m);
	}
}

/*
 * Check all client structs that apply to a netgroup
 */
void
auth_check_all_netgroups(void)
{
#ifdef HAVE_GETNETGRENT
	nfs_client	*ncp, *cp;

	for (ncp = netgroup_clients; ncp != NULL; ncp = ncp->next) {
		char	*hname, *user, *domain;

		setnetgrent(ncp->clnt_name + 1);
		while (getnetgrent(&hname, &user, &domain)) {
			if (hname == NULL || *hname == '\0' || !strcmp(hname, "-"))
				continue;
			if ((cp = auth_get_client(hname)) == NULL &&
			    (cp = auth_create_client(hname, NULL)) == NULL)
				continue;
			auth_add_mountlist(cp, ncp->m);
		}
		endnetgrent();
	}
#endif
}

/*
 * Check all client structs that match an addr/mask pair
 */
void
auth_check_all_netmasks(void)
{
	nfs_client	*ncp, *cp;
	nfs_hash_ent	*hp;
	int		i, match;

	for (ncp = netmask_clients; ncp != NULL; ncp = ncp->next) {
		for (i = 0; i < IPHASHMAX; i++) {
			for (hp = hashtable[i]; hp != NULL; hp = hp->next) {
				match = ((hp->addr.s_addr 
				        ^ ncp->clnt_addr.s_addr)
			                & ncp->clnt_mask.s_addr) == 0;
				if (match) {
					cp = hp->client;
					auth_add_mountlist(cp, ncp->m);
				}
			}
		}
	}
}

/*
 * Add a mount point to a client. Mount points are sorted from most
 * specific to least specific.
 */
nfs_mount *
auth_add_mount(cp, path)
nfs_client *cp;
char *path;
{
	nfs_mount	*mp, **mpp;
	int		len;

	len = strlen(path);

	mp = (nfs_mount*) xmalloc(sizeof(nfs_mount));
	memcpy (&mp->o, &default_options, sizeof(nfs_options));
	mp->client = cp;

	mp->path = xstrdup(path);
	while (len && mp->path[len-1] == '/')
		mp->path[--len] = '\0';
	mp->length = len;

	/* insert mount point into list of mounts. Insert more specific
	 * path before less specific path. For equal mount points, the
	 * first one entered wins so that exports entries like
	 *
	 * /foo/bar	(ro) host1(rw,no_root_squash)
	 *
	 * do the intuitive thing.
	 */
	for (mpp = &(cp->m); *mpp != NULL; mpp = &(*mpp)->next) {
		if (strcmp((*mpp)->path, path) < 0)
			break;
	}

	mp->next = *mpp;
	*mpp = mp;

	return mp;
}

/*
 * Add a list of mount points to an existing client. This code looks
 * somewhat sub-optimal, but we have to make sure the overall order
 * of mount points is preserved (i.e. most specific to least specific).
 * Few Linux machines will have more than a dozen or so paths in their
 * exports file anyway.
 */
static void
auth_add_mountlist(cp, mp)
nfs_client *cp;
nfs_mount *mp;
{
    nfs_mount	*nmp;

    while (mp != NULL) {
	nmp = auth_add_mount(cp, mp->path);
	memcpy (&(nmp->o), &(mp->o), sizeof(nfs_options));

	mp = mp->next;
    }
}

/*
 * Match a hostname against a pattern.
 */
static int
hostmatch(hname, pattern)
const char *hname;
const char *pattern;
{
	int seen_dot = 0;


	for (;;) {
		if (*hname == '\0' || *pattern == '\0')
			return (*hname == *pattern);
		switch (*pattern) {
		case '*':
			while (*hname != '.' && *hname != '\0')
				hname++;
			seen_dot = 1;
			pattern++;
			break;
		case '?':
			if (*hname == '.')
				return (0);
			hname++;
			pattern++;
			break;
		default:
			if (seen_dot) {
				if (tolower(*hname) != tolower(*pattern))
					return (0);
			}
			else if (*hname != *pattern)
				return (0);
			if (*pattern == '.')
				seen_dot = 1;
			hname++;
			pattern++;
			break;
		}
	}
}

/*
 * Initialize hash table. If the auth module has already been initialized, 
 * free all list entries first.
 */
void
auth_init_lists()
{
	struct passwd	*pw;
	int		i;
	uid_t		anon_uid;
	gid_t		anon_gid;

	if (initialized) {
		nfs_hash_ent	*hep, *next;

		auth_free_list(&known_clients);
		auth_free_list(&unknown_clients);
		auth_free_list(&wildcard_clients);
		auth_free_list(&netgroup_clients);
		auth_free_list(&anonymous_client);
		auth_free_list(&default_client);

		for (i = 0; i < IPHASHMAX; i++) {
			for (hep = hashtable[i]; hep != NULL; hep = next) {
				next = hep->next;
				free (hep);
			}
			hashtable[i] = NULL;
		}
	} else {
		for (i = 0; i < IPHASHMAX; i++) {
			hashtable[i] = NULL;
		}
	}

	/* Get the default anon uid/gid */
	if ((pw = getpwnam("nobody")) != NULL) {
		anon_uid = pw->pw_uid;
		anon_gid = pw->pw_gid;
	} else {
		anon_uid = (uid_t) -2;
		anon_gid = (gid_t) -2;
	}

	/* This protects us from stomping all over the place on installations
	 * that have given nobody a uid/gid of -1. This is quite bad for
	 * systems that don't have setfsuid, because seteuid(-1) is a no-op.
	 */
	if (anon_uid == (uid_t)-1) {
		anon_uid = (uid_t) -2;
	}
	if (anon_gid == (gid_t)-1) {
		anon_gid = (gid_t) -2;
	}

	default_options.nobody_uid = anon_uid;
	default_options.nobody_gid = anon_gid;

	initialized = 1;
}

/*
 * Free all members on a list of nfs_clients.
 */
static void
auth_free_list(cpp)
nfs_client	**cpp;
{
	nfs_client	*cp, *nxt_clnt;
	nfs_mount	*mp, *nxt_mp;

	for (cp = *cpp; cp != NULL; cp = nxt_clnt) {
		nxt_clnt = cp->next;
		if (cp->clnt_name != NULL) {
			free (cp->clnt_name);
		}
		for (mp = cp->m; mp != NULL; mp = nxt_mp) {
			nxt_mp = mp->next;
			free (mp->path);
			free (mp);
		}
		if (cp->umap != NULL) {
			ugid_free_map(cp->umap);
		}
		free (cp);
	}
	*cpp = NULL;
}

