/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2003 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * Routines to handle getpw* calls in nscd
 */

#include <assert.h>
#include <errno.h>
#include <memory.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/door.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <thread.h>
#include <unistd.h>
#include <nss_common.h>
#include <ucred.h>

#include "getxby_door.h"
#include "server_door.h"

#include "nscd.h"

static hash_t *uid_hash;
static hash_t *nam_hash;
static mutex_t  passwd_lock = DEFAULTMUTEX;
static waiter_t passwd_wait;

static void getpw_invalidate_unlocked(void);
static void getpw_namekeepalive(int keep, int interval);
static void getpw_uidkeepalive(int keep, int interval);
static void update_pw_bucket(nsc_bucket_t **old, nsc_bucket_t *new,
    int callnumber);
static nsc_bucket_t *fixbuffer(nsc_return_t *in, int maxlen);
static void do_findnams(nsc_bucket_t *ptr, int *table, char *name);
static void do_finduids(nsc_bucket_t *ptr, int *table, int uid);
static void do_invalidate(nsc_bucket_t **ptr, int callnumber);

void
getpw_init(void)
{
	uid_hash = make_ihash(current_admin.passwd.nsc_suggestedsize);
	nam_hash = make_hash(current_admin.passwd.nsc_suggestedsize);
}

static void
do_invalidate(nsc_bucket_t ** ptr, int callnumber)
{
	if (*ptr != NULL && *ptr != (nsc_bucket_t *)-1) {
		/* leave pending calls alone */
		update_pw_bucket(ptr, NULL, callnumber);
	}
}

static void
do_finduids(nsc_bucket_t *ptr, int *table, int uid)
{

	/*
	 * be careful with ptr - it may be -1 or NULL.
	 */
	if (ptr != NULL && ptr != (nsc_bucket_t *)-1) {
		insertn(table, ptr->nsc_hits, uid);
	}
}

static void
do_findnams(nsc_bucket_t *ptr, int *table, char *name)
{

	/*
	 * be careful with ptr - it may be -1 or NULL.
	 */
	if (ptr != NULL && ptr != (nsc_bucket_t *)-1) {
		char *tmp = (char *)insertn(table, ptr->nsc_hits,
			(int)strdup(name));
		if (tmp != (char *)-1)
			free(tmp);
	}
}



void
getpw_revalidate(void)
{
	for (;;) {
		int slp;
		int interval;
		int count;

		slp = current_admin.passwd.nsc_pos_ttl;

		if (slp < 60) {
			slp = 60;
		}

		if ((count = current_admin.passwd.nsc_keephot) != 0) {
			interval = (slp / 2)/count;
			if (interval == 0) interval = 1;
			sleep(slp * 2 / 3);
			getpw_uidkeepalive(count, interval);
			getpw_namekeepalive(count, interval);
		} else {
			sleep(slp);
		}
	}
}

static void
getpw_uidkeepalive(int keep, int interval)
{
	int *table;
	nsc_data_t  ping;
	int i;

	if (!keep)
		return;

	table = maken(keep);
	mutex_lock(&passwd_lock);
	operate_hash(uid_hash, do_finduids, (char *)table);
	mutex_unlock(&passwd_lock);

	for (i = 1; i <= keep; i++) {
	    ping.nsc_call.nsc_callnumber = GETPWUID;
	    if ((ping.nsc_call.nsc_u.uid = table[keep + 1 + i]) == -1)
		continue; /* unused slot in table */
	    launch_update(&ping.nsc_call);
	    sleep(interval);
	}
	free(table);
}


static void
getpw_namekeepalive(int keep, int interval)
{
	int *table;
	union {
		nsc_data_t  ping;
		char space[sizeof (nsc_data_t) + NSCDMAXNAMELEN];
	} u;

	int i;

	if (!keep)
		return;

	table = maken(keep);
	mutex_lock(&passwd_lock);
	operate_hash(nam_hash, do_findnams, (char *)table);
	mutex_unlock(&passwd_lock);

	for (i = 1; i <= keep; i++) {
		char *tmp;
		u.ping.nsc_call.nsc_callnumber = GETPWNAM;

		if ((tmp = (char *)table[keep + 1 + i]) == (char *)-1)
			continue; /* unused slot in table */

		strcpy(u.ping.nsc_call.nsc_u.name, tmp);

		launch_update(&u.ping.nsc_call);
		sleep(interval);
	}

	for (i = 1; i <= keep; i++) {
		char *tmp;
		if ((tmp = (char *)table[keep + 1 + i]) != (char *)-1)
			free(tmp);
	}

	free(table);
}




/*
 *   This routine marks all entries as invalid
 *
 */
void
getpw_invalidate(void)
{
	mutex_lock(&passwd_lock);
	getpw_invalidate_unlocked();
	mutex_unlock(&passwd_lock);
}

static void
getpw_invalidate_unlocked(void)
{
	operate_hash_addr(nam_hash, do_invalidate, (char *)GETPWNAM);
	operate_hash_addr(uid_hash, do_invalidate, (char *)GETPWUID);
	current_admin.passwd.nsc_invalidate_count++;
}

void
getpw_lookup(nsc_return_t *out, int maxsize, nsc_call_t *in, time_t now)
{
	int		out_of_date;
	nsc_bucket_t	*retb;
	char 		**bucket;

	static time_t	lastmod;

	int bufferspace = maxsize - sizeof (nsc_return_t);

	if (current_admin.passwd.nsc_enabled == 0) {
		out->nsc_return_code = NOSERVER;
		out->nsc_bufferbytesused = sizeof (*out);
		return;
	}

	mutex_lock(&passwd_lock);

	if (current_admin.passwd.nsc_check_files) {
		struct stat buf;

		if (stat("/etc/passwd", &buf) < 0) {
			/*EMPTY*/;
		} else if (lastmod == 0) {
			lastmod = buf.st_mtime;
		} else if (lastmod < buf.st_mtime) {
			getpw_invalidate_unlocked();
			lastmod = buf.st_mtime;
		}
	}

	if (current_admin.debug_level >= DBG_ALL) {
		if (MASKUPDATEBIT(in->nsc_callnumber) == GETPWUID) {
			logit("getpw_lookup: looking for uid %d\n",
				in->nsc_u.uid);
		} else {
			logit("getpw_lookup: looking for name %s\n",
				in->nsc_u.name);
		}
	}

	for (;;) {
		if (MASKUPDATEBIT(in->nsc_callnumber) == GETPWUID) {
			bucket = get_hash(uid_hash, (char *)in->nsc_u.uid);
		} else { /* make reasonableness check here  */
			if (strlen(in->nsc_u.name) > NSCDMAXNAMELEN) {
				ucred_t *uc = NULL;

				if (door_ucred(&uc) != 0) {
					logit("getpw_lookup: Name too long, "
					    "but no user credential: %s\n",
					    strerror(errno));
				} else {

					logit("getpw_lookup: Name too long "
					    "from pid %d uid %d\n",
					    ucred_getpid(uc),
					    ucred_getruid(uc));
					ucred_free(uc);
				}


				out->nsc_errno = NSS_NOTFOUND;
				out->nsc_return_code = NOTFOUND;
				out->nsc_bufferbytesused = sizeof (*out);
				goto getout;
			}
			bucket = get_hash(nam_hash, in->nsc_u.name);
		}

		if (*bucket == (char *)-1) {	/* pending lookup */
			if (get_clearance(in->nsc_callnumber) != 0) {
				/* no threads available */
				out->nsc_return_code = NOSERVER;
				/* cannot process now */
				out->nsc_bufferbytesused = sizeof (*out);
				current_admin.passwd.nsc_throttle_count++;
				goto getout;
			}
			nscd_wait(&passwd_wait, &passwd_lock, bucket);
			release_clearance(in->nsc_callnumber);
			continue; /* go back and relookup hash bucket */
		}
		break;
	}

	/*
	 * check for no name_service mode
	 */

	if (*bucket == NULL && current_admin.avoid_nameservice) {
		out->nsc_return_code = NOTFOUND;
		out->nsc_bufferbytesused = sizeof (*out);
	} else if (*bucket == NULL ||
	    (in->nsc_callnumber & UPDATEBIT) ||
	    (out_of_date = (!current_admin.avoid_nameservice &&
		(current_admin.passwd.nsc_old_data_ok == 0) &&
		(((nsc_bucket_t *)*bucket)->nsc_timestamp < now)))) {
		/*
		 * time has expired
		 */
		int saved_errno;
		int saved_hits = 0;
		struct passwd *p;

		if (get_clearance(in->nsc_callnumber) != 0) {
			/* no threads available */
			out->nsc_return_code = NOSERVER;
			/* cannot process now */
			out->nsc_bufferbytesused = sizeof (*out);
			current_admin.passwd.nsc_throttle_count++;
			goto getout;
		}
		if (*bucket != NULL) {
			saved_hits = ((nsc_bucket_t *)*bucket)->nsc_hits;
		}

		/*
		 *  block any threads accessing this bucket if data
		 *  is non-existent or out of date
		 */

		if (*bucket == NULL || out_of_date) {
			update_pw_bucket((nsc_bucket_t **)bucket,
					(nsc_bucket_t *)-1,
					in->nsc_callnumber);
		} else {
			/*
			 * if still not -1 bucket we are doing
			 * update... mark to prevent pileups of threads if
			 * the name service is hanging..
			 */
			((nsc_bucket_t *)(*bucket))->nsc_status |=
				ST_UPDATE_PENDING;
			/* cleared by deletion of old data */
		}
		mutex_unlock(&passwd_lock);

		if (MASKUPDATEBIT(in->nsc_callnumber) == GETPWUID) {
			p = _uncached_getpwuid_r(in->nsc_u.uid, &out->nsc_u.pwd,
				out->nsc_u.buff+sizeof (struct passwd),
				bufferspace);
			saved_errno = errno;
		} else {
			p = _uncached_getpwnam_r(in->nsc_u.name,
				&out->nsc_u.pwd,
				out->nsc_u.buff+sizeof (struct passwd),
				bufferspace);
			saved_errno = errno;
		}

		mutex_lock(&passwd_lock);

		release_clearance(in->nsc_callnumber);

		if (p == NULL) { /* data not found */
			if (current_admin.debug_level >= DBG_CANT_FIND) {
				if (MASKUPDATEBIT(in->nsc_callnumber) ==
					GETPWUID) {
			logit("getpw_lookup: nscd COULDN'T FIND uid %d\n",
					in->nsc_u.uid);
				} else {
		logit("getpw_lookup: nscd COULDN'T FIND passwd name %s\n",
						in->nsc_u.name);
				}
			}

			if (!(UPDATEBIT & in->nsc_callnumber))
			    current_admin.passwd.nsc_neg_cache_misses++;

			retb = (nsc_bucket_t *)malloc(sizeof (nsc_bucket_t));

			retb->nsc_refcount = 1;
			retb->nsc_data.nsc_bufferbytesused =
				sizeof (nsc_return_t);
			retb->nsc_data.nsc_return_code = NOTFOUND;
			retb->nsc_data.nsc_errno = saved_errno;
			memcpy(out, &retb->nsc_data,
				retb->nsc_data.nsc_bufferbytesused);
			update_pw_bucket((nsc_bucket_t **)bucket, retb,
				in->nsc_callnumber);
			goto getout;
		} else {
			if (current_admin.debug_level >= DBG_ALL) {
				if (MASKUPDATEBIT(in->nsc_callnumber) ==
					GETPWUID) {
				logit("getpw_lookup: nscd FOUND uid %d\n",
						in->nsc_u.uid);
				} else {
			logit("getpw_lookup: nscd FOUND passwd name %s\n",
						in->nsc_u.name);
				}
			}
			if (!(UPDATEBIT & in->nsc_callnumber))
			    current_admin.passwd.nsc_pos_cache_misses++;

			retb = fixbuffer(out, bufferspace);
			update_pw_bucket((nsc_bucket_t **)bucket,
				retb, in->nsc_callnumber);
			if (saved_hits)
				retb->nsc_hits = saved_hits;
		}
	} else { 	/* found entry in cache */
		retb = (nsc_bucket_t *)*bucket;

		retb->nsc_hits++;

		memcpy(out, &(retb->nsc_data),
			retb->nsc_data.nsc_bufferbytesused);

		if (out->nsc_return_code == SUCCESS) {
			if (!(UPDATEBIT & in->nsc_callnumber))
			    current_admin.passwd.nsc_pos_cache_hits++;
			if (current_admin.debug_level >= DBG_ALL) {
				if (MASKUPDATEBIT(in->nsc_callnumber) ==
					GETPWUID) {
			logit("getpw_lookup: found uid %d in cache\n",
						in->nsc_u.uid);
				} else {
			logit("getpw_lookup: found name %s in cache\n",
						in->nsc_u.name);
				}
			}
		} else {
			if (!(UPDATEBIT & in->nsc_callnumber))
			    current_admin.passwd.nsc_neg_cache_hits++;
			if (current_admin.debug_level >= DBG_ALL) {
				if (MASKUPDATEBIT(in->nsc_callnumber) ==
					GETPWUID) {
		logit("getpw_lookup: %d marked as NOT FOUND in cache.\n",
						in->nsc_u.uid);
				} else {
		logit("getpw_lookup: %s marked as NOT FOUND in cache.\n",
						in->nsc_u.name);
				}
			}
		}

		if ((retb->nsc_timestamp < now) &&
			!(in->nsc_callnumber & UPDATEBIT) &&
			!(retb->nsc_status & ST_UPDATE_PENDING)) {
			logit("launch update since time = %d\n",
				retb->nsc_timestamp);
			retb->nsc_status |= ST_UPDATE_PENDING;
			/* cleared by deletion of old data */
			launch_update(in);
		}
	}

getout:

	mutex_unlock(&passwd_lock);

	/*
	 *	secure mode check - blank out passwd if call sucessfull
	 *	and caller != effective id
	 */
	if ((current_admin.passwd.nsc_secure_mode != 0) &&
		(out->nsc_return_code == SUCCESS) &&
		!(UPDATEBIT & in->nsc_callnumber)) {

		ucred_t *uc = NULL;

		if (door_ucred(&uc) != 0) {
			perror("door_ucred");
		} else {
			if (ucred_geteuid(uc) != out->nsc_u.pwd.pw_uid) {
				/*
				 *  write *NP* into passwd field if
				 *  not already that way... we fixed
				 *  the buffer code so there's always room.
				 */
				int len;

				char *foo = out->nsc_u.buff
					+ sizeof (struct passwd)
					+ (int)out->nsc_u.pwd.pw_passwd;

				len = strlen(foo);
				if (len > 0 &&
				    strcmp(foo, "*NP*") != 0 &&
				    strcmp(foo, "x") != 0) {
					if (len < 5)
						len = 5;
					strncpy(foo, "*NP*", len);
					/*
					 * strncpy will
					 * blank all
					 */
				}
			}
			ucred_free(uc);
		}
	}
}

/*ARGSUSED*/
static void
update_pw_bucket(nsc_bucket_t **old, nsc_bucket_t *new, int callnumber)
{
	if (*old != NULL && *old != (nsc_bucket_t *)-1) {
		/* old data exists */
		free(*old);
		current_admin.passwd.nsc_entries--;
	}

	/*
	 *  we can do this before reseting *old since we're holding the lock
	 */

	else if (*old == (nsc_bucket_t *)-1) {
		nscd_signal(&passwd_wait, (char **)old);
	}



	*old = new;

	if ((new != NULL) &&
		(new != (nsc_bucket_t *)-1)) {
		/* real data, not just update pending or invalidate */

		new->nsc_hits = 1;
		new->nsc_status = 0;
		new->nsc_refcount = 1;
		current_admin.passwd.nsc_entries++;

		if (new->nsc_data.nsc_return_code == SUCCESS) {
			new->nsc_timestamp = time(NULL) +
				current_admin.passwd.nsc_pos_ttl;
		} else {
			new->nsc_timestamp = time(NULL) +
				current_admin.passwd.nsc_neg_ttl;
		}
	}
}


/*ARGSUSED*/
static nsc_bucket_t *
fixbuffer(nsc_return_t *in, int maxlen)
{
	nsc_bucket_t *retb;
	char *dest;

	nsc_return_t  *out;
	int offset;
	int strs;
	int pwlen;

	/*
	 * find out the size of the data block we're going to need
	 */

	strs = 0;
	strs += 1 + strlen(in->nsc_u.pwd.pw_name);
	pwlen = strlen(in->nsc_u.pwd.pw_passwd);
	if (pwlen < 4)
	    pwlen = 4;
	strs += 1 + pwlen;
	strs += 1 + strlen(in->nsc_u.pwd.pw_age);
	strs += 1 + strlen(in->nsc_u.pwd.pw_comment);
	strs += 1 + strlen(in->nsc_u.pwd.pw_gecos);
	strs += 1 + strlen(in->nsc_u.pwd.pw_dir);
	strs += 1 + strlen(in->nsc_u.pwd.pw_shell);


	/*
	 * allocate it and copy it in
	 * code doesn't assume packing order in original buffer
	 */

	if ((retb = (nsc_bucket_t *)malloc(sizeof (*retb) + strs)) == NULL) {
		return (NULL);
	}

	out = &(retb->nsc_data);



	out->nsc_bufferbytesused = sizeof (*in) + strs;
	out->nsc_return_code 	= SUCCESS;
	out->nsc_errno 		= 0;

	out->nsc_u.pwd.pw_uid = in->nsc_u.pwd.pw_uid;
	out->nsc_u.pwd.pw_gid = in->nsc_u.pwd.pw_gid;

	dest = retb->nsc_data.nsc_u.buff + sizeof (struct passwd);

	offset = (int)dest;

	strcpy(dest, in->nsc_u.pwd.pw_name);
	strs = 1 + strlen(in->nsc_u.pwd.pw_name);
	out->nsc_u.pwd.pw_name = dest - offset;
	dest += strs;

	strcpy(dest, in->nsc_u.pwd.pw_passwd);
	strs = 1 + pwlen;
	out->nsc_u.pwd.pw_passwd = dest - offset;
	dest += strs;

	strcpy(dest, in->nsc_u.pwd.pw_age);
	strs = 1 + strlen(in->nsc_u.pwd.pw_age);
	out->nsc_u.pwd.pw_age = dest - offset;
	dest += strs;

	strcpy(dest, in->nsc_u.pwd.pw_comment);
	strs = 1 + strlen(in->nsc_u.pwd.pw_comment);
	out->nsc_u.pwd.pw_comment = dest - offset;
	dest += strs;

	strcpy(dest, in->nsc_u.pwd.pw_gecos);
	strs = 1 + strlen(in->nsc_u.pwd.pw_gecos);
	out->nsc_u.pwd.pw_gecos = dest - offset;
	dest += strs;

	strcpy(dest, in->nsc_u.pwd.pw_dir);
	strs = 1 + strlen(in->nsc_u.pwd.pw_dir);
	out->nsc_u.pwd.pw_dir = dest - offset;
	dest += strs;

	strcpy(dest, in->nsc_u.pwd.pw_shell);
	out->nsc_u.pwd.pw_shell = dest - offset;

	memcpy(in, out, retb->nsc_data.nsc_bufferbytesused);


	return (retb);

}

void
getpw_uid_reaper()
{
	nsc_reaper("getpw_uid", uid_hash, &current_admin.passwd, &passwd_lock);
}

void
getpw_nam_reaper()
{
	nsc_reaper("getpw_nam", nam_hash, &current_admin.passwd, &passwd_lock);
}