
/*	THIS FILE HAS BEEN PORTED TO LINUX W/O Shadow Passwords 
 *
 *	Ported by Forrest W. Christian  "Finished" 10/15/95
 *	If someone's using shadow passwords and would like a port
 *	let me know.  It should take me about an hour.
 *
 *	Thanks to the original author of this code for providing
 *	a framework for me to port.
 *
 *	If you use this port I'd like to hear about it! 
 *
 *	Drop any comments, questions, or complaints to
 *	forrestc@imach.com
 *
 *	Tested on:
 *		Slackware linux, kernel 1.2.13.
 *		RedHat linux, kernel 2.0.30.
 *
 *	MODIFICATION HISTORY
 *
 *	Modified by Chris Kakris, christos@dsoln.com.au
 *	  - removed custom logging and switched to syslog()
 *	  - extended protocol to allow administrator to change user passwords
 *	  - documented code
 */

/* 
 * poppassd.c
 *
 *	Update passwords on the UNIX system from the Eudora "Change Password"
 *	client.  Uses the syslog logging facility.
 *             
 *	Must be owned by root, and executable only by root.  It can be started 
 *	with an entry in /etc/inetd.conf such as the following:
 *
 *		poppassd stream tcp nowait root  /usr/local/bin/poppassd poppassd
 *
 *	or if you want to use tcp wrappers:
 *
 *		poppassd stream tcp nowait root  /usr/sbin/tcpd /usr/local/bin/poppassd
 * 
 *	and in /etc/services:
 * 
 *		poppassd        106/tcp
 *
 */

/*
 *	The server's responses should be like an FTP server's responses; 
 *	1xx for in progress, 2xx for success, 3xx for more information
 *	needed, 4xx for temporary failure, and 5xx for permanent failure.  
 *	Putting it all together, here's a sample conversation:
 *	
 *	S: 200 hello\r\n
 *	E: user yourloginname\r\n
 *	S: 300 please send your password now\r\n
 *	E: pass yourcurrentpassword\r\n
 *	S: 200 My, that was tasty\r\n
 *	E: newpass yournewpassword\r\n
 *	S: 200 Happy to oblige\r\n
 *	E: quit\r\n
 *	S: 200 Bye-bye\r\n
 *	S: <closes connection>
 *	E: <closes connection>
 *
 *	I (christos@dynamic.net.au) have extended the protocol to allow an
 *	"administrator" to change the passwords for groups of users for which
 *	they are authorised to do so.  There is a file called /etc/poppassd.table
 *	which contains this information.
 *
 *	The protocol extension is simple:  instead of a client response
 *	of "user yourloginname\r\n" the client can preceed this with the
 *	new command as shown below.
 *
 *	S: 200 hello\r\n
 *	E: admin adminname adminpassword\r\n
 *	S: 200 please enter the user name\r\n
 *	E: user loginname\r\n
 *	S: 300 please send your password now\r\n
 *
 *	and the rest of the sequence is the same.  note that a dummy password
 *	can be sent in place of the user password - it isn't verified.  The
 *	administrator's password is.
 *
 *	In case you are wondering - this extended protocol is used by the
 *	wwwpass.cgi perl script that chenry@dynamic.net.au has modified.
 */

#include <stdio.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <signal.h>
#include <unistd.h>
#include <varargs.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/file.h>
#include <time.h>
#include <syslog.h>

#define PF_PLATE        "./ptmp%06x"	/* original temporary file */
#define PF_TEMP         "./ptmp"	/* lock file */

#define PW_CHANGED      0               /* a OK */
#define PW_CANTLOCK     -1              /* error code -- locking failed */
#define PW_CANTOPEN     -2              /* error code -- open failed */
#define PW_CANTRENAME   -3              /* error code -- rename failed */
#define PW_NOCHANGE     -4              /* error code -- no passwd entry */
#define PW_CANTOPENT    -5              /* error code -- open failed for temporary file*/

#define SUCCESS 1
#define FAILURE 0
#define BUFSIZE 255
#define MAXARGS 32

#define FI_NULL ((FILE *) NULL)		/* NULL file pointer */
#define TI_NULL ((time_t *) NULL)	/* NULL time pointer */

char *pf_name = "passwd";		/* password file name */
char *admin_table = "poppassd.table";	/* the poppassd admin table */
char pf_tnam[128];			/* name of temporary file */

FILE *pf_fp;                            /* pointer to real password file */
FILE *pf_tfp;                           /* pointer to temporary file */
FILE *log;

/*	main() is where we handle the server/client protocol.
 *	We can handle either a user requesting to change their
 *	own password (from Eudora or www page) or an administrator
 *	to change the password for a user.
 */

main (argc, argv)
int argc;
char *argv[];
{
	char		line[BUFSIZE];
	char		admin[BUFSIZE];
	char		adminpass[BUFSIZE];
	char		user[BUFSIZE];
	char		pass[BUFSIZE];
	char		newpass[BUFSIZE];
	char		group_name[BUFSIZE];
	struct passwd	*pw;
	struct passwd	*adminpw;
	struct group	*group;

	chdir( "/etc" );
	strcpy( admin, "" );
	strcpy( adminpass, "" );
	strcpy( user, "" );
	strcpy( pass, "" );
	strcpy( newpass, "" );

	/*	We're using the syslogd logging facility.
	 *	All logging is done at the LOG_INFO level.
	 */

	openlog ("poppassd", LOG_PID, LOG_LOCAL2);

	WhoAreYou();

	WriteToClient( "200 hello, who are you?" );
	ReadFromClient(line);

	if ( sscanf(line,"admin %s %s",admin,adminpass) == 2 )
	{
		/*	We've got a request by an administrator to change
		 *	a users password.
		 */

		if ( strlen(admin) >= BUFSIZE )
		{
			WriteToClient("500 Attempting to overwrite my buffers ?");
			syslog(LOG_INFO,"Attempt to overwrite 'admin' buffer");
			exit(1);
		}
		if ( strlen(adminpass) >= BUFSIZE )
		{
			WriteToClient("500 Attempting to overwrite my buffers ?");
			syslog(LOG_INFO,"Attempt to overwrite 'adminpass' buffer");
			exit(1);
		}
		if ( strcmp(admin,"root") == 0 )
		{
			WriteToClient("500 Access by root not permitted.");
			syslog(LOG_INFO,"access by root not permitted");
			exit(1);
		}
		WriteToClient("200 please enter the user name");
		ReadFromClient(line);
	}

	if ( sscanf(line, "user %s", user) == 1 )
	{
		/*	We've got a request from a user to change their
		 *	own password (or this is a continuation of the
		 *	admin request).
		 */
	}
	else
	{
		WriteToClient("500 bad request: should be 'user name' or 'admin name pass'");
		syslog(LOG_INFO,"unrecognised request = [%s]",line);
		exit(1);
	}

	if ( strlen(user) == 0 )
	{
		WriteToClient("500 username required.");
		syslog(LOG_INFO,"No username supplied");
		exit(1);
	}
	if ( strlen(user) >= BUFSIZE )
	{
		WriteToClient("500 Attempting to overwrite my buffers ?");
		syslog(LOG_INFO,"Attempt to overwrite 'user' buffer");
		exit(1);
	}
	if ( strcmp(user,"root") == 0 )
	{
		WriteToClient("500 Can not change root password. Go away.");
		syslog(LOG_INFO,"Attempt to change root password");
		exit(1);
	}

	WriteToClient( "200 your password please." );
	ReadFromClient(line);
	sscanf( line, "pass %s", pass) ;
	if ( strlen(pass) == 0 )
	{
		WriteToClient( "500 password required." );
		syslog(LOG_INFO,"expected 'pass XXXXXX' not supplied");
		exit(1);
	}
	if ( strlen(pass) >= BUFSIZE )
	{
		WriteToClient("500 Attempting to overwrite my buffers ?");
		syslog(LOG_INFO,"Attempt to overwrite 'pass' buffer");
		exit(1);
	}

	WriteToClient( "200 your new password please." );
	ReadFromClient(line);
	sscanf( line, "newpass %s", newpass );
	if ( strlen(newpass) == 0 )
	{
		syslog(LOG_INFO,"expected 'newpass XXXXXX' not supplied");
		WriteToClient("500 new password required");
		exit(1);
	}
	if ( strlen(newpass) >= BUFSIZE )
	{
		WriteToClient("500 Attempting to overwrite my buffers ?");
		syslog(LOG_INFO,"Attempt to overwrite 'newpass' buffer");
		exit(1);
	}

	if ( strlen(admin) > 0 )
		syslog(LOG_INFO,"changing password: admin=[%s] user=[%s]",admin,user);
	else
		syslog(LOG_INFO,"changing password: user=[%s]",user);

	/* new pass must be 6 char or longer */

	if ( strlen(newpass) < 6 )
	{
		WriteToClient( "500 New password too short" );
		syslog(LOG_INFO,"new password too short");
		exit(1);
	}
	
	/* new pass must be different from old pass */

	if ( !strcmp(pass, newpass) )
	{
		WriteToClient( "500 New password must be different." );
		syslog(LOG_INFO,"new password must be different");
		exit(1);
	}

	/*	authenticate user or admin.
	 *	from a security point of view we should never tell a potential hacker
	 *	too much information - just tell them that it failed.
	 */

	if ( strlen(admin) > 0 )
	{
		/*	if the admin password checks out and the admin is
		 *	a valid administrator for this user then we don't
		 *	bother verifying the user password.  it still has
		 *	to be set to something though.
		 */

		if ( ( pw = getpwnam(user) ) == NULL )
		{
			WriteToClient( "500 Authentication failure");
			syslog(LOG_INFO,"unknown user");
			exit(1);
		}
		if ( ( group = getgrgid(pw->pw_gid) ) == NULL )
		{
			WriteToClient( "500 Authentication failure");
			syslog(LOG_INFO,"user belongs to unknown group");
			exit(1);
		}
		strcpy(group_name,group->gr_name);
		if ( ( adminpw = getpwnam(admin) ) == NULL )
		{
			WriteToClient( "500 Authentication failure");
			syslog(LOG_INFO,"unknown admin");
			exit(1);
		}
		if ( chkAdmin(admin,adminpass,group_name,adminpw) == FAILURE )
		{
			WriteToClient( "500 Authentication failure");
			exit(1);
		}
		syslog(LOG_INFO,"admin checks out ok");
		if ( ( pw = getpwnam(user) ) == NULL )
		{
			WriteToClient( "500 Authentication failure");
			syslog(LOG_INFO,"unknown user");
			exit(1);
		}
		if ( setPass( user, newpass, pw ) == FAILURE )
		{ 
			WriteToClient( "500 Unable to change password." );
			syslog(LOG_INFO,"unable to change password");
			exit(1);
		}
	}
	else
	{
		/*	we got a regular user request.  check the password.  */

		if ( (pw=getpwnam(user)) == NULL )
		{
			WriteToClient( "500 Authentication failure");
			syslog(LOG_INFO,"unknown user");
			exit(1);
		}

		if ( chkPass( user, pass, pw ) == FAILURE )
		{
			WriteToClient( "500 Authentication failure");
			syslog(LOG_INFO,"password incorrect");
			exit(1);
		}
		syslog(LOG_INFO,"user checks out ok");
		if ( setPass( user, newpass, pw ) == FAILURE )
		{ 
			WriteToClient( "500 Unable to change password." );
			syslog(LOG_INFO,"unable to change password");
			exit(1);
		}
	}

	WriteToClient( "200 Password changed, thank-you." );
	exit(0);
}

/*	Determine how this call was made and stick an entry
 *	in the log file.
 */
 
WhoAreYou()
{
	struct sockaddr_in	bobby;
	int	  		bobbyLen = sizeof(struct sockaddr);
	struct passwd		*pw;
	char			*inet_ntoa();
	char			*ttyname();

	if (isatty(fileno(stdin)))
	{
		pw = getpwuid(getuid());
		syslog(LOG_INFO,"Connection on %s by %s",ttyname(fileno(stdin)), pw->pw_name);
	}
	else if (lseek(fileno(stdin), 0, L_INCR) >= 0)
	{
		pw = getpwuid(getuid());
		syslog(LOG_INFO,"Connection on FILE by %s", pw->pw_name);
	}
	else if (getpeername(fileno(stdin),(struct sockaddr *)&bobby,&bobbyLen) < 0)
	{
		pw = getpwuid(getuid());
		syslog(LOG_INFO,"Connection on PIPE by %s", pw->pw_name);
	}
	else
	{
		syslog(LOG_INFO,"Network connection from %s",inet_ntoa(bobby.sin_addr));
	}
}

/*	Send a message to the client - notice we use a variable
 *	argument list passed into this function.
 */

WriteToClient( va_alist )
va_dcl
{
        va_list ap;
        char *fmt;      
        char *args[MAXARGS];
        int argno = 0;
        char string[BUFSIZE];
        
        va_start( ap );
        fmt = va_arg(ap, char *);
        while ((args[argno++] = va_arg(ap, char *)) != (char *)0) ;

        vfprintf(stdout, fmt, args);
        fputs("\r\n", stdout );
        fflush(stdout);

        va_end( ap );
}

/*	Read the client response into the character buffer
 *	supplied as a parameter.
 */

ReadFromClient(line)
char *line;
{
        char *sp;
        int i;

        strcpy(line, "" );
        fgets( line, BUFSIZE, stdin );
        if (( sp = strchr(line, '\n')) != NULL ) *sp = '\0'; 
        if (( sp = strchr(line, '\r')) != NULL ) *sp = '\0'; 
}

/*	This routine called if an administrator attempts to change
 *	a user password.  First we make sure that the administrator
 *	supplies a valid password for them selves.  Next we open
 *	the admin table and search it for an entry with the 
 *	administrators name in it.  If we do and the name of the
 *	group to which the user belongs is listed against the
 *	administrator's name then we authorise the password change.
 */

int chkAdmin(admin,adminpass,group_name,adminpw)
char *admin;
char *adminpass;
char *group_name;
struct passwd *adminpw;
{
	FILE	*fp;
        char	buffer[BUFSIZ];
	char	status[16], *ss, *list_of_groups;
	int	result = FAILURE;
	int	currentLine = 0;

	/*	Check the password first  */

	if ( strcmp(crypt(adminpass,adminpw->pw_passwd),adminpw->pw_passwd) != 0 )
	{
		syslog(LOG_INFO,"admin password incorrect");
		return (FAILURE);
	}

	/*	If the admin table can't be opened then we disable any
	 *	administrator functionality.
	 */

        fp = fopen(admin_table,"r"); 
	if ( fp == NULL )
	{
		syslog(LOG_INFO,"unable to open poppassd admin table file");
		return (FAILURE);
	}

	/*	Loop through each line of the admin table.  Skip lines that
	 *	are comments or start with white space.  Other lines must contain
	 *	a ':' seperating the admin name and the list of groups.
	 *
	 *	NOTE: there is a shortcoming in this code.  It doesn't correctly
	 *	handle two groups with similar names, eg, card and cardzone.  I'll
	 *	get around to it one day :)
	 */

        while( fgets(buffer,BUFSIZ,fp) != NULL )
	{
		currentLine++;
		if ( buffer[0] == '#' ) continue;
		if ( isspace(buffer[0]) ) continue;
		if ( sscanf(buffer,"status=%s",status) == 1 )
		{
			if ( strcmp(status,"off") == 0 )
			{
				syslog(LOG_INFO,"administrator table disabled");
				return(FAILURE);
			}
			continue;
		}
		for ( ss=buffer; *ss && *ss != ':'; ss++ );
		if ( *ss == '\0' )
		{
			syslog(LOG_INFO,"format error in line %d of admin table: missing ':'",currentLine);
			return(FAILURE);
		}
		*ss++ = '\0';
		list_of_groups = ss;
		if ( strstr(buffer,admin) == NULL ) continue;
		if ( strstr(list_of_groups,"*") != NULL ) return(SUCCESS);
		if ( strstr(list_of_groups,group_name) != NULL ) return(SUCCESS);
		syslog(LOG_INFO,"administrator does not have privileges for group=[%s]",group_name);
		return(FAILURE);
	}

	syslog(LOG_INFO,"not an administrator");
	fclose(fp);
	return (FAILURE);
}

/*	Make sure that the user password is valid
 */

int chkPass( user, pass, pw )
char *user;
char *pass;
struct passwd *pw;
{
        /*  Compare the supplied password with the password file entry */

        if (strcmp (crypt (pass, pw->pw_passwd), pw->pw_passwd) != 0)
	{
		syslog(LOG_INFO,"user password incorrect");
                return ( FAILURE );
	}
        else 
                return ( SUCCESS );
}

/*	Generate the salt for the password.
 */

makesalt(c)
char c[2];
{
        register long salt;             /* used to compute a salt */
        register int i;                 /* counter in a for loop */

        /*	just mix a few things up for the salt ...
         *	no rhyme or reason here
         */

        salt = (((long) time(TI_NULL))&0x3f) | (getpid() << 5);

        /*	use the bottom 12 bits and map them into the legal alphabet */

        for(i = 0; i < 2; i++)
        {
                c[i] = (salt & 0x3f) + '.';
                if (c[i] > '9') c[i] += 7;
                if (c[i] > 'Z') c[i] += 6;
                salt >>= 6;
        }
}

/*	This is a wrapper routine for UpdatePasswordFile().  It simply
 *	genrates the password salt, calls UpdatePasswordFile() and enters
 *	an entry in the log file.
 */

setPass( user, newpass, pw )
char *user;
char *newpass;
struct passwd *pw;
{

        char	saltc[2];          /* the password's salt */
        int	result;

        makesalt(saltc);
        /* strcpy( pw->pw_passwd, crypt(newpass, saltc)); */
	pw->pw_passwd = crypt(newpass,saltc);

	result = UpdatePasswordFile(pw);
	switch ( result )
	{
		case (PW_CANTOPENT):	syslog(LOG_INFO,"Can't open temporary passwpord file");
					return (FAILURE);
		case (PW_CANTLOCK):	syslog(LOG_INFO,"Can't create lock file");
					return (FAILURE);
		case (PW_CANTOPEN):	syslog(LOG_INFO,"Can't open the password file");
					return (FAILURE);
		case (PW_CHANGED):	syslog(LOG_INFO,"Password changed");
					return (SUCCESS);
		case (PW_CANTRENAME):	syslog(LOG_INFO,"Could not rename temporary file");
					return (FAILURE);
		case (PW_NOCHANGE):	syslog(LOG_INFO,"Password not changed");
					return (FAILURE);
		default:		syslog(LOG_INFO,"Unrecognised return from 'UpdatePasswordFile'");
					return (FAILURE);
	}
	
	return (FAILURE);
}

/*	I guess somebody should change this to handle shadow passwords :)
 *	Basically we make a new copy of the password file, changing the user
 *	account as we go.  At the end we rename the existing password file
 *	to something that can later be deleted (should be a cron job) and the
 *	new file is renamed to the password file.
 *
 *	NOTE: don't do anything tricky inside the sigoff() and sigon() block.
 */

UpdatePasswordFile(p)
struct passwd *p;					/* new password structure */
{
        char		nbuf[BUFSIZ];			/* buffer for passwords being read */
        char		obuf[BUFSIZ];			/* buffer for new entry */
        register char	*q, *s;				/* used for quick name comparison */
        int		retval = PW_NOCHANGE;		/* return value */
        char		keepName[BUFSIZ];

        sigoff();	/*	disable ALL signals at this point	*/

        /*	open the temporary password file	 */

        umask(022);

        sprintf(pf_tnam,PF_PLATE,getpid());
        sprintf(keepName,"passwd.%d",getpid());

        pf_tfp = fopen(pf_tnam, "w"); 
        if ((pf_tfp = fopen(pf_tnam, "w")) == FI_NULL)
        {
                retval = PW_CANTOPENT;
                goto cantlock;
        }

        /*	lock the password file	*/

        if (link(pf_tnam, PF_TEMP) < 0)
        {
                retval = PW_CANTLOCK;
                goto cantlock;
        }
        
        /*	copy the new password structure	*/
        
        sprintf(obuf, "%s:%s:%d:%d:%s:%s:%s\n",
                        p->pw_name, p->pw_passwd, p->pw_uid, p->pw_gid,
                        p->pw_gecos, p->pw_dir, p->pw_shell);

	/*	open the password file	*/

        if ((pf_fp = fopen(pf_name, "r")) == FI_NULL)
        {
                retval = PW_CANTOPEN;
                goto getout;
        }
        
        /*	copy the password file into the temporary one	*/

        while(fgets(nbuf, BUFSIZ, pf_fp) != NULL)
        {
                for(s = nbuf, q = p->pw_name; *s && *s != ':'; s++, q++)
                        if (*s != *q)
                                break;
                if (*s == ':' && *q == '\0')
                {
                        fputs(obuf, pf_tfp);
                        retval = PW_CHANGED;
                } else 
                        fputs(nbuf, pf_tfp);
        }

        if (retval == PW_NOCHANGE)
        {
                goto getout;
        }

        /*	close the temporary file and the real one	*/

        (void) fclose(pf_tfp);
        pf_tfp = FI_NULL;
        (void) fclose(pf_fp);
        pf_fp = FI_NULL;

        /*	now relink; note the lock file is still there	*/

        if ( rename(pf_name,keepName) >= 0 && link(pf_tnam, pf_name) >= 0 && unlink(pf_tnam) >= 0)
        {
                retval = PW_CHANGED;
        }
        else
                retval = PW_CANTRENAME;

getout:
        /*	Only remove lock file if this program obtained it	*/

         (void) unlink(PF_TEMP); 

cantlock:
        /* some clean up */
        if (pf_tfp != FI_NULL)
                (void) fclose(pf_tfp);
        if (pf_fp != FI_NULL)
                (void) fclose(pf_fp);
        (void) unlink(pf_tnam);

        sigon(); /* re-enable ALL signals at this point */

        return(retval);
}


/*	signal handling routines	*/

void (*trap[NSIG])();           /* values returned by signal() */


/*	disable ALL signal trapping	*/
 
sigoff()
{
        register int i;

        for(i = 0; i < NSIG; i++)
                (void) signal(i, SIG_IGN);
}

/*	restore to the initial setting all signals	*/

sigon()
{
        register int i;

        for(i = 0; i < NSIG; i++)
                (void) signal(i, trap[i]);

}





