/* 
 * yppasswdd - 0.1
 * Copyright 1994 Olaf Kirch, <okir@monad.swb.de>
 *
 * This program is covered by the GNU General Public License, version 2.
 * It is provided in the hope that it is useful. However, the author
 * disclaims ALL WARRANTIES, expressed or implied. See the GPL for details.
 */

#include <sys/types.h>
#include <sys/errno.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <termios.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <time.h>
#include <pwd.h>

#include <getopt.h>
#include <syslog.h>
#include <stdio.h>
#include <string.h>

/*
#include <rpcsvc/yppasswd.h>
*/
#include <rpc/rpc.h>
#include <rpc/pmap_clnt.h>
#include "yppasswd.h"


/* How often to retry locking the passwd file...
 */
#define MAX_RETRIES 5

/* Fork this process to have the NIS maps rebuilt. The second
 * define gives argv[0].
 */
#define MAP_UPDATE	"/usr/sbin/yp.pwupdate"
#define MAP_UPDATE0	"yp.pwupdate"

#ifdef DEBUG
#define RPC_SVC_FG
#define _PATH_PASSWD    "/tmp/passwd"
#define _PATH_OLDPASSWD "/tmp/passwd.OLD"
#define _PATH_PTMP  "/tmp/ptmp"
#else
#ifndef _PATH_PASSWD
#define _PATH_PASSWD    "/etc/passwd"
#endif
#ifndef _PATH_OLDPASSWD
#define _PATH_OLDPASSWD "/etc/passwd.OLD"
#endif
#ifndef _PATH_PTMP
#define _PATH_PTMP  "/etc/ptmp"
#endif
#endif

#define file_setpwent   _setpwent
#define file_getpwent   _getpwent
#define file_endpwent   _endpwent

static char *program_name = "";
static char *version = "yppsswdd 0.1";

#define xprt_addr(xprt)	(svc_getcaller(xprt)->sin_addr)
#define xprt_port(xprt)	ntohs(svc_getcaller(xprt)->sin_port)
void yppasswdprog_1( struct svc_req *rqstp, SVCXPRT *transp );
void reaper( int sig );

/*===============================================================*
 * The update handler
 *===============================================================*/
int *
yppasswdproc_update_1(yppasswd *yppw, struct svc_req *rqstp)
{
    struct xpasswd *newpw;	/* passwd struct passed ba the client */
    struct passwd *pw;		/* passwd struct obtained from getpwent() */
    static int	res;
    int		retries, gotit, fd, c;
    char	logbuf[255];
    FILE	*oldfp, *newfp;

    res = 1;

    sprintf( logbuf, "update %.12s (uid=%d) from host %s",
			    yppw->newpw.pw_name,
			    yppw->newpw.pw_uid,
			    inet_ntoa(xprt_addr(rqstp->rq_xprt)));
#ifdef DEBUG
    setpwfile(_PATH_PASSWD);
#endif

    /* Lock the passwd file. We retry several times. Maybe it would be
     * better to just return an error condition and have the client reattempt
     * instead? This procedure is already slow enough...
     */
    retries = 0;
    while ((fd = open(_PATH_PTMP, O_CREAT|O_WRONLY|O_EXCL)) < 0
      && errno == EEXIST && retries < MAX_RETRIES) {
        sleep (1);
        retries++;
    }

    if (retries == MAX_RETRIES) {
        syslog ( LOG_NOTICE, "%s failed", logbuf );
        syslog ( LOG_NOTICE, "password file locked" );
        return &res;
    }
    
    if (fd < 0 || (newfp = fdopen(fd, "w")) == NULL) {
        syslog ( LOG_ERR, "%s failed", logbuf );
        syslog ( LOG_ERR, "Can't create %s. %m", _PATH_PTMP );
        close (fd);
        return &res;
    }

    /* Open the passwd file for reading. We can't use getpwent and friends
     * here, because they go through the YP maps, too.
     */
    if ((oldfp = fopen(_PATH_PASSWD, "r")) == NULL) {
        syslog ( LOG_ERR, "%s failed", logbuf );
        syslog ( LOG_ERR, "Can't open %s: %m", _PATH_PASSWD );
        fclose (newfp);
        return &res;
    }


    gotit = 0;
    newpw = &yppw->newpw;

    /*
     * Loop over all passwd entries
     */
    while((pw = fgetpwent(oldfp)) != NULL) {

        /* check if this is the uid we want to change. A few
         * sanity checks added for consistency.
         */
        if (newpw->pw_uid == pw->pw_uid && newpw->pw_gid == pw->pw_gid
         && !strcmp(newpw->pw_name, pw->pw_name) && !gotit) {

            /* Check the password.
             */
            if (strcmp(crypt(yppw->oldpass, pw->pw_passwd), pw->pw_passwd)) {
        	syslog ( LOG_ERR, "%s rejected", logbuf );
                syslog ( LOG_ERR, "Invalid password." );
                break;
            }

            /* set the new passwd
             */
            pw->pw_passwd = newpw->pw_passwd;
            gotit++;
        }

        /* write the passwd entry to /etc/ptmp
         */
        if (putpwent(pw, newfp) < 0) {
            syslog ( LOG_ERR, "%s failed", logbuf );
            syslog ( LOG_ERR, "Error while writing new password file: %m" );
            break;
        }
        /* fflush (newfp); */
    }
    fclose (newfp);
    fclose (oldfp);

    /* Check if we dropped out of the loop because of an error.
     * If so, return an error to the client.
     */
    if (pw != NULL) {
        unlink (_PATH_PTMP);
        return (&res);
    }

    /* Check whether we actually changed anything
     */
    if (!gotit) {
        syslog ( LOG_ERR, "%s failed", logbuf );
        syslog ( LOG_ERR, "User not in password file." );
        unlink (_PATH_PTMP);
        return (&res);
    }

    unlink (_PATH_OLDPASSWD);
    link (_PATH_PASSWD, _PATH_OLDPASSWD);
    unlink (_PATH_PASSWD);
    link (_PATH_PTMP, _PATH_PASSWD);
    unlink (_PATH_PTMP);
    chmod (_PATH_PASSWD, 0644);

    /* Fork off process to rebuild NIS passwd.* maps. If the fork
     * fails, restore old passwd file and return an error.
     */
    if ((c = fork()) < 0) {
    	unlink( _PATH_PASSWD );
    	link( _PATH_OLDPASSWD, _PATH_PASSWD );
    	syslog( LOG_ERR, "%s failed", logbuf );
    	syslog( LOG_ERR, "Couldn't fork map update process: %m" );
    	return (&res);
    }
    if (c == 0) {
    	execlp(MAP_UPDATE, MAP_UPDATE0, NULL);
    	exit(1);
    }

    syslog ( LOG_INFO, "%s successful. Password changed", logbuf );
    res = 0;

    return (&res);
}

static void
usage(FILE *fp, int n)
{
    fprintf (fp, "usage: %s [-h] [-v]\n", program_name );
    exit(n);
}

void
reaper( int sig )
{
    wait(NULL);
}

void
install_reaper( void )
{
    struct sigaction act, oact;

    act.sa_handler = reaper;
    act.sa_mask = 0;
    act.sa_flags = SA_RESTART;
    sigaction( SIGCHLD, &act, &oact );
}


int
main(int argc, char **argv)
{
    SVCXPRT *transp;
    int opterr;
    int c;

    program_name = argv[0];

    /* Parse the command line options and arguments. */
    opterr = 0;
    while ((c = getopt(argc, argv, "dv")) != EOF)
        switch (c) {
        case 'h':
            usage (stdout, 0);
            break;
        case 'v':
            printf("%s\n", version);
            exit(0);
        case 0:
            break;
        case '?':
        default:
            usage(stderr, 1);
        }

    /* No more arguments allowed. */
    if (optind != argc)
        usage(stderr, 1);

#ifndef RPC_SVC_FG
    /* We first fork off a child. */
    if ((c = fork()) > 0)
        exit(0);
    if (c < 0) {
        fprintf(stderr, "yppasswdd: cannot fork: %s\n", strerror(errno));
        exit(-1);
    }
    /* Now we remove ourselves from the foreground. */
    (void) close(0);
    (void) close(1);
    (void) close(2);
#ifdef TIOCNOTTY
    if ((c = open("/dev/tty", O_RDWR)) >= 0) {
        (void) ioctl(c, TIOCNOTTY, (char *) NULL);
        (void) close(c);
    }
#else
    setsid();
#endif
#endif /* not RPC_SVC_FG */

    /* Initialize logging.
     */
    openlog ( "yppasswdd", LOG_PID, LOG_AUTH );

    /* Register a signal handler to reap children after they terminated
     */
    install_reaper();

    /*
     * Create the RPC server
     */
    (void)pmap_unset(YPPASSWDPROG, YPPASSWDVERS);

    transp = svcudp_create(RPC_ANYSOCK);
    if (transp == NULL) {
        (void)fprintf(stderr, "cannot create udp service.\n");
        exit(1);
    }
    if (!svc_register(transp, YPPASSWDPROG, YPPASSWDVERS, yppasswdprog_1,
            IPPROTO_UDP)) {
        (void)fprintf(stderr, "unable to register yppaswdd udp service.\n");
        exit(1);
    }

    /*
     * Run the server
     */
    svc_run();
    (void)fprintf(stderr, "svc_run returned\n");

    return 1;
}

