/*
 * $Id: wtmp.c,v 1.11 1999/01/27 16:27:16 saw Rel $
 */

#define RHOST_UNKNOWN_NAME        ""     /* perhaps "[from.where?]" */

#define DEVICE_FILE_PREFIX        "/dev/"

#define WTMP_LOCK_TIMEOUT         3      /* in seconds */

#include <fcntl.h>
#include <netdb.h>
#include <paths.h>
#include <utmp.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <malloc.h>
#include <unistd.h>

#ifndef UT_IDSIZE
#define UT_IDSIZE 4            /* XXX - this is sizeof(struct utmp.ut_id) */
#endif

#include "../include/wtmp.h"

#ifdef DEBUG
#include <security/pam_misc.h>
#else
#define D(x)
#endif

/*
 * Find entry for this terminal (if there is one).
 * Utmp file should have been opened and rewinded for the call.
 *
 * XXX: the search should be more or less compatible with libc one.
 * The caller expects that pututline with the same arguments
 * will replace the found entry.
 */

static
const struct utmp *find_utmp_entry(const char *ut_line
        , const char *ut_id)
{
    struct utmp *u_tmp_p;

    while ((u_tmp_p = getutent()) != NULL)
	if ((u_tmp_p->ut_type == INIT_PROCESS ||
             u_tmp_p->ut_type == LOGIN_PROCESS ||
             u_tmp_p->ut_type == USER_PROCESS ||
             u_tmp_p->ut_type == DEAD_PROCESS) &&
            !strncmp(u_tmp_p->ut_id, ut_id, UT_IDSIZE) &&
            !strncmp(u_tmp_p->ut_line, ut_line, UT_LINESIZE))
                break;

    return u_tmp_p;
}

/*
 * Identify the terminal name and the abreviation we will use.
 */

static
void set_terminal_name(const char *terminal, char *ut_line, char *ut_id)
{
    memset(ut_line, 0, UT_LINESIZE);
    memset(ut_id, 0, UT_IDSIZE);

    /* set the terminal entry */
    if ( *terminal == '/' ) {     /* now deal with filenames */
	int o1, o2;

	o1 = strncmp(DEVICE_FILE_PREFIX, terminal, 5) ? 0 : 5;
	if (!strncmp("/dev/tty", terminal, 8)) {
	    o2 = 8;
	} else {
	    o2 = strlen(terminal) - sizeof(UT_IDSIZE);
	    if (o2 < 0)
		o2 = 0;
	}

	strncpy(ut_line, terminal + o1, UT_LINESIZE);
	strncpy(ut_id, terminal + o2, UT_IDSIZE);
    } else if (strchr(terminal, ':')) {  /* deal with X-based session */
	const char *suffix;

	suffix = strrchr(terminal,':');
	strncpy(ut_line, terminal, UT_LINESIZE);
	strncpy(ut_id, suffix, UT_IDSIZE);
    } else {	                         /* finally deal with weird terminals */
	strncpy(ut_line, terminal, UT_LINESIZE);
	ut_id[0] = '?';
	strncpy(ut_id + 1, terminal, UT_IDSIZE - 1);
    }
}

/*
 * Append an entry to wtmp. See utmp_open_session for the return convention.
 * Be carefull: the function uses alarm().
 */

#define WWTMP_STATE_BEGINNING     0
#define WWTMP_STATE_FILE_OPENED   1
#define WWTMP_STATE_SIGACTION_SET 2
#define WWTMP_STATE_LOCK_TAKEN    3

static
int write_wtmp(struct utmp *u_tmp_p
                      , const char **callname, const char **err_descr)
{
    int w_tmp_fd;
    struct flock w_lock;
    struct sigaction act1, act2;
    int state;
    int retval;

    state = WWTMP_STATE_BEGINNING;
    retval = 1;

    do {
        D(("writing to wtmp"));
        w_tmp_fd = open(_PATH_WTMP, O_APPEND|O_WRONLY);
        if (w_tmp_fd == -1) {
            *callname = "wtmp open";
            *err_descr = strerror(errno);
            break;
        }
        state = WWTMP_STATE_FILE_OPENED;

        /* prepare for blocking operation... */
        act1.sa_handler = SIG_DFL;
        (void) sigemptyset(&act1.sa_mask);
        act1.sa_flags = 0;
        if (sigaction(SIGALRM, &act1, &act2) == -1) {
            *callname = "sigaction";
            *err_descr = strerror(errno);
            break;
        }
        (void) alarm(WTMP_LOCK_TIMEOUT);
        state = WWTMP_STATE_SIGACTION_SET;

        /* now we try to lock this file-rcord exclusively; non-blocking */
        memset(&w_lock, 0, sizeof(w_lock));
        w_lock.l_type = F_WRLCK;
        w_lock.l_whence = SEEK_END;
        if (fcntl(w_tmp_fd, F_SETLK, &w_lock) < 0) {
            D(("locking %s failed.", _PATH_WTMP));
            *callname = "fcntl(F_SETLK)";
            *err_descr = strerror(errno);
            break;
        }
        (void) alarm(0);
        (void) sigaction(SIGALRM, &act2, NULL);
        state = WWTMP_STATE_LOCK_TAKEN;

        if (write(w_tmp_fd, u_tmp_p, sizeof(struct utmp)) != -1)
            retval = 0;

    } while(0); /* it's not a loop! */

    if (state >= WWTMP_STATE_LOCK_TAKEN) {
        w_lock.l_type = F_UNLCK;               /* unlock wtmp file */
        (void) fcntl(w_tmp_fd, F_SETLK, &w_lock);
    }else if (state >= WWTMP_STATE_SIGACTION_SET) {
        (void) alarm(0);
        (void) sigaction(SIGALRM, &act2, NULL);
    }

    if (state >= WWTMP_STATE_FILE_OPENED) {
        close(w_tmp_fd);                       /* close wtmp file */
        D(("wtmp written"));
    }

    return retval;
}

/*
 * XXX - if this gets turned into a module, make this a
 * pam_data item. You should put the pid in the name so we can
 * "probably" nest calls more safely...
 */
struct utmp *login_stored_utmp=NULL;

/*
 * Returns:
 *   0     ok,
 *   1     non-fatal error
 *  -1     fatal error
 *  callname and err_descr will be set
 * Be carefull: the function indirectly uses alarm().
 */
int utmp_do_open_session(const char *user, const char *terminal
                        , const char *rhost, pid_t pid
                        , const char **callname, const char **err_descr)
{
    struct utmp u_tmp;
    const struct utmp *u_tmp_p;
    char ut_line[UT_LINESIZE], ut_id[UT_IDSIZE];
    int retval;

    set_terminal_name(terminal, ut_line, ut_id);

    utmpname(_PATH_UTMP);
    setutent();                                           /* rewind file */
    u_tmp_p = find_utmp_entry(ut_line, ut_id);

    /* reset new entry */
    memset(&u_tmp, 0, sizeof(u_tmp));                     /* reset new entry */
    if (u_tmp_p == NULL) {
	D(("[NEW utmp]"));
    } else {
	D(("[OLD utmp]"));

	/*
	 * here, we make a record of the former entry. If the
	 * utmp_close_session code is attatched to the same process,
	 * the wtmp will be replaced, otherwise we leave init to pick
	 * up the pieces.
	 */
	if (login_stored_utmp == NULL) {
	    login_stored_utmp = malloc(sizeof(struct utmp));
            if (login_stored_utmp == NULL) {
                *callname = "malloc";
                *err_descr = "fail";
                endutent();
                return -1;
            }
	}
        memcpy(login_stored_utmp, u_tmp_p, sizeof(struct utmp));
    }

    /* we adjust the entry to reflect the current session */
    {
	strncpy(u_tmp.ut_line, ut_line, UT_LINESIZE);
	memset(ut_line, 0, UT_LINESIZE);
	strncpy(u_tmp.ut_id, ut_id, UT_IDSIZE);
	memset(ut_id, 0, UT_IDSIZE);
	strncpy(u_tmp.ut_user, user
		, sizeof(u_tmp.ut_user));
	strncpy(u_tmp.ut_host, rhost ? rhost : RHOST_UNKNOWN_NAME
		, sizeof(u_tmp.ut_host));

	/* try to fill the host address entry */
	if (rhost != NULL) {
	    struct hostent *hptr;

	    /* XXX: it isn't good to do DNS lookup here...  1998/05/29  SAW */
            hptr = gethostbyname(rhost);
	    if (hptr != NULL && hptr->h_addr_list) {
		memcpy(&u_tmp.ut_addr, hptr->h_addr_list[0]
		       , sizeof(u_tmp.ut_addr));
	    }
	}

	/* we fill in the remaining info */
	u_tmp.ut_type = USER_PROCESS;          /* a user process starting */
	u_tmp.ut_pid = pid;                    /* session identifier */
	u_tmp.ut_time = time(NULL);
    }

    setutent();                                /* rewind file (replace old) */
    pututline(&u_tmp);                         /* write it to utmp */
    endutent();                                /* close the file */

    retval = write_wtmp(&u_tmp, callname, err_descr); /* write to wtmp file */
    memset(&u_tmp, 0, sizeof(u_tmp));          /* reset entry */

    return retval;
}

int utmp_do_close_session(const char *terminal
                              , const char **callname, const char **err_descr)
{
    int retval;
    struct utmp u_tmp;
    const struct utmp *u_tmp_p;
    char ut_line[UT_LINESIZE], ut_id[UT_IDSIZE];

    retval = 0;

    set_terminal_name(terminal, ut_line, ut_id);

    utmpname(_PATH_UTMP);
    setutent();                                              /* rewind file */

    /*
     * if there was a stored entry, return it to the utmp file, else
     * if there is a session to close, we close that
     */
    if (login_stored_utmp) {
	pututline(login_stored_utmp);

	memcpy(&u_tmp, login_stored_utmp, sizeof(u_tmp));
	u_tmp.ut_time = time(NULL);            /* a new time to restart */

        retval = write_wtmp(&u_tmp, callname, err_descr);

	memset(login_stored_utmp, 0, sizeof(u_tmp)); /* reset entry */
	free(login_stored_utmp);
    } else {
        u_tmp_p = find_utmp_entry(ut_line, ut_id);
        if (u_tmp_p != NULL) {
            memset(&u_tmp, 0, sizeof(u_tmp));
            strncpy(u_tmp.ut_line, ut_line, UT_LINESIZE);
            strncpy(u_tmp.ut_id, ut_id, UT_IDSIZE);
            memset(&u_tmp.ut_user, 0, sizeof(u_tmp.ut_user));
            memset(&u_tmp.ut_host, 0, sizeof(u_tmp.ut_host));
            u_tmp.ut_addr = 0;
            u_tmp.ut_type = DEAD_PROCESS;      /* `old' login process */
            u_tmp.ut_pid = 0;
            u_tmp.ut_time = time(NULL);
            setutent();                        /* rewind file (replace old) */
            pututline(&u_tmp);                 /* mark as dead */

            retval = write_wtmp(&u_tmp, callname, err_descr);
        }
    }

    /* clean up */
    memset(ut_line, 0, UT_LINESIZE);
    memset(ut_id, 0, UT_IDSIZE);

    endutent();                                /* close utmp file */
    memset(&u_tmp, 0, sizeof(u_tmp));          /* reset entry */

    return 0;
}
