/* vim: set shiftwidth=4 tabstop=8 softtabstop=4: */
/* $Id */

#define LIBSPT_STATIC
#include "sptprivate.h"
#include <stdio.h>
#include <sys/stat.h>
#include <grp.h>
#include <pwd.h>
#include <time.h>
#include <signal.h>
#ifdef HAVE_LASTLOG_H
# include <lastlog.h>
#endif

/*
 * ====================================
 * macros
 * ====================================
 */

#if UTMP_TYPE == UTMP_TYPE_BSD
# include <utmp.h>
# define NEED_LOCK_FUNC
#elif UTMP_TYPE == UTMP_TYPE_SYSV
# ifdef HAVE_UTMPX
#  define UTMP utmpx
#  define SETUTENT setutxent
#  define PUTUTLINE pututxline
#  define ENDUTENT endutxent
#  include <utmpx.h>
#  ifdef _PATH_WTMPX
#   define WTMP_PATH _PATH_WTMPX
#  elif defined(CONF_WTMPX_PATH)
#   define WTMP_PATH CONF_WTMPX_PATH
#  else
#   error "Don't know where is wtmpx"
#  endif
# else
#  define UTMP utmp
#  define SETUTENT setutent
#  define PUTUTLINE pututline
#  define ENDUTENT endutent
#  include <utmp.h>
#  ifdef _PATH_WTMP
#   define WTMP_PATH _PATH_WTMP
#  elif defined(CONF_WTMP_PATH)
#   define WTMP_PATH CONF_WTMP_PATH
#  else
#   error "Don't know where is wtmp"
#  endif
# endif
# if defined(HAVE_UPDWTMPX) && defined(HAVE_UTMPX)
#  define UPDWTMP(path, utptr) updwtmpx(path, utptr)
# elif defined(HAVE_UPDWTMP) && !defined(HAVE_UTMPX)
#  define UPDWTMP(path, utptr) updwtmp(path, utptr)
# else
#  define NEED_UPDATE_WTMP_FUNC
#  define NEED_LOCK_FUNC
# endif
#endif /* UTMP_TYPE */

#if LASTLOG_TYPE == LASTLOG_TYPE_FILE
# ifndef NEED_LOCK_FUNC
#  define NEED_LOCK_FUNC
# endif
#endif

#define MEMBERLEN(type, memb) (sizeof (*(type *)0).memb)
#define CHECKFUN(name) int name(agent_state *pstate)

/*
 * ====================================
 * types and decls
 * ====================================
 */

typedef struct {
    connection *pconn;
    char *ttyname;
    int ptytype;
    int need_tty_release;
    struct stat master_st;
#if UTMP_TYPE != UTMP_TYPE_NONE
    int utmp_enabled;
    int need_utmp_clear;
    int need_wtmp_clear;
    char *line;
#endif
#if UTMP_TYPE == UTMP_TYPE_BSD
    int tty_slot_num;
#elif UTMP_TYPE == UTMP_TYPE_SYSV
    struct UTMP saved_utmp;
    struct UTMP saved_wtmp;
    char utmpid[MEMBERLEN(struct UTMP, ut_id)];
#endif
} agent_state;

typedef struct {
    int type;
    CHECKFUN((*check));
} ptytype_table_item;

#if UTMP_TYPE == UTMP_TYPE_BSD
static void init_utmp_bsd(agent_state *pstate);
static int login_utmp_bsd(agent_state *pstate, const char *host);
static int login_wtmp_bsd(agent_state *pstate, const char *host);
static int logout_utmp_bsd(agent_state *pstate);
static int logout_wtmp_bsd(agent_state *pstate);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
static void init_utmp_sysv(agent_state *pstate);
static int login_utmp_sysv(agent_state *pstate, const char *host, pid_t pid);
static int login_wtmp_sysv(agent_state *pstate, const char *host, pid_t pid);
static int logout_utmp_sysv(agent_state *pstate, int exit_st);
static int logout_wtmp_sysv(agent_state *pstate, int exit_st);
#endif /* UTMP_TYPE */

static void cleanup(agent_state *pstate);
#ifdef NEED_LOCK_FUNC
static int trylock(int fd);
#endif

static MSGPROC(msg_connect_proc);
static MSGPROC(msg_init_tty_proc);
static MSGPROC(msg_release_tty_proc);
static MSGPROC(msg_login_utmp_proc);
static MSGPROC(msg_logout_utmp_proc);
static MSGPROC(msg_login_wtmp_proc);
static MSGPROC(msg_logout_wtmp_proc);
static MSGPROC(msg_update_lastlog_proc);
static MSGPROC(msg_disconnect_proc);

#ifdef HAVE_PTY_SVR4
static CHECKFUN(check_pty_svr4);
#endif
#ifdef HAVE_PTY_BSD
static CHECKFUN(check_pty_bsd);
#endif
#ifdef HAVE_PTY_PTC
static CHECKFUN(check_pty_ptc);
#endif
#ifdef HAVE_PTY_CLONE
static CHECKFUN(check_pty_clone);
#endif
#ifdef HAVE_PTY_SGI4
static CHECKFUN(check_pty_sgi4);
#endif
#ifdef HAVE_PTY_NUMERIC
static CHECKFUN(check_pty_numeric);
#endif
#ifdef HAVE_PTY_CYGWIN
static CHECKFUN(check_pty_cygwin);
#endif
#ifdef HAVE_PTY_CRAY
static CHECKFUN(check_pty_cray);
#endif


/*
 * ====================================
 * variables
 * ====================================
 */
static const dispatch_table_item first_tab_items[] = {
    { MSG_CONNECT, &msg_connect_proc },
};

static const dispatch_table_item connected_tab_items[] = {
    { MSG_INIT_TTY, &msg_init_tty_proc },
    { MSG_RELEASE_TTY, &msg_release_tty_proc },
    { MSG_LOGIN_UTMP, &msg_login_utmp_proc },
    { MSG_LOGOUT_UTMP, &msg_logout_utmp_proc },
    { MSG_LOGIN_WTMP, &msg_login_wtmp_proc },
    { MSG_LOGOUT_WTMP, &msg_logout_wtmp_proc },
    { MSG_UPDATE_LASTLOG, &msg_update_lastlog_proc },
    { MSG_DISCONNECT, &msg_disconnect_proc },
};

static dispatch_table agent_tab = {
    ARRAYLEN(first_tab_items),
    first_tab_items,
    NULL,
    NULL
};

static const ptytype_table_item ptytype_tab[] = {
#ifdef HAVE_PTY_SVR4
    { PTYTYPE_SVR4, &check_pty_svr4 },
#endif
#ifdef HAVE_PTY_BSD
    { PTYTYPE_BSD, &check_pty_bsd },
#endif
#ifdef HAVE_PTY_PTC
    { PTYTYPE_PTC, &check_pty_ptc },
#endif
#ifdef HAVE_PTY_CLONE
    { PTYTYPE_CLONE, &check_pty_clone },
#endif
#ifdef HAVE_PTY_SGI4
    { PTYTYPE_SGI4, &check_pty_sgi4 },
#endif
#ifdef HAVE_PTY_NUMERIC
    { PTYTYPE_NUMERIC, &check_pty_numeric },
#endif
#ifdef HAVE_PTY_CYGWIN
    { PTYTYPE_CYGWIN, &check_pty_cygwin },
#endif
#ifdef HAVE_PTY_CRAY
    { PTYTYPE_CRAY, &check_pty_cray },
#endif
};

const char *loginname;
gid_t ttygid;
/* xterm style is "????", rxvt one is "?" */
const char user_unknown[] = "?";
int signal_catched = 0;

/*
 * ====================================
 * functions
 * ====================================
 */

/* TODO: syslog */
static void
emsg(const char *msg)
{
    fputs("sptagent: ", stderr);
    fputs(msg, stderr);
}

static void
alloc_fail(void)
{
    emsg("out of memory");
    exit(1);
}

static void
signal_exit(int val)
{
    signal_catched = val;
}

int
main(void)
{
    agent_state state;
    int r;
    struct stat conn_st;
    struct group *grp;
    struct passwd *pw;

    signal(SIGINT, &signal_exit);
    if (fstat(AGENT_CONN_FD, &conn_st)
	    || fstat(AGENT_MASTER_FD, &state.master_st)) {
	emsg("sptagent can be invoked only from libspt\n");
	return 1;
    }

    grp = getgrnam("tty");
    if (grp)
	ttygid = grp->gr_gid;
    else
	ttygid = (gid_t)-1;
    pw = getpwuid(getuid());
    loginname = (pw && pw->pw_name) ? STRDUP(pw->pw_name) : user_unknown;
    if (!loginname)
	alloc_fail();

#if UTMP_TYPE != UTMP_TYPE_NONE
    state.need_utmp_clear = 0;
    state.need_wtmp_clear = 0;
    state.utmp_enabled = 0;
#endif
    agent_tab.arg = &state;
    r = spt_p_create_conn(&state.pconn, &agent_tab, AGENT_CONN_FD);
    if (r) {
	assert(r == SPT_E_NOMEM);
	alloc_fail();
	/* NOTREACHED */
    }

    signal(SIGTERM, &signal_exit);
    signal(SIGINT, &signal_exit);
    signal(SIGHUP, &signal_exit);
    setpgid(0, 0);
    while (1) {
	r = spt_p_do_read(state.pconn);
	switch (r) {
	    msgunion_with_type msg;
	    case SPT_E_NONE:
	    case SPT_IE_AGAIN:
		if (signal_catched) {
		    cleanup(&state);
		    signal(SIGTERM, SIG_DFL);
		    signal(SIGINT, SIG_DFL);
		    signal(SIGHUP, SIG_DFL);
		    kill(getpid(), signal_catched);
		}
		continue;
	    case SPT_IE_PEERDEAD:
		emsg("peer dead\n");
		cleanup(&state);
		return 0;   /* exit with no error */
	    case SPT_E_NOMEM:
		cleanup(&state);
		alloc_fail();
		/* NOTREACHED */
	    case SPT_IE_VIOLATION:
		msg.msgtype = MSG_PROTO_VIOLATION;
		spt_p_send_msg(state.pconn, &msg);
		emsg("unexpected request from client\n");
		return 1;
	    default:
		assert(0);
	}
    }
    /* NOTREACHED */
}

#ifdef NEED_LOCK_FUNC

static RETSIGTYPE
alarm_handler(int dummy)
{
    /* do nothing */
    SIGNAL_RETURN;
}

/*
 * rxvt variants, mlterm, and glibc pututent() does fcntl() locking.
 * sshd, freebsd login(), GNU screen does no lock.
 * rxvt variants and mlterm simply try F_SETLK 10 times with no wait.
 * In contrast, glibc set alarm(1sec) to break F_SETLKW with EINTR.
 * I believe glibc way is the best, except its too long timeout.
 * Anyway, all efforts to keep utmp/wtmp consistent are turned in vain
 * by only one stupid application. Sigh.
 */
static int
trylock(int fd)
{
    struct flock lk;
    struct itimerval it;
    int r;

    BZERO(&lk, sizeof lk);
    BZERO(&it, sizeof it);
    lk.l_whence = SEEK_SET;
    lk.l_type = F_WRLCK;
    it.it_value.tv_usec = 300 * 1000;	/* need more? */

    signal(SIGALRM, &alarm_handler);
    setitimer(ITIMER_REAL, &it, NULL);
    r = fcntl(fd, F_SETLKW, &lk);

    it.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL, &it, NULL);
    signal(SIGALRM, SIG_DFL);
    return r;
}
#endif /* NEED_LOCK_FUNC */

static void
cleanup(agent_state *pstate)
{
    if (pstate->need_tty_release) {
#ifdef CHOWN_FOLLOWS_SYMLINK
	chown(pstate->ttyname, 0, 0);
	chmod(pstate->ttyname, 0666);
#else
	{
	    int fd;
	    if ((fd = open(pstate->ttyname, O_RDONLY | O_NOCTTY)) >= 0) {
		fchown(fd, 0, 0);
		fchmod(fd, 0666);
		close(fd);
	    }
	}
#endif
    }
#if UTMP_TYPE != UTMP_TYPE_NONE
    if (pstate->utmp_enabled) {
	if (pstate->need_utmp_clear) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	    logout_utmp_bsd(pstate);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	    logout_utmp_sysv(pstate, 0);
#endif
	}
	if (pstate->need_wtmp_clear) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	    logout_wtmp_bsd(pstate);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	    logout_wtmp_sysv(pstate, 0);
#endif
	}
    }
#endif	/* UTMP_TYPE != UTMP_TYPE_NONE */
}

/*
 * ====================================
 * message handlers
 * ====================================
 */

static MSGPROC(msg_connect_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    const msg_connect *preq = &pmu->mu_connect;
    unsigned int i;
    int result;

    if (preq->version != SPT_PROTO_VERSION) {
	spt_p_send_result(pconn, ERR_VERSION_MISMATCH);
	exit(1);
    }
    pstate->ttyname = STRDUP(preq->ttyname);
    if (!pstate->ttyname)
	alloc_fail();
    pstate->ptytype = preq->ptytype;
    for (i = 0; i < ARRAYLEN(ptytype_tab); ++i)
	if (ptytype_tab[i].type == preq->ptytype)
	    goto found;
    spt_p_send_result(pconn, ERR_UNKNOWN_PTYTYPE);
    exit(1);
    /* NOTREACHED */
found:
    pstate->need_tty_release = 0;
    result = (*ptytype_tab[i].check) (pstate);
    if (result == SPT_IE_INVAL_PTY) {
	spt_p_send_result(pconn, ERR_WRONG_INST);
	exit(1);
    } else if (result == SPT_E_NOMEM)
	alloc_fail();
    assert(result == SPT_E_NONE);
    agent_tab.n_items = ARRAYLEN(connected_tab_items);
    agent_tab.items = connected_tab_items;
    DEBUG(fprintf(stderr, "msg_connect ok\n"));
#if UTMP_TYPE == UTMP_TYPE_BSD
    init_utmp_bsd(pstate);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
    init_utmp_sysv(pstate);
#endif
    return spt_p_send_result(pconn, ERR_NONE);
}

static MSGPROC(msg_init_tty_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    int result = ERR_CHOWN_FAIL;

#ifdef CHOWN_FOLLOWS_SYMLINK
    if (!chown(pstate->ttyname, getuid(), ttygid))
	if (!chmod(pstate->ttyname, (ttygid == -1) ? 0622 : 0620))
	    result = ERR_NONE;
#else
    {
	int fd;
	if ((fd = open(pstate->ttyname, O_RDONLY | O_NOCTTY)) >= 0) {
	    if (!fchown(fd, getuid(), ttygid))
		if (!fchmod(fd, (ttygid == -1) ? 0622 : 0620))
		    result = ERR_NONE;
	    close(fd);
	}
    }
#endif
    pstate->need_tty_release = 1;
    return spt_p_send_result(pconn, result);
}

static MSGPROC(msg_release_tty_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    int result = ERR_CHOWN_FAIL;

#ifdef CHOWN_FOLLOWS_SYMLINK
    if (!chown(pstate->ttyname, 0, 0))
	if (!chmod(pstate->ttyname, 0666))
	    result = ERR_NONE;
#else
    {
	int fd;
	if ((fd = open(pstate->ttyname, O_RDONLY | O_NOCTTY)) >= 0) {
	    if (!fchown(fd, 0, 0))
		if (!fchmod(fd, 0666))
		    result = ERR_NONE;
	    close(fd);
	}
    }
#endif
    pstate->need_tty_release = 0;
    return spt_p_send_result(pconn, result);
}

static MSGPROC(msg_login_utmp_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    const msg_utmp *preq = &pmu->mu_utmp;
    int r = 1;

    DEBUG(fprintf(stderr, "msg_login_utmp_proc(%s, %d)\n",
		preq->host, (int)preq->pid));
#if UTMP_TYPE != UTMP_TYPE_NONE
    if (pstate->utmp_enabled) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	r = login_utmp_bsd(pstate, preq->host);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	r = login_utmp_sysv(pstate, preq->host, (pid_t)preq->pid);
#endif
    }
#endif	/* UTMP_TYPE != UTMP_TYPE_NONE */
    return spt_p_send_result(pconn, r ? ERR_UTMP_FAIL : ERR_NONE);
}

static MSGPROC(msg_logout_utmp_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    int r = 1;

    DEBUG(fprintf(stderr, "msg_logout_utmp_proc"));
#if UTMP_TYPE != UTMP_TYPE_NONE
    if (pstate->need_utmp_clear && pstate->utmp_enabled) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	r = logout_utmp_bsd(pstate);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	r = logout_utmp_sysv(pstate, pmu->mu_int);
#endif
	pstate->need_utmp_clear = 0;    /* even if failed */
    }
#endif	/* UTMP_TYPE != UTMP_TYPE_NONE */
    return spt_p_send_result(pconn, r ? ERR_UTMP_FAIL : ERR_NONE);
}

static MSGPROC(msg_login_wtmp_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    const msg_utmp *preq = &pmu->mu_utmp;
    int r = 1;

    DEBUG(fprintf(stderr, "msg_login_wtmp_proc(%s, %d)\n",
		preq->host, (int)preq->pid));
#if UTMP_TYPE != UTMP_TYPE_NONE
    if (pstate->utmp_enabled) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	r = login_wtmp_bsd(pstate, preq->host);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	r = login_wtmp_sysv(pstate, preq->host, (pid_t)preq->pid);
#endif
    }
#endif	/* UTMP_TYPE != UTMP_TYPE_NONE */
    return spt_p_send_result(pconn, r ? ERR_UTMP_FAIL : ERR_NONE);
}

static MSGPROC(msg_logout_wtmp_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    int r = 1;

    DEBUG(fprintf(stderr, "msg_logout_wtmp_proc"));
#if UTMP_TYPE != UTMP_TYPE_NONE
    if (pstate->need_wtmp_clear && pstate->utmp_enabled) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	r = logout_wtmp_bsd(pstate);
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	r = logout_wtmp_sysv(pstate, pmu->mu_int);
#endif
	pstate->need_wtmp_clear = 0;    /* even if failed */
    }
#endif	/* UTMP_TYPE != UTMP_TYPE_NONE */
    return spt_p_send_result(pconn, r ? ERR_UTMP_FAIL : ERR_NONE);
}

static MSGPROC(msg_disconnect_proc) {
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    int r = 0;

    if (pstate->need_tty_release) {
#ifdef CHOWN_FOLLOWS_SYMLINK
	if (chown(pstate->ttyname, 0, 0))
	    r = 1;
	if (chmod(pstate->ttyname, 0666))
	    r = 1;
#else
	{
	    int fd;
	    if ((fd = open(pstate->ttyname, O_RDONLY | O_NOCTTY)) >= 0) {
		if (fchown(fd, 0, 0))
		    r = 1;
		if (fchmod(fd, 0666))
		    r = 1;
		close(fd);
	    } else
		r = 1;
	}
#endif
    }
#if UTMP_TYPE != UTMP_TYPE_NONE
    if (pstate->utmp_enabled) {
	if (pstate->need_utmp_clear) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	    if (logout_utmp_bsd(pstate))
		r = 1;
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	    if (logout_utmp_sysv(pstate, 0))
		r = 1;
#endif
	}
	if (pstate->need_wtmp_clear) {
#if UTMP_TYPE == UTMP_TYPE_BSD
	    if (logout_wtmp_bsd(pstate))
		r = 1;
#elif UTMP_TYPE == UTMP_TYPE_SYSV
	    if (logout_wtmp_sysv(pstate, 0))
		r = 1;
#endif
	}
    }
#endif	/* UTMP_TYPE != UTMP_TYPE_NONE */
    spt_p_send_result(pconn, r ? ERR_CLEANUP_FAIL : ERR_NONE);
    spt_p_destroy_conn(pconn);
    FREE(pstate->ttyname);
    if (loginname != user_unknown)
	FREE((char *)loginname);
    DEBUG(MEMORY_REPORT());
    exit(0);
    /* NOTREACHED */
    return 0;
}

#define LASTLOGSIZE sizeof(struct lastlog)
static MSGPROC(msg_update_lastlog_proc)
{
    agent_state *pstate = (agent_state *)arg;
    connection *pconn = pstate->pconn;
    const msg_utmp *preq = &pmu->mu_utmp;
    int r = 1;

#if LASTLOG_TYPE != LASTLOG_TYPE_NONE
    if (pstate->utmp_enabled) {
	struct lastlog ll;
	int fd;

	strncpy(ll.ll_line, pstate->line, sizeof ll.ll_line);
	strncpy(ll.ll_host, preq->host, sizeof ll.ll_host);
	ll.ll_time = time(NULL);
#if LASTLOG_TYPE == LASTLOG_TYPE_FILE
	if ((fd = open(CONF_LASTLOG_PATH, O_WRONLY)) >= 0) {
	    if (!trylock(fd)) {
		lseek(fd, getuid() * LASTLOGSIZE, SEEK_SET);
		if (write(fd, &ll, LASTLOGSIZE) == LASTLOGSIZE)
		    r = 0;
	    }
	    close(fd);
	}
#elif LASTLOG_TYPE == LASTLOG_TYPE_DIR
	{
	    char filename[PATH_MAX];
	    const char *user;
	    /* avoid "?" in filename. FIXME: stupid implementation */
	    user = (loginname == user_unknown) ? "unknown" : loginname;
	    if (sizeof(CONF_LASTLOG_PATH "/") - 1 + strlen(user) + 1
		    <= sizeof filename) {
#ifdef HAVE_SNPRINTF /* sprintf is safe, but just in case */
		snprintf(filename, PATH_MAX, CONF_LASTLOG_PATH "/%s", user);
#else
		sprintf(filename, CONF_LASTLOG_PATH "/%s", user);
#endif
		if ((fd = open(filename, O_WRONLY | O_CREAT, 0644)) >= 0) {
		    if (!trylock(fd)) {
			if (write(fd, &ll, LASTLOGSIZE) == LASTLOGSIZE)
			    r = 0;
		    }
		    close(fd);
		}
	    }
	}
#endif
    }
#endif /* LASTLOG_TYPE != LASTLOG_TYPE_NONE */
    return spt_p_send_result(pconn, r ? ERR_UTMP_FAIL : ERR_NONE);
}


/*
 * ====================================
 * pty checkers
 * ====================================
 */

/* tested */
#ifdef HAVE_PTY_SVR4
static CHECKFUN(check_pty_svr4) {
    pstate->need_tty_release = 1;
    return (strcmp(pstate->ttyname, ptsname(AGENT_MASTER_FD)) == 0)
	? SPT_E_NONE : SPT_IE_INVAL_PTY;
}
#endif /* HAVE_PTY_SVR4 */

/* tested */
#ifdef HAVE_PTY_BSD
static CHECKFUN(check_pty_bsd) {
    struct stat pty_st;
    char mastername[sizeof "/dev/pty??"];

    if (strncmp(pstate->ttyname, "/dev/tty", sizeof "/dev/tty" - 1))
	return SPT_IE_INVAL_PTY;
    if (strlen(pstate->ttyname) != sizeof mastername - 1)
	return SPT_IE_INVAL_PTY;
    strcpy(mastername, pstate->ttyname);
    mastername[5] = 'p';
    if (stat(mastername, &pty_st)
	    || pty_st.st_dev != pstate->master_st.st_dev
	    || pty_st.st_ino != pstate->master_st.st_ino)
	return SPT_IE_INVAL_PTY;
    return SPT_E_NONE;
}
#endif /* HAVE_PTY_BSD */

/* UNTESTED */
#ifdef HAVE_PTY_PTC
/* AIX, SGI3 */
static CHECKFUN(check_pty_ptc) {
    return (strcmp(pstate->ttyname, ttyname(AGENT_MASTER_FD)) == 0)
	? SPT_E_NONE : SPT_IE_INVAL_PTY;
}
#endif /* HAVE_PTY_PTC */

/* UNTESTED */
#ifdef HAVE_PTY_CLONE
/* HPUX */
static CHECKFUN(check_pty_clone) {
    return (strcmp(pstate->ttyname, ptsname(AGENT_MASTER_FD)) == 0)
	? SPT_E_NONE : SPT_IE_INVAL_PTY;
}
#endif /* HAVE_PTY_CLONE */

/* UNTESTED */
#ifdef HAVE_PTY_SGI4
/* _getpty() */
static CHECKFUN(check_pty_sgi4)
{
    int masterfd;
    struct stat slave_st;

    /* 
     * I don't know how to check whether given slave is really associated
     * to the master. Fortunately, slave pty created by _openpty() is owned
     * by the real user.  So only its owner and name is checked. This 
     * does not cause any security problem.
     * "/dev/" prefix is important!
     */
    pstate->need_tty_release = 1;
    if ((strncmp(pstate->ttyname, "/dev/", sizeof "/dev/" - 1) == 0)
	    && (strstr(pstate->ttyname, "/./") == NULL)
	    && (strstr(pstate->ttyname, "/../") == NULL)
	    && !stat(pstate->ttyname, &slave_st)
	    && slave_st.st_uid == getuid())
	return SPT_E_NONE;
    return SPT_E_INVAL_PTY;
}
#endif /* HAVE_PTY_SGI4 */

/* UNTESTED */
#ifdef HAVE_PTY_NUMERIC
/* SCO */
static CHECKFUN(check_pty_numeric)
{
    struct stat pty_st;
    char mastername[sizeof "/dev/ptyp???"];

    if (strncmp(pstate->ttyname, "/dev/ttyp", sizeof "/dev/ttyp" - 1) != 0)
	return SPT_IE_INVAL_PTY;
    if (strlen(pstate->ttyname > sizeof mastername - 1))
	return SPT_IE_INVAL_PTY;
    strcpy(mastername, pstate->ttyname);
    mastername[5] = 'p';
    if (stat(mastername, &pty_st)
	    || pty_st.st_dev != pstate->master_st.st_dev
	    || pty_st.st_ino != pstate->master_st.st_ino)
	return SPT_IE_INVAL_PTY;
    return SPT_E_NONE;
}
#endif /* HAVE_PTY_NUMERIC */

/* tested */
#ifdef HAVE_PTY_CYGWIN
static CHECKFUN(check_pty_cygwin) {
    return (strcmp(pstate->ttyname, ptsname(AGENT_MASTER_FD)) == 0)
	? SPT_E_NONE : SPT_IE_INVAL_PTY;
}
#endif /* HAVE_PTY_SVR4 */

/* UNTESTED */
#ifdef HAVE_PTY_CRAY
/* UNICOS */
static CHECKFUN(check_pty_cray)
{
    struct stat pty_st;
    char mastername[sizeof "/dev/pty/???"];

    if (strncmp(pstate->ttyname, "/dev/ttyp", sizeof "/dev/ttyp" - 1) != 0)
	return SPT_IE_INVAL_PTY;
    if (strlen(pstate->ttyname > sizeof mastername - 1))
	return SPT_IE_INVAL_PTY;
    strcpy(mastername, pstate->ttyname);
    mastername[5] = 'p';
    mastername[8] = '/';
    if (stat(mastername, &pty_st)
	    || pty_st.st_dev != pstate->master_st.st_dev
	    || pty_st.st_ino != pstate->master_st.st_ino)
	return SPT_IE_INVAL_PTY;
    return SPT_E_NONE;
}
#endif /* HAVE_PTY_NUMERIC */


/*
 * ====================================
 * utmp implementations
 * ====================================
 */

#if UTMP_TYPE == UTMP_TYPE_BSD
#define UTMPSIZE sizeof(struct utmp)
static int
find_tty_slot(const char *line)
{
    FILE *fp = fopen("/etc/ttys", "r");
    int slotnum = 1;
    char *buf;
    size_t bufsize = 20;
    size_t n_read = 0;

    if (!fp)
	return -1;
    if ((buf = MALLOC(bufsize)) == NULL)
	goto err1;
    while (1) {
	char *p = fgets(buf + n_read, bufsize - n_read, fp);
	if (!p)
	    break;
	n_read += strlen(p);
	if (buf[n_read - 1] != '\n') {
	    /*
	     * if /etc/ttys does not end with '\n', the last line is
	     * ignored. this makes everything easy.
	     */
	    if (n_read < bufsize - 1)
		break;
	    bufsize *= 2;
	    if ((buf = REALLOCF(buf, bufsize)) == NULL)
		goto err1;
	    continue;
	}
	n_read = 0;
	p = buf + STRCSPN(buf, " \t\n#");
	*p = '\0';
	if (*buf == '\0')
	    continue;
	if (strcmp(buf, line) == 0) {
	    FREE(buf);
	    fclose(fp);
	    return slotnum;
	}
	++slotnum;
    }
    FREE(buf);
err1:
    fclose(fp);
    return -1;
}

static void
init_utmp_bsd(agent_state *pstate)
{
    if (strncmp(pstate->ttyname, "/dev/tty", sizeof "/dev/tty" - 1) != 0)
	return;	    /* just in case */
    pstate->line = pstate->ttyname + 5;
    if ((pstate->tty_slot_num = find_tty_slot(pstate->line)) < 0)
	return;
    DEBUG(fprintf(stderr, "init_utmp_bsd: slot=%d\n", pstate->tty_slot_num));
    pstate->utmp_enabled = 1;
}

static int
login_utmp_bsd(agent_state *pstate, const char *host)
{
    struct utmp ut;
    int fd;

    DEBUG(fprintf(stderr, "login_utmp_bsd: host=%s\n", host));
    /* these entries does not need to be null-terminated */
    strncpy(ut.ut_line, pstate->line, UT_LINESIZE);
    strncpy(ut.ut_name, loginname, UT_NAMESIZE);
    strncpy(ut.ut_host, host, UT_HOSTSIZE);
    ut.ut_time = time(NULL);
    if ((fd = open(_PATH_UTMP, O_WRONLY)) < 0)
	return SPT_E_UTMP_FAIL;
    if (!trylock(fd)) {
	lseek(fd, pstate->tty_slot_num * UTMPSIZE, SEEK_SET);
	if (write(fd, &ut, UTMPSIZE) == UTMPSIZE) {
	    close(fd);
	    pstate->need_utmp_clear = 1;
	    DEBUG(fprintf(stderr, "login_utmp_bsd: ok\n"));
	    return SPT_E_NONE;
	}
    }
    close(fd);
    return SPT_E_UTMP_FAIL;
}

static int
logout_utmp_bsd(agent_state *pstate)
{
    struct utmp ut;
    int fd;

    BZERO(&ut, UTMPSIZE);
    if ((fd = open(_PATH_UTMP, O_WRONLY)) < 0)
	return SPT_E_UTMP_FAIL;
    if (!trylock(fd)) {
	lseek(fd, pstate->tty_slot_num * UTMPSIZE, SEEK_SET);
	if (write(fd, &ut, UTMPSIZE) == UTMPSIZE) {
	    close(fd);
	    return SPT_E_NONE;
	}
    }
    close(fd);
    return SPT_E_UTMP_FAIL;
}

static int
login_wtmp_bsd(agent_state *pstate, const char *host)
{
    struct utmp ut;
    int fd;
    struct stat st;

    strncpy(ut.ut_line, pstate->line, UT_LINESIZE);
    strncpy(ut.ut_name, loginname, UT_NAMESIZE);
    strncpy(ut.ut_host, host, UT_HOSTSIZE);
    ut.ut_time = time(NULL);
    if ((fd = open(_PATH_WTMP, O_WRONLY)) < 0)
	return SPT_E_UTMP_FAIL;
    if (!trylock(fd)
	    && !fstat(fd, &st)) {
	/* remove incomplete entry */
	off_t pos = st.st_size - st.st_size % UTMPSIZE;
	lseek(fd, pos, SEEK_SET);
	if (write(fd, &ut, UTMPSIZE) == UTMPSIZE) {
	    close(fd);	/* unlock on close */
	    pstate->need_wtmp_clear = 1;
	    return SPT_E_NONE;
	}
	ftruncate(fd, pos);
    }
    close(fd);
    return SPT_E_UTMP_FAIL;
}

static int
logout_wtmp_bsd(agent_state *pstate)
{
    int fd;
    struct stat st;
    struct utmp ut;

    DEBUG(fprintf(stderr, "logout_wtmp_bsd\n"));
    BZERO(&ut, UTMPSIZE);
    strncpy(ut.ut_line, pstate->line, sizeof ut.ut_line);
    ut.ut_time = time(NULL);
    if ((fd = open(_PATH_WTMP, O_WRONLY)) < 0)
	return SPT_E_UTMP_FAIL;
    if (!trylock(fd)
	    && !fstat(fd, &st)) {
	/* remove incomplete entry */
	off_t pos = st.st_size - st.st_size % UTMPSIZE;
	lseek(fd, pos, SEEK_SET);
	if (write(fd, &ut, UTMPSIZE) == UTMPSIZE) {
	    DEBUG(fprintf(stderr, "logout_wtmp_bsd: ok\n"));
	    close(fd);
	    return SPT_E_NONE;
	}
	ftruncate(fd, pos);
    }
    close(fd);
    return SPT_E_UTMP_FAIL;
}

#elif UTMP_TYPE == UTMP_TYPE_SYSV

#if defined(HAVE_UTMPX) || defined(linux)
# define HAVE_UTMP_UT_SESSION
# define HAVE_UTMP_UT_TV
# define UTMP_APIS_HAVE_RETURN_CODE
#endif
#define HAVE_UTMP_UT_HOST

#define UTMPSIZE sizeof(struct UTMP)

#ifdef NEED_UPDATE_WTMP_FUNC
static struct UTMP *
update_wtmp(const char *path, struct UTMP *utptr)
{
    struct stat st;
    int fd;

    if ((fd = open(path, O_WRONLY)) < 0)
	return NULL;
    if (!trylock(fd)
	    && !fstat(fd, &st)) {
	/* remove incomplete entry */
	off_t pos = st.st_size - st.st_size % UTMPSIZE;
	lseek(fd, pos, SEEK_SET);
	if (write(fd, utptr, UTMPSIZE) == UTMPSIZE) {
	    close(fd);	/* unlock on close */
	    return utptr;
	}
	ftruncate(fd, pos);
    }
    close(fd);
    return utptr;
}
#endif /* NEED_UPDATE_WTMP_FUNC */

void
set_timefield(struct UTMP *utptr)
{
#ifdef HAVE_UTMP_UT_TV
#ifdef HAVE_GETTIMEOFDAY
    gettimeofday(&utptr->ut_tv, NULL);
#else
    utptr->ut_tv.tv_sec = time(NULL);
    utptr->ut_tv.ut_usec = 0;
#endif
#else /* !HAVE_UT_TV */
    utptr->ut_time = time(NULL);
#endif /* !HAVE_UT_TV */
}

static void
init_utmp_sysv(agent_state *pstate)
{
    const char *line;
    const char *idpos;

    if (strncmp(pstate->ttyname, "/dev/", 5) != 0)
	return;	    /* just in case */
    line = pstate->line = pstate->ttyname + 5;

    /*
     * Now determine ut_id field. Unfortunately, ut_id naming varies from
     * implementations to implementations. Old /dev/tty?? style ttys
     * are named as "p0", "p1" ... in most applications, but there is
     * no common naming to /dev/pts/[num] style ttys.
     * Well known applications do:
     *
     * xterm: p[num]
     * rxvt family: vt[hex] (sprintf(ut_id, "vt%02x", ptynumber & 255))
     * kterm (not __sgi): last PTYCHARLEN characters
     * kterm (__sgi): strip first three characters
     * screen(sgi): strip first 3 characters (same as above)
     * screen(linux): /[num] (same as sgi implementation. broken?)
     * screen(_IBMR2): first sizeof(ut_id) characters of ut_line
     * screen(others): last two characters
     * mlterm: [num] (after "/")
     * openssh: ts/?, s/?? (last sizeof(ut_id) characters)
     *
     * ut_id is intended to distinguish ttys as alternative to /etc/ttys,
     * so it is the uniqueness that is most important. In this point of
     * view, I think openssh naming is the best way.
     */
    idpos = (strncmp(line, "tty", 3) == 0) ? line + 3 : line;
    if (strlen(idpos) > sizeof pstate->utmpid)
	idpos += strlen(idpos) - sizeof pstate->utmpid;
    strncpy(pstate->utmpid, idpos, sizeof pstate->utmpid);

    pstate->utmp_enabled = 1;
}

static void
set_utmp_sysv(const agent_state *pstate, struct UTMP *utptr,
	const char *host, pid_t pid)
{
    /* perform zero clear first. this might improve portability a bit. */
    BZERO(utptr, UTMPSIZE);

    strncpy(utptr->ut_line, pstate->line, sizeof utptr->ut_line);
    strncpy(utptr->ut_id, pstate->utmpid, sizeof utptr->ut_id);

    /*
     * rxvt family and mlterm set this to parent's pid. The others
     * set to child's. I believe the latter way is correct.
     */
    utptr->ut_pid = pid;

#ifdef HAVE_UTMP_UT_SESSION
    /*
     * I don't know what to set ut_session to. Most of SysV manuals
     * only say "session id, used for windowing".
     * rxvt family set it to parent's session id.
     * xterm, kterm and probably early ssh set to child's.
     * The others do nothing. 
     * Currently I follow xterm way even though getsid() caller is
     * restricted to the same session's processes on some platforms.
     */
#ifdef HAVE_GETSID
    utptr->ut_session = getsid(pid);
#endif
#endif /* HAVE_UTMP_UT_SESSION */

    utptr->ut_type = USER_PROCESS;
    strncpy(utptr->ut_user, loginname, sizeof utptr->ut_user);
    set_timefield(utptr);

#ifdef HAVE_UTMP_UT_HOST
    strncpy(utptr->ut_host, host, sizeof utptr->ut_host);
#ifndef linux
    {
	const char *p;
	if ((p = STRRCHR(host, ':')) != NULL
		&& p < host + sizeof utptr->ut_host)
	    utptr->ut_host[p - host] = '\0';
    }
#endif /* !linux */
#endif /* HAVE_UTMP_UT_HOST */

#ifdef HAVE_UTMP_UT_ADDR
    /* 
     * ut_addr is left unset in most terminal emulators. In addition,
     * ut_addr field is too short to IPv6 address. We don't want to
     * bother to set this field.
     */
    /* *(unsigned long *)&utptr->ut_addr = 0; */
#endif /* HAVE_UTMP_UT_ADDR */

}

static int
login_utmp_sysv(agent_state *pstate, const char *host, pid_t pid)
{
    struct UTMP ut;
#ifdef UTMP_APIS_HAVE_RETURN_CODE
    struct UTMP *res;
#endif

    set_utmp_sysv(pstate, &ut, host, pid);
    MEMCPY(&pstate->saved_utmp, &ut, UTMPSIZE);
#ifdef UTMP_APIS_HAVE_RETURN_CODE
    SETUTENT();
    res = PUTUTLINE(&ut);
    if (res)
	pstate->need_utmp_clear = 1;
    ENDUTENT();
    return res ? SPT_E_NONE : SPT_E_UTMP_FAIL;
#else
    SETUTENT();
    PUTUTLINE(&ut);
    pstate->need_utmp_clear = 1;
    ENDUTENT();
    return SPT_E_NONE;
#endif
}

#if defined(HAVE_STRUCT_UTMP_UT_EXIT_E_EXIT)
# define UT_E_EXIT ut_exit.e_exit
# define UT_E_TERM ut_exit.e_termination
#elif defined(HAVE_STRUCT_UTMP_UT_EXIT_UT_EXIT)
# define UT_E_EXIT ut_exit.ut_exit
#elif defined(HAVE_STRUCT_UTMP_UT_EXIT_UT_E_EXIT)
# define UT_E_EXIT ut_exit.ut_e_exit
#elif defined(HAVE_STRUCT_UTMP_UT_EXIT___E_EXIT)
# define UT_E_EXIT ut_exit.__e_exit
#endif
#if defined(UT_E_EXIT) || defined(UT_E_TERM)
# include <sys/wait.h>
#endif

static int
logout_utmp_sysv(agent_state *pstate, int exit_st)
{
    struct UTMP ut;
#ifdef UTMP_APIS_HAVE_RETURN_CODE
    struct UTMP *res;
#endif

    MEMCPY(&ut, &pstate->saved_utmp, UTMPSIZE);
    /* 
     * Linux manpage tell us to clear ut_user  and openssh and rxvt does clear
     * it but Solaris 8 returns error if we do (though logout logging itself
     * succeeds). This ifdef is s/||/&&/ of screen.
     */
#if !defined(sun) && !defined(SVR4)
    BZERO(ut.ut_user, sizeof ut.ut_user);
#endif
    BZERO(ut.ut_host, sizeof ut.ut_host);
    /*
     * Linux manpage says ut_line should be clear, but some deliberately
     * set it on Linux. In addition, solaris's pututxline() fails if this
     * field is cleard. I suppose linux's description is simply wrong.
     */
    /* BZERO(ut.ut_line, sizeof ut.ut_line); */
    /* Linux manpage says ut_tv should also be zero cleard. Is it wrong? */
    set_timefield(&ut);
    ut.ut_type = DEAD_PROCESS;
#ifdef UT_E_EXIT
    ut.UT_E_EXIT = WEXITSTATUS(exit_st);
#endif
#ifdef UT_E_TERM
    ut.UT_E_TERM = WTERMSIG(exit_st);
#endif

#ifdef UTMP_APIS_HAVE_RETURN_CODE
    SETUTENT();
    res = PUTUTLINE(&ut);
    ENDUTENT();
    return res ? SPT_E_NONE : SPT_E_UTMP_FAIL;
#else
    SETUTENT();
    PUTUTLINE(&ut);
    ENDUTENT();
    return SPT_E_NONE;
#endif
}

static int
login_wtmp_sysv(agent_state *pstate, const char *host, pid_t pid)
{
    struct UTMP ut;

    set_utmp_sysv(pstate, &ut, host, pid);
    MEMCPY(&pstate->saved_wtmp, &ut, UTMPSIZE);
#ifdef UPDWTMP
    UPDWTMP(WTMP_PATH, &ut);
#else
    if (update_wtmp(WTMP_PATH, &ut) == NULL)
	return SPT_E_UTMP_FAIL;
#endif
    pstate->need_wtmp_clear = 1;
    return SPT_E_NONE;
}

static int
logout_wtmp_sysv(agent_state *pstate, int exit_st)
{
    struct UTMP ut;

    MEMCPY(&ut, &pstate->saved_wtmp, UTMPSIZE);
    BZERO(ut.ut_user, sizeof ut.ut_user);
    set_timefield(&ut);
#ifdef UT_E_EXIT
    ut.UT_E_EXIT = WEXITSTATUS(exit_st);
#endif
#ifdef UT_E_TERM
    ut.UT_E_TERM = WTERMSIG(exit_st);
#endif

#ifdef UPDWTMP
    UPDWTMP(WTMP_PATH, &ut);
#else
    if (update_wtmp(WTMP_PATH, &ut) == NULL)
	return SPT_E_UTMP_FAIL;
#endif
    return SPT_E_NONE;
}
#endif /* UTMP_TYPE == UTMP_TYPE_SYSV */
