/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (C) 1996
 *	David L. Nugent.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY DAVID L. NUGENT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL DAVID L. NUGENT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <err.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#include "pw.h"

#define debugging 0

enum {
	_UC_NONE,
	_UC_DEFAULTPWD,
	_UC_REUSEUID,
	_UC_REUSEGID,
	_UC_NISPASSWD,
	_UC_DOTDIR,
	_UC_NEWMAIL,
	_UC_LOGFILE,
	_UC_HOMEROOT,
	_UC_HOMEMODE,
	_UC_SHELLPATH,
	_UC_SHELLS,
	_UC_DEFAULTSHELL,
	_UC_DEFAULTGROUP,
	_UC_EXTRAGROUPS,
	_UC_DEFAULTCLASS,
	_UC_MINUID,
	_UC_MAXUID,
	_UC_MINGID,
	_UC_MAXGID,
	_UC_EXPIRE,
	_UC_PASSWORD,
	_UC_FIELDS
};

static char     bourne_shell[] = "sh";

static char    *system_shells[_UC_MAXSHELLS] =
{
	bourne_shell,
	"csh",
	"tcsh"
};

static char const *booltrue[] =
{
	"yes", "true", "1", "on", NULL
};
static char const *boolfalse[] =
{
	"no", "false", "0", "off", NULL
};

static struct userconf config =
{
	0,			/* Default password for new users? (nologin) */
	0,			/* Reuse uids? */
	0,			/* Reuse gids? */
	NULL,			/* NIS version of the passwd file */
	"/usr/share/skel",	/* Where to obtain skeleton files */
	NULL,			/* Mail to send to new accounts */
	"/var/log/userlog",	/* Where to log changes */
	"/home",		/* Where to create home directory */
	_DEF_DIRMODE,		/* Home directory perms, modified by umask */
	"/bin",			/* Where shells are located */
	system_shells,		/* List of shells (first is default) */
	bourne_shell,		/* Default shell */
	NULL,			/* Default group name */
	NULL,			/* Default (additional) groups */
	NULL,			/* Default login class */
	1000, 32000,		/* Allowed range of uids */
	1000, 32000,		/* Allowed range of gids */
	0,			/* Days until account expires */
	0			/* Days until password expires */
};

static char const *comments[_UC_FIELDS] =
{
	"#\n# pw.conf - user/group configuration defaults\n#\n",
	"\n# Password for new users? no=nologin yes=loginid none=blank random=random\n",
	"\n# Reuse gaps in uid sequence? (yes or no)\n",
	"\n# Reuse gaps in gid sequence? (yes or no)\n",
	"\n# Path to the NIS passwd file (blank or 'no' for none)\n",
	"\n# Obtain default dotfiles from this directory\n",
	"\n# Mail this file to new user (/etc/newuser.msg or no)\n",
	"\n# Log add/change/remove information in this file\n",
	"\n# Root directory in which $HOME directory is created\n",
	"\n# Mode for the new $HOME directory, will be modified by umask\n",
	"\n# Colon separated list of directories containing valid shells\n",
	"\n# Comma separated list of available shells (without paths)\n",
	"\n# Default shell (without path)\n",
	"\n# Default group (leave blank for new group per user)\n",
	"\n# Extra groups for new users\n",
	"\n# Default login class for new users\n",
	"\n# Range of valid default user ids\n",
	NULL,
	"\n# Range of valid default group ids\n",
	NULL,
	"\n# Days after which account expires (0=disabled)\n",
	"\n# Days after which password expires (0=disabled)\n"
};

static char const *kwds[] =
{
	"",
	"defaultpasswd",
	"reuseuids",
	"reusegids",
	"nispasswd",
	"skeleton",
	"newmail",
	"logfile",
	"home",
	"homemode",
	"shellpath",
	"shells",
	"defaultshell",
	"defaultgroup",
	"extragroups",
	"defaultclass",
	"minuid",
	"maxuid",
	"mingid",
	"maxgid",
	"expire_days",
	"password_days",
	NULL
};

static char    *
unquote(char const * str)
{
	if (str && (*str == '"' || *str == '\'')) {
		char           *p = strchr(str + 1, *str);

		if (p != NULL)
			*p = '\0';
		return (char *) (*++str ? str : NULL);
	}
	return (char *) str;
}

int
boolean_val(char const * str, int dflt)
{
	if ((str = unquote(str)) != NULL) {
		int             i;

		for (i = 0; booltrue[i]; i++)
			if (strcmp(str, booltrue[i]) == 0)
				return 1;
		for (i = 0; boolfalse[i]; i++)
			if (strcmp(str, boolfalse[i]) == 0)
				return 0;
	}
	return dflt;
}

int
passwd_val(char const * str, int dflt)
{
	if ((str = unquote(str)) != NULL) {
		int             i;

		for (i = 0; booltrue[i]; i++)
			if (strcmp(str, booltrue[i]) == 0)
				return P_YES;
		for (i = 0; boolfalse[i]; i++)
			if (strcmp(str, boolfalse[i]) == 0)
				return P_NO;

		/*
		 * Special cases for defaultpassword
		 */
		if (strcmp(str, "random") == 0)
			return P_RANDOM;
		if (strcmp(str, "none") == 0)
			return P_NONE;

		errx(1, "Invalid value for default password");
	}
	return dflt;
}

char const     *
boolean_str(int val)
{
	if (val == P_NO)
		return (boolfalse[0]);
	else if (val == P_RANDOM)
		return ("random");
	else if (val == P_NONE)
		return ("none");
	else
		return (booltrue[0]);
}

char           *
newstr(char const * p)
{
	char	*q;

	if ((p = unquote(p)) == NULL)
		return (NULL);

	if ((q = strdup(p)) == NULL)
		err(1, "strdup()");

	return (q);
}

struct userconf *
read_userconfig(char const * file)
{
	FILE	*fp;
	char	*buf, *p;
	const char *errstr;
	size_t	linecap;
	ssize_t	linelen;

	buf = NULL;
	linecap = 0;

	if ((fp = fopen(file, "r")) == NULL)
		return (&config);

	while ((linelen = getline(&buf, &linecap, fp)) > 0) {
		if (*buf && (p = strtok(buf, " \t\r\n=")) != NULL && *p != '#') {
			static char const toks[] = " \t\r\n,=";
			char           *q = strtok(NULL, toks);
			int             i = 0;
			mode_t          *modeset;

			while (i < _UC_FIELDS && strcmp(p, kwds[i]) != 0)
				++i;
#if debugging
			if (i == _UC_FIELDS)
				printf("Got unknown kwd `%s' val=`%s'\n", p, q ? q : "");
			else
				printf("Got kwd[%s]=%s\n", p, q);
#endif
			switch (i) {
			case _UC_DEFAULTPWD:
				config.default_password = passwd_val(q, 1);
				break;
			case _UC_REUSEUID:
				config.reuse_uids = boolean_val(q, 0);
				break;
			case _UC_REUSEGID:
				config.reuse_gids = boolean_val(q, 0);
				break;
			case _UC_NISPASSWD:
				config.nispasswd = (q == NULL || !boolean_val(q, 1))
					? NULL : newstr(q);
				break;
			case _UC_DOTDIR:
				config.dotdir = (q == NULL || !boolean_val(q, 1))
					? NULL : newstr(q);
				break;
				case _UC_NEWMAIL:
				config.newmail = (q == NULL || !boolean_val(q, 1))
					? NULL : newstr(q);
				break;
			case _UC_LOGFILE:
				config.logfile = (q == NULL || !boolean_val(q, 1))
					? NULL : newstr(q);
				break;
			case _UC_HOMEROOT:
				config.home = (q == NULL || !boolean_val(q, 1))
					? "/home" : newstr(q);
				break;
			case _UC_HOMEMODE:
				modeset = setmode(q);
				config.homemode = (q == NULL || !boolean_val(q, 1))
					? _DEF_DIRMODE : getmode(modeset, _DEF_DIRMODE);
				free(modeset);
				break;
			case _UC_SHELLPATH:
				config.shelldir = (q == NULL || !boolean_val(q, 1))
					? "/bin" : newstr(q);
				break;
			case _UC_SHELLS:
				for (i = 0; i < _UC_MAXSHELLS && q != NULL; i++, q = strtok(NULL, toks))
					system_shells[i] = newstr(q);
				if (i > 0)
					while (i < _UC_MAXSHELLS)
						system_shells[i++] = NULL;
				break;
			case _UC_DEFAULTSHELL:
				config.shell_default = (q == NULL || !boolean_val(q, 1))
					? (char *) bourne_shell : newstr(q);
				break;
			case _UC_DEFAULTGROUP:
				q = unquote(q);
				config.default_group = (q == NULL || !boolean_val(q, 1) || GETGRNAM(q) == NULL)
					? NULL : newstr(q);
				break;
			case _UC_EXTRAGROUPS:
				while ((q = strtok(NULL, toks)) != NULL) {
					if (config.groups == NULL)
						config.groups = sl_init();
					sl_add(config.groups, newstr(q));
				}
				break;
			case _UC_DEFAULTCLASS:
				config.default_class = (q == NULL || !boolean_val(q, 1))
					? NULL : newstr(q);
				break;
			case _UC_MINUID:
				if ((q = unquote(q)) != NULL) {
					config.min_uid = strtounum(q, 0,
					    UID_MAX, &errstr);
					if (errstr)
						warnx("Invalid min_uid: '%s';"
						    " ignoring", q);
				}
				break;
			case _UC_MAXUID:
				if ((q = unquote(q)) != NULL) {
					config.max_uid = strtounum(q, 0,
					    UID_MAX, &errstr);
					if (errstr)
						warnx("Invalid max_uid: '%s';"
						    " ignoring", q);
				}
				break;
			case _UC_MINGID:
				if ((q = unquote(q)) != NULL) {
					config.min_gid = strtounum(q, 0,
					    GID_MAX, &errstr);
					if (errstr)
						warnx("Invalid min_gid: '%s';"
						    " ignoring", q);
				}
				break;
			case _UC_MAXGID:
				if ((q = unquote(q)) != NULL) {
					config.max_gid = strtounum(q, 0,
					    GID_MAX, &errstr);
					if (errstr)
						warnx("Invalid max_gid: '%s';"
						    " ignoring", q);
				}
				break;
			case _UC_EXPIRE:
				if ((q = unquote(q)) != NULL) {
					config.expire_days = strtonum(q, 0,
					    INT_MAX, &errstr);
					if (errstr)
						warnx("Invalid expire days:"
						    " '%s'; ignoring", q);
				}
				break;
			case _UC_PASSWORD:
				if ((q = unquote(q)) != NULL) {
					config.password_days = strtonum(q, 0,
					    INT_MAX, &errstr);
					if (errstr)
						warnx("Invalid password days:"
						    " '%s'; ignoring", q);
				}
				break;
			case _UC_FIELDS:
			case _UC_NONE:
				break;
			}
		}
	}
	free(buf);
	fclose(fp);

	return (&config);
}


int
write_userconfig(struct userconf *cnf, const char *file)
{
	int             fd;
	int             i, j;
	FILE           *buffp;
	FILE           *fp;
	char		cfgfile[MAXPATHLEN];
	char           *buf;
	size_t          sz;

	if (file == NULL) {
		snprintf(cfgfile, sizeof(cfgfile), "%s/" _PW_CONF,
		    conf.etcpath);
		file = cfgfile;
	}

	if ((fd = open(file, O_CREAT|O_RDWR|O_TRUNC|O_EXLOCK, 0644)) == -1)
		return (0);

	if ((fp = fdopen(fd, "w")) == NULL) {
		close(fd);
		return (0);
	}

	sz = 0;
	buf = NULL;
	buffp = open_memstream(&buf, &sz);
	if (buffp == NULL)
		err(EXIT_FAILURE, "open_memstream()");

	for (i = _UC_NONE; i < _UC_FIELDS; i++) {
		int             quote = 1;

		if (buf != NULL)
			memset(buf, 0, sz);
		rewind(buffp);
		switch (i) {
		case _UC_DEFAULTPWD:
			fputs(boolean_str(cnf->default_password), buffp);
			break;
		case _UC_REUSEUID:
			fputs(boolean_str(cnf->reuse_uids), buffp);
			break;
		case _UC_REUSEGID:
			fputs(boolean_str(cnf->reuse_gids), buffp);
			break;
		case _UC_NISPASSWD:
			fputs(cnf->nispasswd ?  cnf->nispasswd : "", buffp);
			quote = 0;
			break;
		case _UC_DOTDIR:
			fputs(cnf->dotdir ?  cnf->dotdir : boolean_str(0),
			    buffp);
			break;
		case _UC_NEWMAIL:
			fputs(cnf->newmail ?  cnf->newmail : boolean_str(0),
			    buffp);
			break;
		case _UC_LOGFILE:
			fputs(cnf->logfile ?  cnf->logfile : boolean_str(0),
			    buffp);
			break;
		case _UC_HOMEROOT:
			fputs(cnf->home, buffp);
			break;
		case _UC_HOMEMODE:
			fprintf(buffp, "%04o", cnf->homemode);
			quote = 0;
			break;
		case _UC_SHELLPATH:
			fputs(cnf->shelldir, buffp);
			break;
		case _UC_SHELLS:
			for (j = 0; j < _UC_MAXSHELLS &&
			    system_shells[j] != NULL; j++)
				fprintf(buffp, "%s\"%s\"", j ?
				    "," : "", system_shells[j]);
			quote = 0;
			break;
		case _UC_DEFAULTSHELL:
			fputs(cnf->shell_default ?  cnf->shell_default :
			    bourne_shell, buffp);
			break;
		case _UC_DEFAULTGROUP:
			fputs(cnf->default_group ?  cnf->default_group : "",
			    buffp);
			break;
		case _UC_EXTRAGROUPS:
			for (j = 0; cnf->groups != NULL &&
			    j < (int)cnf->groups->sl_cur; j++)
				fprintf(buffp, "%s\"%s\"", j ?
				    "," : "", cnf->groups->sl_str[j]);
			quote = 0;
			break;
		case _UC_DEFAULTCLASS:
			fputs(cnf->default_class ?  cnf->default_class : "",
			    buffp);
			break;
		case _UC_MINUID:
			fprintf(buffp, "%ju", (uintmax_t)cnf->min_uid);
			quote = 0;
			break;
		case _UC_MAXUID:
			fprintf(buffp, "%ju", (uintmax_t)cnf->max_uid);
			quote = 0;
			break;
		case _UC_MINGID:
			fprintf(buffp, "%ju", (uintmax_t)cnf->min_gid);
			quote = 0;
			break;
		case _UC_MAXGID:
			fprintf(buffp, "%ju", (uintmax_t)cnf->max_gid);
			quote = 0;
			break;
		case _UC_EXPIRE:
			fprintf(buffp, "%jd", (intmax_t)cnf->expire_days);
			quote = 0;
			break;
		case _UC_PASSWORD:
			fprintf(buffp, "%jd", (intmax_t)cnf->password_days);
			quote = 0;
			break;
		case _UC_NONE:
			break;
		}
		fflush(buffp);

		if (comments[i])
			fputs(comments[i], fp);

		if (*kwds[i]) {
			if (quote)
				fprintf(fp, "%s = \"%s\"\n", kwds[i], buf);
			else
				fprintf(fp, "%s = %s\n", kwds[i], buf);
#if debugging
			printf("WROTE: %s = %s\n", kwds[i], buf);
#endif
		}
	}
	fclose(buffp);
	free(buf);
	return (fclose(fp) != EOF);
}
