/*	Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T	*/
/*	  All Rights Reserved  	*/

/*	THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF AT&T	*/
/*	The copyright notice above does not evidence any   	*/
/*	actual or intended publication of such source code.	*/

#ident	"@(#)sm_svc.c	1.38	99/04/27 SMI"	/* SVr4.0 1.2	*/
/*
 *  		PROPRIETARY NOTICE (Combined)
 *
 *  This source code is unpublished proprietary information
 *  constituting, or derived under license from AT&T's Unix(r) System V.
 *  In addition, portions of such source code were derived from Berkeley
 *  4.3 BSD under license from the Regents of the University of
 *  California.
 *
 *
 *
 *  		Copyright Notice
 *
 *  Notice of copyright on this source code product does not indicate
 *  publication.
 *
 *  	(c) 1986-1989,1994-1996,1998,1999  Sun Microsystems, Inc.
 *  	(c) 1983,1984,1985,1986,1987,1988,1989  AT&T.
 *		All rights reserved.
 */
#include <stdio.h>
#include <stdlib.h>
#include <ftw.h>
#include <signal.h>
#include <string.h>
#include <syslog.h>
#include <netconfig.h>
#include <unistd.h>
#include <netdb.h>
#include <rpc/rpc.h>
#include <netinet/in.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sockio.h>
#include <dirent.h>
#include <errno.h>
#include <rpcsvc/sm_inter.h>
#include <rpcsvc/nsm_addr.h>
#include <thread.h>
#include <synch.h>
#include <net/if.h>
#include "sm_statd.h"


#define	home0		"/var/statmon"
#define	current0	"/var/statmon/sm"
#define	backup0		"/var/statmon/sm.bak"
#define	state0		"/var/statmon/state"

#define	home1		"statmon"
#define	current1	"statmon/sm/"
#define	backup1		"statmon/sm.bak/"
#define	state1		"statmon/state"

/*
 * User and group IDs to run as.  These are hardwired, rather than looked
 * up at runtime, because they are very unlikely to change and because they
 * provide some protection against bogus changes to the passwd and group
 * files.
 */

#define	DAEMON_USER	1
#define	DAEMON_GROUP	12

char STATE[MAXPATHLEN], CURRENT[MAXPATHLEN], BACKUP[MAXPATHLEN];
static char statd_home[MAXPATHLEN];

int debug;
int regfiles_only = 0;		/* 1 => use symlinks in statmon, 0 => don't */
char hostname[MAXHOSTNAMELEN];

/*
 * These variables will be used to store all the
 * alias names for the host, as well as the -a
 * command line hostnames.
 */
int host_name_count;
char **host_name; /* store -a opts */
int  addrix; /* # of -a entries */


/*
 * The following 2 variables are meaningful
 * only under a HA configuration.
 * The path_name array is dynamically allocated in main() during
 * command line argument processing for the -p options.
 */
char **path_name = NULL;  /* store -p opts */
int  pathix = 0;  /* # of -p entries */

/* Global variables.  Refer to sm_statd.h for description */
mutex_t crash_lock;
int die;
int in_crash;
cond_t crash_finish;
mutex_t sm_trylock;
rwlock_t thr_rwlock;
cond_t retrywait;
mutex_t name_addrlock;

/* forward references */
static void set_statmon_owner(void);
static void copy_client_names(void);
static void one_statmon_owner(const char *);
static int nftw_owner(const char *, const struct stat *, int, struct FTW *);

/*
 * statd protocol
 * 	commands:
 * 		SM_STAT
 * 			returns stat_fail to caller
 * 		SM_MON
 * 			adds an entry to the monitor_q and the record_q
 *			This message is sent by the server lockd to the server
 *			statd, to indicate that a new client is to be monitored.
 *			It is also sent by the server lockd to the client statd
 *			to indicate that a new server is to be monitored.
 * 		SM_UNMON
 * 			removes an entry from the monitor_q and the record_q
 * 		SM_UNMON_ALL
 * 			removes all entries from a particular host from the
 * 			monitor_q and the record_q.  Our statd has this
 * 			disabled.
 * 		SM_SIMU_CRASH
 * 			simulate a crash.  removes everything from the
 * 			record_q and the recovery_q, then calls statd_init()
 * 			to restart things.  This message is sent by the server
 *			lockd to the server statd to have all clients notified
 *			that they should reclaim locks.
 * 		SM_NOTIFY
 *			Sent by statd on server to statd on client during
 *			crash recovery.  The client statd passes the info
 *			to its lockd so it can attempt to reclaim the locks
 *			held on the server.
 *
 * There are three main hash tables used to keep track of things.
 * 	mon_table
 * 		table that keeps track hosts statd must watch.  If one of
 * 		these hosts crashes, then any locks held by that host must
 * 		be released.
 * 	record_table
 * 		used to keep track of all the hostname files stored in
 * 		the directory /var/statmon/sm.  These are client hosts who
 *		are holding or have held a lock at some point.  Needed
 *		to determine if a file needs to be created for host in
 *		/var/statmon/sm.
 *	recov_q
 *		used to keep track hostnames during a recovery
 *
 * The entries are hashed based upon the name.
 *
 * There is a directory /var/statmon/sm which holds a file named
 * for each host that is holding (or has held) a lock.  This is
 * used during initialization on startup, or after a simulated
 * crash.
 */

static void
sm_prog_1(rqstp, transp)
	struct svc_req *rqstp;
	SVCXPRT *transp;
{
	union {
		struct sm_name sm_stat_1_arg;
		struct mon sm_mon_1_arg;
		struct mon_id sm_unmon_1_arg;
		struct my_id sm_unmon_all_1_arg;
		struct stat_chge ntf_arg;
		struct reg1args reg1_arg;
	} argument;

	union {
		sm_stat_res stat_resp;
		sm_stat	mon_resp;
		struct reg1res reg1_resp;
	} result;

	bool_t (*xdr_argument)(), (*xdr_result)();
	char *(*local)();

	/*
	 * Dispatch according to which protocol is being used:
	 *	NSM_ADDR_PROGRAM is the private lockd address
	 *		registration protocol.
	 *	SM_PROG is the normal statd (NSM) protocol.
	 */
	if (rqstp->rq_prog == NSM_ADDR_PROGRAM) {
		switch (rqstp->rq_proc) {
		case NULLPROC:
			svc_sendreply(transp, xdr_void, (caddr_t)NULL);
			return;

		case NSMADDRPROC1_REG:
			xdr_argument = xdr_reg1args;
			xdr_result = xdr_reg1res;
			local = (char *(*)()) nsmaddrproc1_reg;
			break;

		default:
			svcerr_noproc(transp);
			return;
		}
	} else {
		switch (rqstp->rq_proc) {
		case NULLPROC:
			svc_sendreply(transp, xdr_void, (caddr_t)NULL);
			return;

		case SM_STAT:
			xdr_argument = xdr_sm_name;
			xdr_result = xdr_sm_stat_res;
			local = (char *(*)()) sm_status;
			break;

		case SM_MON:
			xdr_argument = xdr_mon;
			xdr_result = xdr_sm_stat_res;
			local = (char *(*)()) sm_mon;
			break;

		case SM_UNMON:
			xdr_argument = xdr_mon_id;
			xdr_result = xdr_sm_stat;
			local = (char *(*)()) sm_unmon;
			break;

		case SM_UNMON_ALL:
			xdr_argument = xdr_my_id;
			xdr_result = xdr_sm_stat;
			local = (char *(*)()) sm_unmon_all;
			break;

		case SM_SIMU_CRASH:
			xdr_argument = xdr_void;
			xdr_result = xdr_void;
			local = (char *(*)()) sm_simu_crash;
			break;

		case SM_NOTIFY:
			xdr_argument = xdr_stat_chge;
			xdr_result = xdr_void;
			local = (char *(*)()) sm_notify;
			break;

		default:
			svcerr_noproc(transp);
			return;
		}
	}

	(void) memset(&argument, 0, sizeof (argument));
	if (!svc_getargs(transp, xdr_argument, (caddr_t)&argument)) {
		svcerr_decode(transp);
		return;
	}

	(void) memset(&result, 0, sizeof (result));
	(*local)(&argument, &result);
	if (!svc_sendreply(transp, xdr_result, (caddr_t)&result)) {
		svcerr_systemerr(transp);
	}

	if (!svc_freeargs(transp, xdr_argument, (caddr_t)&argument)) {
			syslog(LOG_ERR, "statd: unable to free arguments\n");
		}
}

/*
 * Remove all files under directory path_dir.
 */
static int
remove_dir(path_dir)
char *path_dir;
{
	DIR	*dp;
	struct dirent   *dirp;
	char tmp_path[MAXPATHLEN];

	if ((dp = opendir(path_dir)) == (DIR *)NULL) {
		if (debug)
		    syslog(LOG_ERR,
			"warning: open directory %s failed: %m\n", path_dir);
		return (1);
	}

	for (dirp = readdir(dp); dirp != (struct dirent *)NULL;
		dirp = readdir(dp)) {
		if (strcmp(dirp->d_name, ".") != 0 &&
			strcmp(dirp->d_name, "..") != 0) {
			if (strlen(path_dir) + strlen(dirp->d_name) +2 >
				MAXPATHLEN) {

				syslog(LOG_ERR,
		"statd: remove dir %s/%s failed.  Pathname too long.\n",
				path_dir, dirp->d_name);

				continue;
			}
			(void) strcpy(tmp_path, path_dir);
			(void) strcat(tmp_path, "/");
			(void) strcat(tmp_path, dirp->d_name);
			delete_file(tmp_path);
		}
	}
	(void) closedir(dp);
	return (0);
}

/*
 * Copy all files from directory `from_dir' to directory `to_dir'.
 * Symlinks, if any, are preserved.
 */
void
copydir_from_to(from_dir, to_dir)
char *from_dir;
char *to_dir;
{
	int	n;
	DIR	*dp;
	struct dirent   *dirp;
	char rname[MAXNAMELEN];
	char path[MAXPATHLEN+MAXNAMELEN+2];

	if ((dp = opendir(from_dir)) == (DIR *)NULL) {
		if (debug)
		    syslog(LOG_ERR,
			"warning: open directory %s failed: %m\n", from_dir);
		return;
	}

	for (dirp = readdir(dp); dirp != (struct dirent *)NULL;
		dirp = readdir(dp)) {
		if (strcmp(dirp->d_name, ".") == 0 ||
			strcmp(dirp->d_name, "..") == 0) {
			continue;
		}

		(void) strcpy(path, from_dir);
		(void) strcat(path, "/");
		(void) strcat(path, dirp->d_name);

		if (is_symlink(path)) {
			/*
			 * Follow the link to get the referenced file name
			 * and make a new link for that file in to_dir.
			 */
			n = readlink(path, rname, MAXNAMELEN);
			if (n <= 0) {
				if (debug >= 2) {
				    (void) printf(
					"copydir_from_to: can't read link %s\n",
					path);
				}
				continue;
			}
			rname[n] = '\0';

			(void) create_symlink(to_dir, rname, dirp->d_name);
		} else {
			/*
			 * Simply copy regular files to to_dir.
			 */
			(void) strcpy(path, to_dir);
			(void) strcat(path, "/");
			(void) strcat(path, dirp->d_name);
			(void) create_file(path);
		}
	}

	(void) closedir(dp);
}

static int
init_hostname(void)
{
	struct lifnum lifn;
	int sock;

	if ((sock = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) {
		syslog(LOG_ERR, "statd:init_hostname, socket: %m");
		return (-1);
	}

	lifn.lifn_family = AF_UNSPEC;
	lifn.lifn_flags = 0;

	if (ioctl(sock, SIOCGLIFNUM, (char *)&lifn) < 0) {
		syslog(LOG_ERR,
		"statd:init_hostname, get number of interfaces, error: %m");
		close(sock);
		return (-1);
	}

	host_name_count = lifn.lifn_count;

	host_name = (char **)malloc(host_name_count * sizeof (char *));
	if (host_name == NULL) {
		perror("statd -a can't get ip configuration\n");
		close(sock);
		return (-1);
	}
	close(sock);
	return (0);
}

main(argc, argv)
	int argc;
	char **argv;
{
	int t;
	int c;
	int ppid;
	extern char *optarg;
	int choice = 0;
	struct rlimit rl;
	int mode;
	int sz;
	int connmaxrec = RPC_MAXDATASIZE;

	addrix = 0;
	pathix = 0;

	(void) gethostname(hostname, MAXHOSTNAMELEN);
	if (init_hostname() < 0)
		exit(1);

	while ((c = getopt(argc, argv, "Dd:a:p:r")) != EOF)
		switch (c) {
		case 'd':
			(void) sscanf(optarg, "%d", &debug);
			break;
		case 'D':
			choice = 1;
			break;
		case 'a':
			if (addrix < host_name_count) {
				if (strcmp(hostname, optarg) != 0) {
					sz = strlen(optarg);
					if (sz < MAXHOSTNAMELEN) {
						host_name[addrix] =
							(char *)xmalloc(sz+1);
						if (host_name[addrix] !=
						    NULL) {
						(void) sscanf(optarg, "%s",
							host_name[addrix]);
							addrix++;
						}
					} else
					(void) fprintf(stderr,
				    "statd: -a name of host is too long.\n");
				}
			} else
				(void) fprintf(stderr,
				    "statd: -a exceeding maximum hostnames\n");
			break;
		case 'p':
			if (strlen(optarg) < MAXPATHLEN) {
				/* If the path_name array has not yet	   */
				/* been malloc'ed, do that.  The array	   */
				/* should be big enough to hold all of the */
				/* -p options we might have.  An upper	   */
				/* bound on the number of -p options is	   */
				/* argc/2, because each -p option consumes */
				/* two arguments.  Here the upper bound	   */
				/* is supposing that all the command line  */
				/* arguments are -p options, which would   */
				/* actually never be the case.		   */
				if (path_name == NULL) {
					size_t sz = (argc/2) * sizeof (char *);

					path_name = (char **)malloc(sz);
					if (path_name == NULL) {
						(void) fprintf(stderr,
						"statd: malloc failed\n");
						exit(1);
					}
					(void) memset(path_name, 0, sz);
				}
				path_name[pathix] = optarg;
				pathix++;
			} else {
				(void) fprintf(stderr,
				"statd: -p pathname is too long.\n");
			}
			break;
		case 'r':
			regfiles_only = 1;
			break;
		default:
			(void) fprintf(stderr,
			"statd [-d level] [-D]\n");
			return (1);
		}

	if (choice == 0) {
		(void) strcpy(statd_home, home0);
		(void) strcpy(CURRENT, current0);
		(void) strcpy(BACKUP, backup0);
		(void) strcpy(STATE, state0);
	} else {
		(void) strcpy(statd_home, home1);
		(void) strcpy(CURRENT, current1);
		(void) strcpy(BACKUP, backup1);
		(void) strcpy(STATE, state1);
	}
	if (debug)
		(void) printf("debug is on, create entry: %s, %s, %s\n",
			CURRENT, BACKUP, STATE);

	if (getrlimit(RLIMIT_NOFILE, &rl))
		(void) printf("statd: getrlimit failed. \n");

	/* Set maxfdlimit current soft limit */
	rl.rlim_cur = MAX_FDS;
	if (setrlimit(RLIMIT_NOFILE, &rl) != 0)
		syslog(LOG_ERR, "statd: unable to set RLIMIT_NOFILE to %d\n",
			MAX_FDS);

	if (!debug) {
		ppid = fork();
		if (ppid == -1) {
			(void) fprintf(stderr, "statd: fork failure\n");
			(void) fflush(stderr);
			abort();
		}
		if (ppid != 0) {
			exit(0);
		}
		for (t = 0; t < rl.rlim_max; t++)
			(void) close(t);

		(void) open("/dev/null", O_RDONLY);
		(void) open("/dev/null", O_WRONLY);
		(void) dup(1);
		(void) setsid();
		openlog("statd", LOG_PID, LOG_DAEMON);
	}

	/* Get other aliases from each interface. */
	merge_hosts();

	/*
	 * Set to automatic mode such that threads are automatically
	 * created
	 */
	mode = RPC_SVC_MT_AUTO;
	if (!rpc_control(RPC_SVC_MTMODE_SET, &mode)) {
		syslog(LOG_ERR,
			"statd:unable to set automatic MT mode.");
		exit(1);
	}

	/*
	 * Set non-blocking mode and maximum record size for
	 * connection oriented RPC transports.
	 */
	if (!rpc_control(RPC_SVC_CONNMAXREC_SET, &connmaxrec)) {
		syslog(LOG_INFO, "unable to set maximum RPC record size");
	}

	if (!svc_create(sm_prog_1, SM_PROG, SM_VERS, "netpath")) {
	    syslog(LOG_ERR,
		"statd: unable to create (SM_PROG, SM_VERS) for netpath.");
	    exit(1);
	}

	if (!svc_create(sm_prog_1, NSM_ADDR_PROGRAM, NSM_ADDR_V1, "netpath")) {
	    syslog(LOG_ERR,
	"statd: unable to create (NSM_ADDR_PROGRAM, NSM_ADDR_V1) for netpath.");
	}

	/*
	 * Make sure /var/statmon and any alternate (-p) statmon
	 * directories exist and are owned by daemon.  Then change our uid
	 * to daemon.  The uid change is to prevent attacks against local
	 * daemons that trust any call from a local root process.
	 */

	set_statmon_owner();

	/*
	 *
	 * statd now runs as a daemon rather than root and can not
	 * dump core under / because of the permission. It is
	 * important that current working directory of statd be
	 * changed to writable directory /var/statmon so that it
	 * can dump the core upon the receipt of the signal.
	 * One still need to set allow_setid_core to non-zero in
	 * /etc/system to get the core dump.
	 *
	 */

	if (chdir(statd_home) < 0) {
		syslog(LOG_ERR, "can't chdir %s: %m", statd_home);
		exit(1);
	}

	copy_client_names();

	rwlock_init(&thr_rwlock, USYNC_THREAD, NULL);
	mutex_init(&crash_lock, USYNC_THREAD, NULL);
	mutex_init(&name_addrlock, USYNC_THREAD, NULL);
	cond_init(&crash_finish, USYNC_THREAD, NULL);
	cond_init(&retrywait, USYNC_THREAD, NULL);
	sm_inithash();
	die = 0;
	/*
	 * This variable is set to ensure that an sm_crash
	 * request will not be done at the same time
	 * when a statd_init is being done, since sm_crash
	 * can reset some variables that statd_init will be using.
	 */
	in_crash = 1;
	statd_init();

	if (debug)
		(void) printf("Starting svc_run\n");
	svc_run();
	syslog(LOG_ERR, "statd: svc_run returned\n");
	/* NOTREACHED */
	thr_exit((void *) 1);

}

/*
 * Make sure the ownership of the statmon directories is correct, then
 * change our uid to match.  If the top-level directories (/var/statmon, -p
 * arguments) don't exist, they are created first.  The sm and sm.bak
 * directories are not created here, but if they already exist, they are
 * chowned to the correct uid, along with anything else in the
 * directories.
 */

static void
set_statmon_owner()
{
	int i;

	/*
	 * Recursively chown/chgrp /var/statmon and the alternate paths,
	 * creating them if necessary.
	 */
	one_statmon_owner(statd_home);
	for (i = 0; i < pathix; i++) {
		char alt_path[MAXPATHLEN];

		snprintf(alt_path, MAXPATHLEN, "%s/statmon", path_name[i]);
		one_statmon_owner(alt_path);
	}

	if (setgid(DAEMON_GROUP) < 0) {
		syslog(LOG_ERR, "can't setgid to %d: %m", DAEMON_GROUP);
		exit(1);
	}
	if (setuid(DAEMON_USER) < 0) {
		syslog(LOG_ERR, "can't setuid to %d: %m", DAEMON_USER);
		exit(1);
	}
}

/*
 * Copy client names from the alternate statmon directories into
 * /var/statmon.  The top-level (statmon) directories should already
 * exist, though the sm and sm.bak directories might not.
 */

static void
copy_client_names()
{
	int i;
	char buf[MAXPATHLEN+SM_MAXPATHLEN];

	/*
	 * Copy all clients from alternate paths to /var/statmon/sm
	 * Remove the files in alternate directory when copying is done.
	 */
	for (i = 0; i < pathix; i++) {
		/*
		 * If the alternate directories do not exist, create it.
		 * If they do exist, just do the copy.
		 */
		snprintf(buf, sizeof (buf), "%s/statmon/sm", path_name[i]);
		if ((mkdir(buf, SM_DIRECTORY_MODE)) == -1) {
			if (errno != EEXIST) {
				syslog(LOG_ERR,
					"can't mkdir %s: %m\n", buf);
				continue;
			}
			copydir_from_to(buf, CURRENT);
			(void) remove_dir(buf);
		}

		(void) snprintf(buf, sizeof (buf), "%s/statmon/sm.bak",
				path_name[i]);
		if ((mkdir(buf, SM_DIRECTORY_MODE)) == -1) {
			if (errno != EEXIST) {
				syslog(LOG_ERR,
					"can't mkdir %s: %m\n", buf);
				continue;
			}
			copydir_from_to(buf, BACKUP);
			(void) remove_dir(buf);
		}
	}
}

/*
 * Create the given directory if it doesn't already exist.  Set the user
 * and group to daemon for the directory and anything under it.
 */

static void
one_statmon_owner(const char *dir)
{
	if ((mkdir(dir, SM_DIRECTORY_MODE)) == -1) {
		if (errno != EEXIST) {
			syslog(LOG_ERR, "can't mkdir %s: %m",
			    dir);
			return;
		}
	}

	if (debug)
		printf("Setting owner for %s\n", dir);

	if (nftw(dir, nftw_owner, MAX_FDS, FTW_PHYS) != 0) {
		syslog(LOG_WARNING, "error setting owner for %s: %m",
		    dir);
	}
}

/*
 * Set the user and group to daemon for the given file or directory.  If
 * it's a directory, also makes sure that it is mode 755.
 * Generates a syslog message but does not return an error if there were
 * problems.
 */

/*ARGSUSED3*/
static int
nftw_owner(const char *path, const struct stat *statp, int info,
	struct FTW *ftw)
{
	if (!(info == FTW_F || info == FTW_D))
		return (0);

	/*
	 * Some older systems might have mode 777 directories.  Fix that.
	 */

	if (info == FTW_D && (statp->st_mode & (S_IWGRP | S_IWOTH)) != 0) {
		mode_t newmode = (statp->st_mode & ~(S_IWGRP | S_IWOTH)) &
			S_IAMB;

		if (debug)
			printf("chmod %03o %s\n", newmode, path);
		if (chmod(path, newmode) < 0) {
			int error = errno;

			syslog(LOG_WARNING, "can't chmod %s to %03o: %m",
			    path, newmode);
			if (debug)
				printf("  FAILED: %s\n", strerror(error));
		}
	}

	/* If already owned by daemon, don't bother changing. */
	if (statp->st_uid == DAEMON_USER &&
	    statp->st_gid == DAEMON_GROUP)
		return (0);

	if (debug)
		printf("lchown %s daemon:daemon\n", path);
	if (lchown(path, DAEMON_USER, DAEMON_GROUP) < 0) {
		int error = errno;

		syslog(LOG_WARNING, "can't chown %s to daemon: %m",
		    path);
		if (debug)
			printf("  FAILED: %s\n", strerror(error));
	}

	return (0);
}
