/*
 * 5799-WZQ (C) COPYRIGHT = NONE
 * LICENSED MATERIALS - PROPERTY OF IBM
 */
/* $Header:passwd.c 12.0$ */
/* $ACIS:passwd.c 12.0$ */
/* $Source: /ibm/acis/usr/src/bin/RCS/passwd.c,v $ */

#ifndef lint
static char *rcsid = "$Header:passwd.c 12.0$";
#endif

#include <sys/nfs_defines.h>

/*
 * Copyright (c) 1983 Regents of the University of California.
 * All rights reserved.  The Berkeley software License Agreement
 * specifies the terms and conditions for redistribution.
 */

#ifndef lint
char copyright[] =
"@(#) Copyright (c) 1983 Regents of the University of California.\n\
 All rights reserved.\n";
#endif not lint

#ifndef lint
static char sccsid[] = "@(#)passwd.c	4.24 (Berkeley) 5/28/86";
#endif not lint

#ifdef NFS
/* passwd.c.r1.3	Fri Dec 18 14:29:21 1987NFSSRC */
#endif NFS

/*
 * Modify a field in the password file (either
 * password, login shell, or gecos field).
 * This program should be suid with an owner
 * with write permission on /etc/passwd.
 */
#include <sys/types.h>
#include <sys/file.h>
#include <sys/time.h>
#include <sys/resource.h>

#include <stdio.h>
#include <signal.h>
#include <pwd.h>
#include <ndbm.h>
#include <errno.h>
#include <strings.h>
#include <ctype.h>

/*
 * This should be the first thing returned from a getloginshells()
 * but too many programs know that it is /bin/sh.
 */
#define DEFSHELL "/bin/sh"

char	temp[] = "/etc/ptmp";
char	passwd[] = "/etc/passwd";
char	*getpass();
char	*getlogin();
char	*getfingerinfo();
char	*getloginshell();
char	*getnewpasswd();
char	*malloc();
char	*getusershell();
struct  passwd *pgetpwent();  
struct  passwd *pgetpwnam();  
extern	int errno;

char 	*progname;					/* name of this prog */

main(argc, argv)
	char *argv[];
{
	struct passwd *pwd;
	char *cp, *uname;
	int fd, u, dochfn, dochsh, err;
	FILE *tf;
	DBM *dp;

	if ((progname = rindex(argv[0], '/')) == NULL)
		progname = argv[0];
	else
		progname++;
	dochfn = 0, dochsh = 0;
	argc--, argv++;
	while (argc > 0 && argv[0][0] == '-') {
		for (cp = &argv[0][1]; *cp; cp++) switch (*cp) {

		case 'f':
			if (dochsh)
				goto bad;
			dochfn = 1;
			break;

		case 's':
			if (dochfn) {
		bad:
				fprintf(stderr,
				   "%s: Only one of -f and -s allowed.\n",progname); 
				exit(1);
			}
			dochsh = 1;
			break;

		default:
			fprintf(stderr, "%s: -%c: unknown option.\n",
								progname,*cp);
			exit(1);
		}
		argc--, argv++;
	}
	if (!dochfn && !dochsh) {
		if (strcmp(progname, "chfn") == 0)
			dochfn = 1;
		else if (strcmp(progname, "chsh") == 0)
			dochsh = 1;
	}
	u = getuid();
	if (argc < 1) {
		if ((uname = getlogin()) == NULL) {
			if ((pwd = getpwuid(u)) == NULL) {
				fprintf(stderr,"No available user name, try: ");
				fprintf(stderr,"%s [-f] [-s] user\n",progname);
				exit(1);
			} else {
				uname = pwd->pw_name;
			}
		} else {
			pwd = pgetpwnam(uname);
		}
		printf("Changing %s for %s.\n",
		    dochfn ? "finger information" :
			dochsh ? "login shell" : "password",
		    uname);
		fflush(stdout);
	} else {
		uname = *argv++;
		pwd = pgetpwnam(uname);
	}
	if (pwd == NULL) {
		fprintf(stderr, "%s: %s: unknown user.\n", progname, uname);
		exit(1);
	}
	if (u != 0 && u != pwd->pw_uid) {
		printf("Permission denied.\n");
		exit(1);
	}
	if (dochfn)
		cp = getfingerinfo(pwd);
	else if (dochsh)
		cp = getloginshell(pwd, u, *argv);
	else
		cp = getnewpasswd(pwd, u);
	(void) signal(SIGHUP, SIG_IGN);
	(void) signal(SIGINT, SIG_IGN);
	(void) signal(SIGQUIT, SIG_IGN);
	(void) signal(SIGTSTP, SIG_IGN);
	(void) umask(0);
	fd = open(temp, O_WRONLY|O_CREAT|O_EXCL, 0644);
	if (fd < 0) {
		err = errno;

		fprintf(stderr, "%s: ",progname);
		if (err == EEXIST)
			fprintf(stderr, "password file busy - try again.\n");
		else {
			errno = err;
			perror(temp);
		}
		exit(1);
	}
	if ((tf = fdopen(fd, "w")) == NULL) {
		fprintf(stderr, "%s: fdopen failed?\n",progname);
		exit(1);
	}
	if ((dp = dbm_open(passwd, O_RDWR, 0644)) == NULL) {
		err = errno;
		fprintf(stderr, "Warning: dbm_open failed: ");
		errno = err;
		perror(passwd);
	} else if (flock(dp->dbm_dirf, LOCK_EX) < 0) {
		perror("Warning: lock failed");
		dbm_close(dp);
		dp = NULL;
	}
	unlimit(RLIMIT_CPU);
	unlimit(RLIMIT_FSIZE);
	/*
	 * Copy passwd to temp, replacing matching lines
	 * with new password.
	 */
	while ((pwd = pgetpwent()) != NULL) {
		if (strcmp(pwd->pw_name, uname) == 0) {
			if (u && u != pwd->pw_uid) {
				fprintf(stderr, "%s: permission denied.\n",progname);
				goto out;
			}
			if (dochfn)
				pwd->pw_gecos = cp;
			else if (dochsh)
				pwd->pw_shell = cp;
			else
				pwd->pw_passwd = cp;
			if (pwd->pw_gecos[0] == '*')	/* ??? */
				pwd->pw_gecos++;
			replace(dp, pwd);
		}
		fprintf(tf,"%s:%s:%d:%d:%s:%s:%s\n",
			pwd->pw_name,
			pwd->pw_passwd,
			pwd->pw_uid,
			pwd->pw_gid,
			pwd->pw_gecos,
			pwd->pw_dir,
			pwd->pw_shell);
	}
	pendpwent();
	if (dp != NULL && dbm_error(dp))
		fprintf(stderr, "Warning: dbm_store failed\n");
	(void) fflush(tf);
	if (ferror(tf)) {
		fprintf(stderr, "Warning: %s write error, %s not updated\n",
		    temp, passwd);
		goto out;
	}
	(void) fclose(tf);
	if (dp != NULL)
		dbm_close(dp);
	if (rename(temp, passwd) < 0) {
		fprintf("%s: ",progname); perror("rename");
	out:
		(void) unlink(temp);
		exit(1);
	}
	exit(0);
}

unlimit(lim)
{
	struct rlimit rlim;

	rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY;
	(void) setrlimit(lim, &rlim);
}

/*
 * Replace the password entry in the dbm data base with pwd.
 */
replace(dp, pwd)
	DBM *dp;
	struct passwd *pwd;
{
	datum key, content;
	register char *cp, *tp;
	char buf[BUFSIZ];

	if (dp == NULL)
		return;

	cp = buf;
#define	COMPACT(e)	tp = pwd->pw_/**/e; while (*cp++ = *tp++);
	COMPACT(name);
	COMPACT(passwd);
	bcopy((char *)&pwd->pw_uid, cp, sizeof (int));
	cp += sizeof (int);
	bcopy((char *)&pwd->pw_gid, cp, sizeof (int));
	cp += sizeof (int);
	bcopy((char *)&pwd->pw_quota, cp, sizeof (int));
	cp += sizeof (int);
	COMPACT(comment);
	COMPACT(gecos);
	COMPACT(dir);
	COMPACT(shell);
	content.dptr = buf;
	content.dsize = cp - buf;
	key.dptr = pwd->pw_name;
	key.dsize = strlen(pwd->pw_name);
	dbm_store(dp, key, content, DBM_REPLACE);
	key.dptr = (char *)&pwd->pw_uid;
	key.dsize = sizeof (int);
	dbm_store(dp, key, content, DBM_REPLACE);
}

char *
getnewpasswd(pwd, u)
	register struct passwd *pwd;
	int u;
{
	char saltc[2];
	long salt;
	int i, insist = 0, ok, flags;
	int c, pwlen;
	static char pwbuf[10];
	long time();
	char *crypt(), *pw, *p;

	if (pwd->pw_passwd[0] && u != 0) {
		(void) strcpy(pwbuf, getpass("Old password:"));
		pw = crypt(pwbuf, pwd->pw_passwd);
		if (strcmp(pw, pwd->pw_passwd) != 0) {
			printf("Sorry.\n");
			exit(1);
		}
	}
tryagain:
	(void) strcpy(pwbuf, getpass("New password:"));
	pwlen = strlen(pwbuf);
	if (pwlen == 0) {
		printf("Password unchanged.\n");
		exit(1);
	}
	/*
	 * Insure password is of reasonable length and
	 * composition.  If we really wanted to make things
	 * sticky, we could check the dictionary for common
	 * words, but then things would really be slow.
	 */
	ok = 0;
	flags = 0;
	p = pwbuf;
	while (c = *p++) {
		if (c >= 'a' && c <= 'z')
			flags |= 2;
		else if (c >= 'A' && c <= 'Z')
			flags |= 4;
		else if (c >= '0' && c <= '9')
			flags |= 1;
		else
			flags |= 8;
	}
	if (flags >= 7 && pwlen >= 4)
		ok = 1;
	if ((flags == 2 || flags == 4) && pwlen >= 6)
		ok = 1;
	if ((flags == 3 || flags == 5 || flags == 6) && pwlen >= 5)
		ok = 1;
	if (!ok && insist < 2) {
		printf("Please use %s.\n", flags == 1 ?
			"at least one non-numeric character" :
			"a longer password");
		insist++;
		goto tryagain;
	}
	if (strcmp(pwbuf, getpass("Retype new password:")) != 0) {
		printf("Mismatch - password unchanged.\n");
		exit(1);
	}
	(void) time(&salt);
	salt = 9 * getpid();
	saltc[0] = salt & 077;
	saltc[1] = (salt>>6) & 077;
	for (i = 0; i < 2; i++) {
		c = saltc[i] + '.';
		if (c > '9')
			c += 7;
		if (c > 'Z')
			c += 6;
		saltc[i] = c;
	}
	return (crypt(pwbuf, saltc));
}

char *
getloginshell(pwd, u, arg)
	struct passwd *pwd;
	int u;
	char *arg;
{
	static char newshell[BUFSIZ];
	char *cp, *valid, *getusershell();

	if (pwd->pw_shell == 0 || *pwd->pw_shell == '\0')
		pwd->pw_shell = DEFSHELL;
	if (u != 0) {
		for (valid = getusershell(); valid; valid = getusershell())
			if (strcmp(pwd->pw_shell, valid) == 0)
				break;
		if (valid == NULL) {
			printf("Cannot change from restricted shell %s\n",
				pwd->pw_shell);
			exit(1);
		}
	}
	if (arg != 0) {
		(void) strncpy(newshell, arg, sizeof newshell - 1);
		newshell[sizeof newshell - 1] = 0;
	} else {
		printf("Old shell: %s\nNew shell: ", pwd->pw_shell);
		(void)fgets(newshell, sizeof (newshell) - 1, stdin);
		cp = index(newshell, '\n');
		if (cp)
			*cp = '\0';
	}
	if (newshell[0] == '\0' || strcmp(newshell, pwd->pw_shell) == 0) {
		printf("Login shell unchanged.\n");
		exit(1);
	}
	/*
	 * Allow user to give shell name w/o preceding pathname.
	 */
	if (u == 0) {
		valid = newshell;
	} else {
		setusershell();
		for (valid = getusershell(); valid; valid = getusershell()) {
			if (newshell[0] == '/') {
				cp = valid;
			} else {
				cp = rindex(valid, '/');
				if (cp == 0)
					cp = valid;
				else
					cp++;
			}
			if (strcmp(newshell, cp) == 0)
				break;
		}
	}
	if (valid == 0) {
		printf("%s is unacceptable as a new shell.\n",
		    newshell);
		exit(1);
	}
	if (access(valid, X_OK) < 0) {
		printf("%s is unavailable.\n", valid);
		exit(1);
	}
	if (strcmp(valid, DEFSHELL) == 0)
		valid[0] = '\0';
	return (valid);
}

struct default_values {
	char *name;
	char *office_num;
	char *office_phone;
	char *home_phone;
};

/*
 * Get name, room number, school phone, and home phone.
 */
char *
getfingerinfo(pwd)
	struct passwd *pwd;
{
	char in_str[BUFSIZ];
	struct default_values *defaults, *get_defaults();
	static char answer[4*BUFSIZ];

	answer[0] = '\0';
	defaults = get_defaults(pwd->pw_gecos);
	printf("Default values are printed inside of '[]'.\n");
	printf("To accept the default, type <return>.\n");
	printf("To have a blank entry, type the word 'none'.\n");
	/*
	 * Get name.
	 */
	do {
		printf("\nName [%s]: ", defaults->name);
		(void) fgets(in_str, BUFSIZ, stdin);
		if (special_case(in_str, defaults->name)) 
			break;
	} while (illegal_input(in_str));
	(void) strcpy(answer, in_str);
	/*
	 * Get room number.
	 */
	do {
		printf("Room number (Exs: 597E or 197C) [%s]: ",
			defaults->office_num);
		(void) fgets(in_str, BUFSIZ, stdin);
		if (special_case(in_str, defaults->office_num))
			break;
	} while (illegal_input(in_str) || illegal_building(in_str));
	(void) strcat(strcat(answer, ","), in_str);
	/*
	 * Get office phone number.
	 * Remove hyphens.
	 */
	do {
		printf("Office Phone (Ex: 6426000) [%s]: ",
			defaults->office_phone);
		(void) fgets(in_str, BUFSIZ, stdin);
		if (special_case(in_str, defaults->office_phone))
			break;
		remove_hyphens(in_str);
	} while (illegal_input(in_str) || not_all_digits(in_str));
	(void) strcat(strcat(answer, ","), in_str);
	/*
	 * Get home phone number.
	 * Remove hyphens if present.
	 */
	do {
		printf("Home Phone (Ex: 9875432) [%s]: ", defaults->home_phone);
		(void) fgets(in_str, BUFSIZ, stdin);
		if (special_case(in_str, defaults->home_phone))
			break;
		remove_hyphens(in_str);
	} while (illegal_input(in_str) || not_all_digits(in_str));
	(void) strcat(strcat(answer, ","), in_str);
	if (strcmp(answer, pwd->pw_gecos) == 0) {
		printf("Finger information unchanged.\n");
		exit(1);
	}
	return (answer);
}

/*
 * Prints an error message if a ':' or a newline is found in the string.
 * A message is also printed if the input string is too long.
 * The password file uses :'s as seperators, and are not allowed in the "gcos"
 * field.  Newlines serve as delimiters between users in the password file,
 * and so, those too, are checked for.  (I don't think that it is possible to
 * type them in, but better safe than sorry)
 *
 * Returns '1' if a colon or newline is found or the input line is too long.
 */
illegal_input(input_str)
	char *input_str;
{
	char *ptr;
	int error_flag = 0;
	int length = strlen(input_str);

	if (index(input_str, ':')) {
		printf("':' is not allowed.\n");
		error_flag = 1;
	}
	if (input_str[length-1] != '\n') {
		/* the newline and the '\0' eat up two characters */
		printf("Maximum number of characters allowed is %d\n",
			BUFSIZ-2);
		/* flush the rest of the input line */
		while (getchar() != '\n')
			/* void */;
		error_flag = 1;
	}
	/*
	 * Delete newline by shortening string by 1.
	 */
	input_str[length-1] = '\0';
	/*
	 * Don't allow control characters, etc in input string.
	 */
	for (ptr=input_str; *ptr != '\0'; ptr++) {
		if ((int) *ptr < 040) {
			printf("Control characters are not allowed.\n");
			error_flag = 1;
			break;
		}
	}
	return (error_flag);
}

/*
 * Removes '-'s from the input string.
 */
remove_hyphens(str)
	char *str;
{
	char *hyphen;

	while ((hyphen = index(str, '-')) != NULL)
		(void) strcpy(hyphen, hyphen+1);
}

/*
 *  Checks to see if 'str' contains only digits (0-9).  If not, then
 *  an error message is printed and '1' is returned.
 */
not_all_digits(str)
	char *str;
{
	char *ptr;

	for (ptr = str; *ptr != '\0'; ++ptr)
		if (!isdigit(*ptr)) {
			printf("Phone numbers can only contain digits.\n");
			return (1);
		}
	return (0);
}

/*
 * Deal with Berkeley buildings.  Abbreviating Cory to C and Evans to E.
 * Correction changes "str".
 *
 * Returns 1 if incorrect room format.
 * 
 * Note: this function assumes that the newline has been removed from str.
 */
illegal_building(str)
	register char *str;
{
	int length = strlen(str);
	register char *ptr;

	/*
	 * If the string is [Ee]vans or [Cc]ory or ends in
	 * [ \t0-9][Ee]vans or [ \t0-9M][Cc]ory, then contract the name
	 * into 'E' or 'C', as the case may be, and delete leading blanks.
	 */
	if (length >= 5 && strcmp(ptr = str + length - 4, "vans") == 0 &&
	    (*--ptr == 'e' || *ptr == 'E') &&
	    (--ptr < str || isspace(*ptr) || isdigit(*ptr))) {
		for (; ptr > str && isspace(*ptr); ptr--)
			;
		ptr++;
		*ptr++ = 'E';
		*ptr = '\0';
	} else
	if (length >= 4 && strcmp(ptr = str + length - 3, "ory") == 0 &&
	    (*--ptr == 'c' || *ptr == 'C') &&
	    (--ptr < str || *ptr == 'M' || isspace(*ptr) || isdigit(*ptr))) {
		for (; ptr > str && isspace(*ptr); ptr--)
			;
		ptr++;
		*ptr++ = 'C';
		*ptr = '\0';
	}
	return (0);
}

/*
 * get_defaults picks apart "str" and returns a structure points.
 * "str" contains up to 4 fields separated by commas.
 * Any field that is missing is set to blank.
 */
struct default_values *
get_defaults(str)
	char *str;
{
	struct default_values *answer;

	answer = (struct default_values *)
		malloc((unsigned)sizeof(struct default_values));
	if (answer == (struct default_values *) NULL) {
		fprintf(stderr,
			"\nUnable to allocate storage in get_defaults!\n");
		exit(1);
	}
	/*
	 * Values if no corresponding string in "str".
	 */
	answer->name = str;
	answer->office_num = "";
	answer->office_phone = "";
	answer->home_phone = "";
	str = index(answer->name, ',');
	if (str == 0) 
		return (answer);
	*str = '\0';
	answer->office_num = str + 1;
	str = index(answer->office_num, ',');
	if (str == 0) 
		return (answer);
	*str = '\0';
	answer->office_phone = str + 1;
	str = index(answer->office_phone, ',');
	if (str == 0) 
		return (answer);
	*str = '\0';
	answer->home_phone = str + 1;
	return (answer);
}

/*
 *  special_case returns true when either the default is accepted
 *  (str = '\n'), or when 'none' is typed.  'none' is accepted in
 *  either upper or lower case (or any combination).  'str' is modified
 *  in these two cases.
 */
special_case(str,default_str)
	char *str, *default_str;
{
	static char word[] = "none\n";
	char *ptr, *wordptr;

	/*
	 *  If the default is accepted, then change the old string do the 
	 *  default string.
	 */
	if (*str == '\n') {
		(void) strcpy(str, default_str);
		return (1);
	}
	/*
	 *  Check to see if str is 'none'.  (It is questionable if case
	 *  insensitivity is worth the hair).
	 */
	wordptr = word-1;
	for (ptr = str; *ptr != '\0'; ++ptr) {
		++wordptr;
		if (*wordptr == '\0')	/* then words are different sizes */
			return (0);
		if (*ptr == *wordptr)
			continue;
		if (isupper(*ptr) && (tolower(*ptr) == *wordptr))
			continue;
		/*
		 * At this point we have a mismatch, so we return
		 */
		return (0);
	}
	/*
	 * Make sure that words are the same length.
	 */
	if (*(wordptr+1) != '\0')
		return (0);
	/*
	 * Change 'str' to be the null string
	 */
	*str = '\0';
	return (1);
}

#ifdef NFS
/*
 * Special versions of the password file code which do not expand Yellow 
 * Pages entries.
 */
static char EMPTY[] = "";
static FILE *pwf = NULL;
static char line[BUFSIZ+1];
static struct passwd pwd;

pendpwent()
{
	if (pwf != NULL) {
		fclose(pwf);
		pwf = NULL;
	}
}

char *
pwskip(p)
register char *p;
{
	while (*p && *p != ':' && *p != '\n')
		++p;
	if (*p)
		*p++ = 0;
	return(p);
}

struct passwd *
pgetpwent()
{
	register char *p;
	
	if (pwf == NULL) {
		if ((pwf = fopen( passwd, "r" )) == NULL)
			return(0);
	}
	p = fgets(line, BUFSIZ, pwf);
	if (p == NULL)
		return(0);
	pwd.pw_name = p;
	p = pwskip(p);
	pwd.pw_passwd = p;
	p = pwskip(p);
	pwd.pw_uid = atoi(p);
	p = pwskip(p);
	pwd.pw_gid = atoi(p);
	pwd.pw_quota = 0;
	pwd.pw_comment = EMPTY;
	p = pwskip(p);
	pwd.pw_gecos = p;
	p = pwskip(p);
	pwd.pw_dir = p;
	p = pwskip(p);
	pwd.pw_shell = p;
	while (*p && *p != '\n')
		p++;
	*p = '\0';
	return(&pwd);
}

struct passwd *
pgetpwnam(name)
char *name;
{
	register struct passwd *p;
	
	while ((p = pgetpwent()) != NULL && strcmp(name, p->pw_name) != 0)
		;
	pendpwent();
	return(p);
}

#endif NFS
