/*
   Copyright (c) 1995-1998 by Cisco systems, Inc.

   Permission to use, copy, modify, and distribute this software for
   any purpose and without fee is hereby granted, provided that this
   copyright and permission notice appear on all copies of the
   software and supporting documentation, the name of Cisco Systems,
   Inc. not be used in advertising or publicity pertaining to
   distribution of the program without specific prior permission, and
   notice be given in supporting documentation that modification,
   copying and distribution is by permission of Cisco Systems, Inc.

   Cisco Systems, Inc. makes no representations about the suitability
   of this software for any purpose.  THIS SOFTWARE IS PROVIDED ``AS
   IS'' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
   WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
   FITNESS FOR A PARTICULAR PURPOSE.
*/

#include "tac_plus.h"

#ifdef MAXSESS
char *wholog = WHOLOG_DEFAULT;
/*
 * initialize wholog file for tracking of user logins/logouts from
 * accounting records.
 */
void
maxsess_loginit()
{
    int fd;

    fd = open(wholog, O_CREAT | O_RDWR, 0600);
    if (fd < 0) {
	report(LOG_ERR, "Can't create: %s", wholog);
    } else {
	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "Initialize %s", wholog);
	}
	close(fd);
    }
}

/*
 * Given a port description, return it in a canonical format.
 *
 * This piece of goo is to cover the fact that an async line in EXEC
 * mode is known as "ttyXX", but the same line doing PPP or SLIP is
 * known as "AsyncXX".
 */
static char *
portname(oldport)
char *oldport;
{
    char *p = oldport;

    if (!strncmp(p, "Async", 5) || !strncmp(p, "tty", 3)) {
	while (!isdigit(*p) && *p) {
	    ++p;
	}
    }
    if (!*p) {
	if (debug & DEBUG_ACCT_FLAG)
	    report(LOG_DEBUG, "Maxsess -- Malformed portname: %s", oldport);
	return (oldport);
    }
    return (p);
}

/*
 * Seek to offset and write a buffer into the file pointed to by fp
 */
static void
write_record(name, fp, buf, size, offset)
FILE *fp;
long offset;
int size;
void *buf;
char *name;
{
    if (fseek(fp, offset, SEEK_SET) < 0) {
	report(LOG_ERR, "%s fd=%d Cannot seek to %d %s",
	       name, fileno(fp), offset, sys_errlist[errno]);
    }
    if (fwrite(buf, size, 1, fp) != 1) {
	report(LOG_ERR, "%s fd=%d Cannot write %d bytes",
	       name, fileno(fp), size);
    }
}

static void
process_stop_record(idp)
struct identity *idp;
{
    int recnum;
    struct peruser pu;
    FILE *fp;
    char *nasport = portname(idp->NAS_port);

    /* If we can't access the file, skip all checks. */
    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	report(LOG_ERR, "Can't open %s for updating", wholog);
	return;
    }
    tac_lockfd(wholog, fileno(fp));

    for (recnum = 0; 1; recnum++) {

	fseek(fp, recnum * sizeof(struct peruser), SEEK_SET);

	if (fread(&pu, sizeof(pu), 1, fp) <= 0) {
	    break;
	}

	/* A match for this record? */
	if (!(STREQ(pu.NAS_name, idp->NAS_name) &&
	      STREQ(pu.NAS_port, nasport))) {
	    continue;
	}

	/* A match. Zero out this record */
	bzero(&pu, sizeof(pu));

	write_record(wholog, fp, &pu, sizeof(pu), 
		     recnum * sizeof(struct peruser));

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "STOP record -- clear %s entry %d for %s/%s",
		   wholog, recnum, idp->username, nasport);
	}
    }
    fclose(fp);
}

static void
process_start_record(idp)
struct identity *idp;
{
    int recnum;
    int foundrec = -1;
    int freerec = -1;
    char *nasport = portname(idp->NAS_port);
    struct peruser pu;
    FILE *fp;

    /* If we can't access the file, skip all checks. */
    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	report(LOG_ERR, "Can't open %s for updating", wholog);
	return;
    }
    tac_lockfd(wholog, fileno(fp));

    for (recnum = 0; (fread(&pu, sizeof(pu), 1, fp) > 0); recnum++) {
	/* Match for this NAS/Port record? */
	if (STREQ(pu.NAS_name, idp->NAS_name) && STREQ(pu.NAS_port, nasport)) {
	    foundrec = recnum;
	    break;
	}
	/* Found a free slot on the way */
	if (pu.username[0] == '\0') {
	    freerec = recnum;
	}
    }

    /* This is a START record, so write a new record or update the existing
     * one.  Note that we bzero(), so the strncpy()'s will truncate long
     * names and always leave a null-terminated string. 
     */

    bzero(&pu, sizeof(pu));
    strncpy(pu.username, idp->username, sizeof(pu.username) - 1);
    strncpy(pu.NAS_name, idp->NAS_name, sizeof(pu.NAS_name) - 1);
    strncpy(pu.NAS_port, nasport, sizeof(pu.NAS_port) - 1);
    strncpy(pu.NAC_address, idp->NAC_address, sizeof(pu.NAC_address) - 1);

    /* Already in DB? */
    if (foundrec >= 0) {

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG,
	      "START record -- overwrite existing %s entry %d for %s %s/%s",
		   wholog, foundrec, pu.NAS_name, pu.username, pu.NAS_port);
	}
	write_record(wholog, fp, &pu, sizeof(pu),
		     foundrec * sizeof(struct peruser));
	fclose(fp);
	return;
    }

    /* Not found in DB, but we have a free slot */
    if (freerec >= 0) {

	write_record(wholog, fp, &pu, sizeof(pu),
		     freerec * sizeof(struct peruser));

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "START record -- %s entry %d for %s %s/%s added",
		   wholog, freerec, pu.NAS_name, pu.username, pu.NAS_port);
	}
	fclose(fp);
	return;
    }

    /* No free slot. Add record at the end */
    write_record(wholog, fp, &pu, sizeof(pu),
		 recnum * sizeof(struct peruser));

    if (debug & DEBUG_MAXSESS_FLAG) {
	report(LOG_DEBUG, "START record -- %s entry %d for %s %s/%s added",
	       wholog, recnum, pu.NAS_name, pu.username, pu.NAS_port);
    }
    fclose(fp);
}


/*
 * Given a start or a stop accounting record, update the file of
 * records which tracks who's logged on and where.
 */
loguser(rec)
struct acct_rec *rec;
{
    struct identity *idp;
    int i;

    /* We're only interested in start/stop records */
    if ((rec->acct_type != ACCT_TYPE_START) &&
	(rec->acct_type != ACCT_TYPE_STOP)) {
	return;
    }
    /* ignore command accounting records */
    for (i = 0; i < rec->num_args; i++) {
	char *avpair = rec->args[i];
	if ((strncmp(avpair, "cmd=", 4) == 0) && strlen(avpair) > 4) {
	    return;
	}
    }

    /* Extract and store just the port number, since the port names are
     * different depending on whether this is an async interface or an exec
     * line. */
    idp = rec->identity;

    switch (rec->acct_type) {
    case ACCT_TYPE_START:
	process_start_record(idp);
	return;

    case ACCT_TYPE_STOP:
	process_stop_record(idp);
	return;
    }
}

/* Read up to n bytes from descriptor fd into array ptr with timeout t
 * seconds.
 *
 * Return -1 on error, eof or timeout. Otherwise return number of
 * bytes read. */

int
timed_read(fd, ptr, nbytes, timeout)
int fd;
u_char *ptr;
int nbytes;
int timeout;
{
    int nread;
    fd_set readfds, exceptfds;
    struct timeval tout;

    tout.tv_sec = timeout;
    tout.tv_usec = 0;

    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);

    FD_ZERO(&exceptfds);
    FD_SET(fd, &exceptfds);

    while (1) {
	int status = select(fd + 1, &readfds, (fd_set *) NULL,
			    &exceptfds, &tout);

	if (status == 0) {
	    report(LOG_DEBUG, "%s: timeout reading fd %d", session.peer, fd);
	    return (-1);
	}
	if (status < 0) {
	    if (errno == EINTR)
		continue;
	    report(LOG_DEBUG, "%s: error in select %s fd %d",
		   session.peer, sys_errlist[errno], fd);
	    return (-1);
	}
	if (FD_ISSET(fd, &exceptfds)) {
	    report(LOG_DEBUG, "%s: exception on fd %d",
		   session.peer, fd);
	    return (-1);
	}
	if (!FD_ISSET(fd, &readfds)) {
	    report(LOG_DEBUG, "%s: spurious return from select",
		   session.peer);
	    continue;
	}
	nread = read(fd, ptr, nbytes);

	if (nread < 0) {
	    if (errno == EINTR) {
		continue;
	    }
	    report(LOG_DEBUG, "%s %s: error reading fd %d nread=%d %s",
		 session.peer, session.port, fd, nread, sys_errlist[errno]);
	    return (-1);	/* error */
	}
	if (nread == 0) {
	    return (-1);	/* eof */
	}
	return (nread);
    }
    /* NOTREACHED */
}

#ifdef ENABLE_FINGER

/*
 * Contact a NAS (using finger) to check how many sessions this USER
 * is currently running on it.
 *
 * Note that typically you run this code when you are in the middle of
 * trying to login to a Cisco NAS on a given port. Because you are
 * part way through a login when you do this, you can get inconsistent
 * reports for that particular port about whether the user is
 * currently logged in on it or not, so we ignore output which claims
 * that the user is using that line currently.
 *
 * This is extremely Cisco specific -- finger formats appear to vary wildly.
 * The format we're expecting is:

    Line     User      Host(s)               Idle Location
   0 con 0             idle                 never
  18 vty 0   usr0      idle                    30 barley.cisco.com
  19 vty 1   usr0      Virtual Exec             2
  20 vty 2             idle                     0 barley.cisco.com

 * Column zero contains a space or an asterisk character.  The line number
 * starts at column 1 and is 3 digits wide.  User names start at column 13,
 * with a maximum possible width of 10.
 */

static int
ckfinger(user, nas, idp)
char *user, *nas;
struct identity *idp;
{
    struct sockaddr_in sin;
    struct servent *serv;
    int count, s, bufsize;
    char *buf, *p, *pn;
    int incr = 4096, slop = 32;
    u_long inaddr;
    char *curport = portname(idp->NAS_port);
    char *name;

    /* The finger service, aka port 79 */
    serv = getservbyname("finger", "tcp");
    if (serv) {
	sin.sin_port = serv->s_port;
    } else {
	sin.sin_port = 79;
    }

    /* Get IP addr for the NAS */
    inaddr = inet_addr(nas);
    if (inaddr != -1) {
	/* A dotted decimal address */
	bcopy(&inaddr, &sin.sin_addr, sizeof(inaddr));
	sin.sin_family = AF_INET;
    } else {
	struct hostent *host = gethostbyname(nas);

	if (host == NULL) {
	    report(LOG_ERR, "ckfinger: gethostbyname %s failure: %s",
		   nas, sys_errlist[errno]);
	    return (0);
	}
	bcopy(host->h_addr, &sin.sin_addr, host->h_length);
	sin.sin_family = host->h_addrtype;
    }

    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
	report(LOG_ERR, "ckfinger: socket: %s", sys_errlist[errno]);
	return (0);
    }
    if (connect(s, (struct sockaddr *) & sin, sizeof(sin)) < 0) {
	report(LOG_ERR, "ckfinger: connect failure %s", sys_errlist[errno]);
	close(s);
	return (0);
    }
    /* Read in the finger output into a single flat buffer */
    buf = NULL;
    bufsize = 0;
    for (;;) {
	int x;

	buf = tac_realloc(buf, bufsize + incr + slop);
	x = timed_read(s, buf + bufsize, incr, 10);
	if (x <= 0) {
	    break;
	}
	bufsize += x;
    }

    /* Done talking here */
    close(s);
    buf[bufsize] = '\0';

    if (bufsize <= 0) {
	report(LOG_ERR, "ckfinger: finger failure");
	free(buf);
	return (0);
    }
    /* skip first line in buffer */
    p = strchr(buf, '\n');
    if (p) {
	p++;
    }
    p = strchr(p, '\n');
    if (p) {
	p++;
    }
    /* Tally each time this user appears */
    for (count = 0; p && *p; p = pn) {
	int i, len, nmlen;
	char nmbuf[11];

	/* Find next line */
	pn = strchr(p, '\n');
	if (pn) {
	    ++pn;
	}
	/* Calculate line length */
	if (pn) {
	    len = pn - p;
	} else {
	    len = strlen(p);
	}

	/* Line too short -> ignore */
	if (len < 14) {
	    continue;
	}
	/* Always ignore the NAS/port we're currently trying to login on. */
	if (isdigit(*curport)) {
	    int thisport;

	    if (sscanf(p + 1, " %d", &thisport) == 1) {
		if ((atoi(curport) == thisport) &&
		    !strcmp(idp->NAS_name, nas)) {

		    if (debug & DEBUG_MAXSESS_FLAG) {
			report(LOG_DEBUG, "%s session on %s/%s discounted",
			       user, idp->NAS_name, idp->NAS_port);
		    }
		    continue;
		}
	    }
	}
	/* Extract username, up to 10 chars wide, starting at char 13 */
	nmlen = 0;
	name = p + 13;
	for (i = 0; *name && !isspace(*name) && (i < 10); i++) {
	    nmbuf[nmlen++] = *name++;
	}
	nmbuf[nmlen++] = '\0';

	/* If name matches, up the count */
	if (STREQ(user, nmbuf)) {
	    count++;

	    if (debug & DEBUG_MAXSESS_FLAG) {
		char c = *pn;

		*pn = '\0';
		report(LOG_DEBUG, "%s matches: %s", user, p);
		*pn = c;
	    }
	}
    }
    free(buf);
    return (count);
}

/*
 * Verify how many sessions a user has according to the wholog file.
 * Use finger to contact each NAS that wholog says has this user
 * logged on.
 */
static int
countusers_by_finger(idp)
struct identity *idp;
{
    FILE *fp;
    struct peruser pu;
    int x, naddr, nsess, n;
    char **addrs, *uname;

    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	return (0);
    }
    uname = idp->username;

    /* Count sessions */
    tac_lockfd(wholog, fileno(fp));
    nsess = 0;
    naddr = 0;
    addrs = NULL;

    while (fread(&pu, sizeof(pu), 1, fp) > 0) {
	int dup;

	/* Ignore records for everyone except this user */
	if (strcmp(pu.username, uname)) {
	    continue;
	}
	/* Only check a given NAS once */
	for (dup = 0, x = 0; x < naddr; ++x) {
	    if (STREQ(addrs[x], pu.NAS_name)) {
		dup = 1;
		break;
	    }
	}
	if (dup) {
	    continue;
	}
	/* Add this address to our list */
	addrs = (char **) tac_realloc((char *) addrs,
				      (naddr + 1) * sizeof(char *));
	addrs[naddr] = tac_strdup(pu.NAS_name);
	naddr += 1;

	/* Validate via finger */
	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "Running finger on %s for user %s/%s",
		   pu.NAS_name, uname, idp->NAS_port);
	}
	n = ckfinger(uname, pu.NAS_name, idp);

	if (debug & DEBUG_MAXSESS_FLAG) {
	    report(LOG_DEBUG, "finger reports %d active session%s for %s on %s",
		   n, (n == 1 ? "" : "s"), uname, pu.NAS_name);
	}
	nsess += n;
    }

    /* Clean up and return */
    fclose(fp);
    for (x = 0; x < naddr; ++x) {
	free(addrs[x]);
    }
    free(addrs);

    return (nsess);
}
#endif /* ENABLE_FINGER */

/*
 * Estimate how many sessions a named user currently owns by looking in
 * the wholog file.
 */
static int
countuser(idp)
struct identity *idp;
{
    FILE *fp;
    struct peruser pu;
    int nsess;

    /* Access log */
    fp = fopen(wholog, "r+");
    if (fp == NULL) {
	return (0);
    }
    /* Count sessions. Skip any session associated with the current port. */
    tac_lockfd(wholog, fileno(fp));
    nsess = 0;
    while (fread(&pu, sizeof(pu), 1, fp) > 0) {

	/* Current user */
	if (strcmp(pu.username, idp->username)) {
	    continue;
	}
	/* skip current port on current NAS */
	if (STREQ(portname(pu.NAS_port), portname(idp->NAS_port)) &&
	    STREQ(pu.NAS_name, idp->NAS_name)) {
	    continue;
	}
	nsess += 1;
    }

    /* Clean up and return */
    fclose(fp);
    return (nsess);
}

/*
 * is_async()
 * Tell if the named NAS port is an async-like device.
 *
 * Finger reports async users, but not ISDN ones (yay).  So we can do
 * a "slow" double check for async, but not ISDN.
 */
static int
is_async(portname)
char *portname;
{
    if (isdigit(*portname) || !strncmp(portname, "Async", 5) ||
	!strncmp(portname, "tty", 3)) {
	return (1);
    }
    return (0);
}

/*
 * See if this user can have more sessions.
 */
int
maxsess_check_count(user, data)
char *user;
struct author_data *data;
{
    int sess, maxsess;
    struct identity *id;

    /* No max session configured--don't check */
    id = data->id;

    maxsess = cfg_get_intvalue(user, TAC_IS_USER, S_maxsess, TAC_PLUS_RECURSE);
    if (!maxsess) {
	if (debug & (DEBUG_MAXSESS_FLAG | DEBUG_AUTHOR_FLAG)) {
	    report(LOG_DEBUG, "%s may run an unlimited number of sessions",
		   user);
	}
	return (0);
    }
    /* Count sessions for this user by looking in our wholog file */
    sess = countuser(id);

    if (debug & (DEBUG_MAXSESS_FLAG | DEBUG_AUTHOR_FLAG)) {
	report(LOG_DEBUG,
	       "user %s is running %d out of a maximum of %d sessions",
	       user, sess, maxsess);
    }
#ifdef ENABLE_FINGER
    if ((sess >= maxsess) && is_async(id->NAS_port)) {
	/* If we have finger available, double check this count by contacting
	 * the NAS */
	sess = countusers_by_finger(id);
    }
#endif

    /* If it's really too high, don't authorize more services */
    if (sess >= maxsess) {
	char buf[80];

	sprintf(buf,
		"Login failed; too many active sessions (%d maximum)",
		maxsess);

	data->msg = tac_strdup(buf);

	if (debug & (DEBUG_AUTHOR_FLAG | DEBUG_MAXSESS_FLAG)) {
	    report(LOG_DEBUG, data->msg);
	}
	data->status = AUTHOR_STATUS_FAIL;
	data->output_args = NULL;
	data->num_out_args = 0;
	return (1);
    }
    return (0);
}

#else				/* MAXSESS */

/*
 * The following code is not needed or used. It exists solely to
 * prevent compilers from "helpfully" complaining that this source
 * file is empty when MAXSESS is not defined. This upsets novices
 * building the software, and I get complaints
 */

static int dummy = 0;

#endif				/* MAXSESS */
