/*
 * Copyright 2000, International Business Machines Corporation and others.
 * All Rights Reserved.
 *
 * This software has been released under the terms of the IBM Public
 * License.  For details, see the LICENSE file in the top-level source
 * directory or online at http://www.openafs.org/dl/license10.html
 */

/*
 * ALL RIGHTS RESERVED
 */

/* These two needed for rxgen output to work */
#include <afsconfig.h>
#include <afs/param.h>
#include <afs/stds.h>
#include <afs/opr.h>

#include <roken.h>

#ifdef AFS_NT40_ENV
#include <afs/fs_utils.h>
#else
#include <afs/venus.h>
#endif

#include <rx/xdr.h>
#include <afs/prs_fs.h>
#include <afs/sys_prototypes.h>
#include <afs/afs_consts.h>

#include "kkids.h"

#define MAXNAME 100

static int using_child = 0;
static FILE *childin, *childout;	/* file pointers on pipe to kpwvalid */

/* this removes symlinks from the tail end of path names.
 * PRECONDITION: name must be either absolute ('/something') or
 * explictly relative to current directory ('./something')
 */
static int
simplify_name(char *orig_name, char *true_name)
{
    struct stat statbuff;


#ifdef AFS_NT40_ENV
    if (stat(orig_name, &statbuff) < 0) {
	*true_name = '\0';
	return 0;
    } else {
	strcpy(true_name, orig_name);
	return 1;
    }
#else /* !NT40 */
    {
	int link_chars_read;
	char *last_component;
	if (lstat(orig_name, &statbuff) < 0) {
	    /* if lstat fails, it's possible that it's transient, but
	     * unlikely.  Let's hope it isn't, and continue... */
	    *true_name = '\0';
	    return 0;
	}

	/*
	 * The lstat succeeded.  If the given file is a symlink, substitute
	 * the contents of the link for the file name.
	 */
	if ((statbuff.st_mode & S_IFMT) == S_IFLNK) {
	    link_chars_read = readlink(orig_name, true_name, 1024);
	    if (link_chars_read <= 0) {
		*true_name = '\0';
		return 0;
	    }

	    true_name[link_chars_read++] = '\0';

	    /*
	     * If the symlink is an absolute pathname, we're fine.  Otherwise, we
	     * have to create a full pathname using the original name and the
	     * relative symlink name.  Find the rightmost slash in the original
	     * name (we know there is one) and splice in the symlink contents.
	     */
	    if (true_name[0] != '/') {
		last_component = (char *)strrchr(orig_name, '/');
		strcpy(++last_component, true_name);
		strcpy(true_name, orig_name);
	    }
	} else
	    strcpy(true_name, orig_name);

	return 1;		/* found it */
    }
#endif /* !NT40 */
}


  /* We find our own location by:
   * 1. checking for an absolute or relative path name in argv[0]
   *         this is moderately system-dependant: argv[0] is just a convention.
   * 2. successively checking each component of PATH, and concatenating argv[0]
   *    onto it, then stating the result.
   * if it exists, it must be us, eh?
   * NB there may be possible security implications involving
   * symlinks; I think they are only relevant if the symlink points
   * directly at kpasswd, not when it points at kpasswd's parent directory.
   */
static int
find_me(char *arg, char *parent_dir)
{
    char *bp;			/* basename pointer */
    char *dp;			/* dirname pointer */
    char *pathelt, orig_name[1024], truename[1022];

#define explicitname(a,b,c) \
        (  ((a) == '/') ||                                \
           (   ((a) == '.') &&                            \
               (   ((b) == '/') ||                        \
                   (   ((b) == '.') && ((c) == '/') )       \
               )                                        \
            )                                           \
         )

    if (strlen(arg) > 510)	/* just give up */
	return 0;

    *parent_dir = '\0';
    truename[0] = '\0';

    if (explicitname(arg[0], arg[1], arg[2])) {
	strcpy(orig_name, arg);
	simplify_name(orig_name, truename);
    } else {
	bp = (char *)strrchr(arg, '/');
	if (bp) {
	    orig_name[0] = '.';
	    orig_name[1] = '/';
	    strcpy(orig_name + 2, arg);
	    simplify_name(orig_name, truename);
	}
    }

    if (!truename[0]) {		/* didn't find it */
	char path[2046];

	dp = getenv("PATH");
	if (!dp)
	    return 0;
	strncpy(path, dp, 2045);

	for (pathelt = strtok(path, ":"); pathelt;
	     pathelt = strtok(NULL, ":")) {
	    strncpy(orig_name, pathelt, 510);

	    bp = orig_name + strlen(orig_name);
	    *bp = '/';		/* replace NUL with / */
	    strncpy(bp + 1, arg, 510);

	    if (simplify_name(orig_name, truename))
		break;
	}
    }
    if (!truename[0])		/* didn't find it */
	return 0;		/* give up */

    /* DID FIND IT */
    /*
     * Find rightmost slash, if any.
     */
    bp = (char *)strrchr(truename, '/');
    if (bp) {
	/*
	 * Found it.  Designate everything before it as the parent directory,
	 * everything after it as the final component.
	 */
	strncpy(parent_dir, truename, bp - truename);
	parent_dir[bp - truename] = 0;
    } else {
	/*
	 * No slash appears in the given file name.  Set parent_dir to the current
	 * directory, and the last component as the given name.
	 */
	strcpy(parent_dir, ".");
    }

    return 1;			/* found it */
}

#define SkipLine(str) do { while (*str != '\0' && *str !='\n') str++; if (*str == '\n') str++; } while(0)

/* this function returns TRUE (1) if the file is in AFS, otherwise false (0) */
static int
InAFS(char *apath)
{
    struct ViceIoctl blob;
    afs_int32 code;
    char space[AFS_PIOCTL_MAXSIZE];

    blob.in_size = 0;
    blob.out_size = AFS_PIOCTL_MAXSIZE;
    blob.out = space;

    code = pioctl(apath, VIOC_FILE_CELL_NAME, &blob, 1);
    if (code) {
	if ((errno == EINVAL) || (errno == ENOENT))
	    return 0;
    }
    return 1;
}

struct Acl {
    int nplus;
    int nminus;
    struct AclEntry *pluslist;
    struct AclEntry *minuslist;
};

struct AclEntry {
    struct AclEntry *next;
    char name[MAXNAME];
    afs_int32 rights;
};

static struct Acl *
ParseAcl(char *astr)
{
    int nplus = 0, nminus = 0, i, trights = 0;
    char tname[MAXNAME + 1] = "";
    struct AclEntry *first, *last, *tl;
    struct Acl *ta;
    sscanf(astr, "%d", &nplus);
    SkipLine(astr);
    sscanf(astr, "%d", &nminus);
    SkipLine(astr);

    ta = calloc(sizeof(*ta), 1);
    ta->nplus = nplus;

    last = 0;
    first = 0;
    for (i = 0; i < nplus; i++) {
	sscanf(astr, "%" opr_stringize(MAXNAME) "s %d", tname, &trights);
	SkipLine(astr);
	tl = malloc(sizeof(struct AclEntry));
	if (!first)
	    first = tl;
	strcpy(tl->name, tname);
	tl->rights = trights;
	tl->next = 0;
	if (last)
	    last->next = tl;
	last = tl;
    }
    ta->pluslist = first;

    return ta;
}

static char *
safestrtok(char *str, char *tok)
{
    char *temp;

    if (str)
	return (strtok(str, tok));

    temp = strtok(NULL, tok);
    if (temp)
	*(temp - 1) = *tok;

    return temp;

}


/* If it exists, we do some fussing about whether or not this
 * is a reasonably secure path - not that it makes *much* difference, since
 * there's not much point in being more secure than the kpasswd executable.
 */
/* 1.  is this directory in AFS?
 * 2.  Is every component of the pathname secure
 *     (ie, only system:administrators have w or a rights)?
 */
static int
is_secure(char *dir)
{
    char *temp;
    struct ViceIoctl blob;
    struct AclEntry *te;
    char space[2046];
    afs_int32 code;
    struct Acl *ta;

    if (!InAFS(dir))		/* final component *must* be in AFS */
	return 0;

#ifndef INSECURE
    for (temp = safestrtok(dir, "/"); temp; temp = safestrtok(NULL, "/")) {
	/* strtok keeps sticking NUL in place of /, so we can look at
	 * ever-longer chunks of the path.
	 */
	if (!InAFS(dir))
	    continue;

	blob.out_size = AFS_PIOCTL_MAXSIZE;
	blob.in_size = 0;
	blob.out = space;
	code = pioctl(dir, VIOCGETAL, &blob, 1);
	if (code) {
	    continue;
	}
	ta = ParseAcl(space);
	if (ta->nplus <= 0)
	    continue;

	for (te = ta->pluslist; te; te = te->next) {
	    if (((te->rights & PRSFS_INSERT) && (te->rights & PRSFS_DELETE))
		|| (te->rights & (PRSFS_WRITE | PRSFS_ADMINISTER)))
		if (strcmp(te->name, "system:administrators"))
		    return 0;	/* somebody who we can't trust has got power */
	}
    }
#endif /* INSECURE */

    return 1;
}

/* Then, once we've found our own location, we look for a program named
 * kpwvalid.
 */

/* look for a password-checking program named kpwvalid.
 * It has to be in a secure place (same place as this executable)
 */
static int
kpwvalid_is(char *dir)
{
    struct stat statbuff;
    int len;

    len = (int)strlen(dir);
    strcpy(dir + len, "/kpwvalid");

    if (stat(dir, &statbuff) < 0) {
	/* if lstat fails, it's possible that it's transient, but
	 * unlikely.  Let's hope it isn't, and continue... */
	*(dir + len) = '\0';
	return 0;
    }

    *(dir + len) = '\0';
    return 1;
}

#ifdef AFS_NT40_ENV
/* We don't allow the use of kpwvalid executable scripts to set policy
 * for passwd changes.
 */
int
init_child(char *myname)
{

    using_child = 0;
    return using_child;

}
#else /* !NT40 */
int
init_child(char *myname)
{
    int pipe1[2], pipe2[2];
    pid_t pid;
    char dirpath[1024];
    char *argv[2];

    if (!(find_me(myname, dirpath) && is_secure(dirpath)
	  && kpwvalid_is(dirpath))) {
	using_child = 0;
	return 0;
    }

    /* make a couple of pipes, one for the child's stdin, and the other
     * for the child's stdout.  The parent writes to the former, and
     * reads from the latter, the child reads from the former, and
     * writes to the latter.
     */
    if (pipe(pipe1) || pipe(pipe2)) {
	using_child = 0;
	return 0;
    }

    /* fork a child */
    pid = fork();
    if (pid == -1) {
	using_child = 0;
	perror("kpasswd: can't fork because ");
	return (using_child);
    }
    if (pid == 0) {		/* in child process */
	/* tie stdin and stdout to these pipes */
	/* if dup2 doesn't exist everywhere, close and then dup, but make */
	/* sure that you really get stdin or stdout from the dup. */
	if ((-1 == dup2(pipe1[0], 0)) || (-1 == dup2(pipe2[1], 1))) {
	    perror("kpasswd: can't exec kpwvalid because ");
	    exit(-1);
	}

	strcat(dirpath, "/kpwvalid");
	argv[1] = NULL;
	argv[0] = dirpath;
	execv(dirpath, argv);
	return 0;
    } else {
	using_child = pid;	/* save it for later */
	childin = fdopen(pipe1[1], "w");
	childout = fdopen(pipe2[0], "r");
	return (using_child);
    }
}
#endif /* not NT40 */

int
password_bad(char *pw)
{
    int rc;
    rc = 0;

    if (using_child) {
	fprintf(childin, "%s\n", pw);
	fflush(childin);
	if (fscanf(childout, "%d", &rc) < 1)
	    rc = -1;
    }

    return (rc);
}

/* this is originally only used to give the child the old password, so she
 * can compare putative new passwords against it.
 */
int
give_to_child(char *pw)
{
    int rc;
    rc = 0;

    if (using_child) {
	fprintf(childin, "%s\n", pw);
	fflush(childin);
    }

    return (rc);
}

/* quickly and painlessly
 */
int
terminate_child(void)
{
    int rc;
    rc = 0;

#ifndef AFS_NT40_ENV
    if (using_child) {
	rc = kill(using_child, SIGKILL);
    }
#endif
    return (rc);
}
